mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-27 17:58:07 +00:00
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:
parent
bb98595694
commit
e5d21e87cb
4 changed files with 114 additions and 0 deletions
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: '実行中',
|
||||
|
|
|
|||
|
|
@ -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: '執行中',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue