fix(desktop): hydrate goal indicator on /goal set and controls, full locale copy

Follow-ups on the salvaged goal-status display (#63527):

- Seed the goal store from the /goal dispatch notice ("⊙ Goal set …") and
  from /goal status|pause|resume|clear exec output in slash.ts. The backend
  only emits status.update kind:"goal" after the first turn's post-turn
  judge, so without this the indicator stayed empty while the kickoff turn
  ran (sweeper review finding on #63527).
- Add the missing ja / zh-hant statusStack goal copy — desktop ships four
  locales, not two (sweeper review finding on #55651).
- Add a component-level vitest for the composer goal indicator rendering
  from store states: none / active / paused / detail line / other-session.

Co-authored-by: HaisamAbbas <95044189+HaisamAbbas@users.noreply.github.com>

Assisted-by: Claude Fable 5 via Hermes Agent
This commit is contained in:
teknium1 2026-07-26 14:23:18 -07:00 committed by Teknium
parent bb98595694
commit e5d21e87cb
4 changed files with 114 additions and 0 deletions

View file

@ -0,0 +1,87 @@
import { cleanup, render, screen } from '@testing-library/react'
import { MemoryRouter } from 'react-router-dom'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { I18nProvider } from '@/i18n'
import { $goalsBySession, type SessionGoal } from '@/store/goals'
import { ComposerStatusStack } from './index'
// The stack measures itself into a surface var — jsdom has no ResizeObserver.
class ResizeObserverStub {
observe() {}
unobserve() {}
disconnect() {}
}
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
const SID = 'sess-goal-1'
const goal = (status: SessionGoal['status'], title = 'ship the feature', detail?: string): SessionGoal => ({
detail,
status,
title,
updatedAt: Date.now()
})
function renderStack(sessionId: null | string = SID) {
return render(
<MemoryRouter>
<I18nProvider configClient={null} initialLocale="en">
<ComposerStatusStack queue={null} sessionId={sessionId} />
</I18nProvider>
</MemoryRouter>
)
}
describe('ComposerStatusStack goal indicator', () => {
beforeEach(() => {
$goalsBySession.set({})
})
afterEach(() => {
cleanup()
$goalsBySession.set({})
})
it('renders nothing when the session has no goal', () => {
const view = renderStack()
expect(view.container.firstChild).toBeNull()
})
it('shows an active goal with its title', () => {
$goalsBySession.set({ [SID]: goal('active') })
renderStack()
expect(screen.getByText('Goal active')).toBeTruthy()
expect(screen.getByText('ship the feature')).toBeTruthy()
})
it('labels a paused goal as paused', () => {
$goalsBySession.set({ [SID]: goal('paused') })
renderStack()
expect(screen.getByText('Goal paused')).toBeTruthy()
expect(screen.getByText('ship the feature')).toBeTruthy()
})
it('shows the continuation detail line for an active goal', () => {
$goalsBySession.set({ [SID]: goal('active', 'ship it', 'Continuing toward goal (3/20)') })
renderStack()
expect(screen.getByText('Continuing toward goal (3/20)')).toBeTruthy()
})
it('scopes the indicator to the goal-owning session', () => {
$goalsBySession.set({ 'other-session': goal('active') })
const view = renderStack()
expect(view.container.firstChild).toBeNull()
})
})

View file

@ -18,6 +18,7 @@ import { setSessionYolo } from '@/lib/yolo-session'
import { openCommandPalettePage } from '@/store/command-palette'
import { setComposerDraft } from '@/store/composer'
import { enqueueQueuedPrompt } from '@/store/composer-queue'
import { applyGoalStatusText } from '@/store/goals'
import { dismissNotification, notify, notifyError } from '@/store/notifications'
import { setPetScale } from '@/store/pet-gallery'
import { $petGenInput, openPetGenerate } from '@/store/pet-generate'
@ -232,6 +233,15 @@ export function useSlashCommand(deps: SlashCommandDeps) {
// `/goal <text>` looked like it did nothing.
if ((dispatch.type === 'send' || dispatch.type === 'prefill') && dispatch.notice?.trim()) {
renderSlashOutput(dispatch.notice.trim())
// `/goal <text>` returns its "⊙ Goal set …" notice here and kicks
// off the first turn immediately; the backend only emits a
// `status.update kind:"goal"` after that turn's post-turn judge
// runs. Seed the goal store from the notice so the indicator shows
// the active goal right away instead of after the first turn.
if (name === 'goal') {
applyGoalStatusText(sessionId, dispatch.notice.trim())
}
}
const message = ('message' in dispatch ? dispatch.message : '')?.trim() ?? ''
@ -313,6 +323,15 @@ export function useSlashCommand(deps: SlashCommandDeps) {
const output = result && typeof result === 'object' ? (result as SlashExecResponse) : null
const body = output?.output || `/${name}: no output`
// `/goal status|pause|resume|clear` come back as plain exec output
// ("⊙ Goal (active, 3/20 turns): …", "⏸ Goal paused: …", "✓ Goal
// cleared." …). Mirror it into the goal store so the composer
// indicator tracks pause/resume/clear immediately.
if (name === 'goal' && output?.output) {
applyGoalStatusText(sessionId, output.output)
}
renderSlashOutput(output?.warning ? `warning: ${output.warning}\n${body}` : body)
return

View file

@ -1880,6 +1880,10 @@ export const ja = defineLocale({
statusStack: {
agents: 'エージェント',
background: count => `バックグラウンド ${count}`,
goalActive: '目標進行中',
goalDone: '目標達成',
goalPaused: '目標一時停止中',
goalWaiting: '目標待機中',
subagents: count => `サブエージェント ${count}`,
todos: (done, total) => `タスク ${done}/${total}`,
running: '実行中',

View file

@ -1825,6 +1825,10 @@ export const zhHant = defineLocale({
statusStack: {
agents: '代理',
background: count => `${count} 個背景任務`,
goalActive: '目標進行中',
goalDone: '目標已完成',
goalPaused: '目標已暫停',
goalWaiting: '目標等待中',
subagents: count => `${count} 個子代理`,
todos: (done, total) => `任務 ${done}/${total}`,
running: '執行中',