diff --git a/desktop/src/i18n/locales/en.ts b/desktop/src/i18n/locales/en.ts
index fb5ce29e..11395f30 100644
--- a/desktop/src/i18n/locales/en.ts
+++ b/desktop/src/i18n/locales/en.ts
@@ -888,6 +888,26 @@ export const en = {
'chat.userMessageReference': 'User message',
'chat.assistantMessageReference': 'Assistant message',
'chat.slashCommands': 'Slash commands',
+ 'chat.goalEvent.created': 'Goal created',
+ 'chat.goalEvent.replaced': 'Goal replaced',
+ 'chat.goalEvent.statusTitle': 'Goal status',
+ 'chat.goalEvent.paused': 'Goal paused',
+ 'chat.goalEvent.resumed': 'Goal resumed',
+ 'chat.goalEvent.completed': 'Goal completed',
+ 'chat.goalEvent.cleared': 'Goal cleared',
+ 'chat.goalEvent.message': 'Goal update',
+ 'chat.goalEvent.objective': 'Objective: {value}',
+ 'chat.goalEvent.statusValue': 'Status: {value}',
+ 'chat.goalEvent.budget': 'Budget: {value}',
+ 'chat.goalEvent.continuations': 'Continuations: {value}',
+ 'chat.activeGoal.title': 'Active goal',
+ 'chat.activeGoal.running': 'Loop running',
+ 'chat.activeGoal.active': 'Active',
+ 'chat.activeGoal.paused': 'Paused',
+ 'chat.activeGoal.completed': 'Completed',
+ 'chat.activeGoal.budget': 'Budget {value}',
+ 'chat.activeGoal.continuations': 'Continuations {value}',
+ 'chat.activeGoal.elapsed': 'Elapsed {value}',
'slash.mcp.title': 'Available MCP tools',
'slash.mcp.subtitle': 'Global and current-project MCP servers available in this chat context.',
'slash.mcp.subtitleWithProject': 'Showing global plus project MCP for: {path}',
diff --git a/desktop/src/i18n/locales/zh.ts b/desktop/src/i18n/locales/zh.ts
index 7a87d605..675d700d 100644
--- a/desktop/src/i18n/locales/zh.ts
+++ b/desktop/src/i18n/locales/zh.ts
@@ -890,6 +890,26 @@ export const zh: Record
= {
'chat.userMessageReference': '用户消息',
'chat.assistantMessageReference': 'AI 回复',
'chat.slashCommands': '斜杠命令',
+ 'chat.goalEvent.created': '目标已创建',
+ 'chat.goalEvent.replaced': '目标已替换',
+ 'chat.goalEvent.statusTitle': '目标状态',
+ 'chat.goalEvent.paused': '目标已暂停',
+ 'chat.goalEvent.resumed': '目标已恢复',
+ 'chat.goalEvent.completed': '目标已完成',
+ 'chat.goalEvent.cleared': '目标已清除',
+ 'chat.goalEvent.message': '目标更新',
+ 'chat.goalEvent.objective': '目标:{value}',
+ 'chat.goalEvent.statusValue': '状态:{value}',
+ 'chat.goalEvent.budget': '预算:{value}',
+ 'chat.goalEvent.continuations': '续作次数:{value}',
+ 'chat.activeGoal.title': '当前目标',
+ 'chat.activeGoal.running': '自循环运行中',
+ 'chat.activeGoal.active': '进行中',
+ 'chat.activeGoal.paused': '已暂停',
+ 'chat.activeGoal.completed': '已完成',
+ 'chat.activeGoal.budget': '预算 {value}',
+ 'chat.activeGoal.continuations': '续作次数 {value}',
+ 'chat.activeGoal.elapsed': '已用时 {value}',
'slash.mcp.title': '可用 MCP 工具',
'slash.mcp.subtitle': '展示当前聊天上下文里的全局 MCP 和当前项目 MCP。',
'slash.mcp.subtitleWithProject': '当前展示全局 MCP 以及这个项目的 MCP:{path}',
diff --git a/desktop/src/pages/ActiveSession.test.tsx b/desktop/src/pages/ActiveSession.test.tsx
index 5eeda9a2..a22c4648 100644
--- a/desktop/src/pages/ActiveSession.test.tsx
+++ b/desktop/src/pages/ActiveSession.test.tsx
@@ -135,6 +135,78 @@ describe('ActiveSession task polling', () => {
expect(screen.getByTestId('chat-input')).toHaveAttribute('data-variant', 'default')
})
+ it('shows the current goal as a persistent session status panel', () => {
+ const sessionId = 'goal-visible-session'
+
+ useSessionStore.setState({
+ sessions: [{
+ id: sessionId,
+ title: 'Goal Visible Session',
+ createdAt: '2026-05-07T00:00:00.000Z',
+ modifiedAt: '2026-05-07T00:00:00.000Z',
+ messageCount: 1,
+ projectPath: '/workspace/project',
+ workDir: '/workspace/project',
+ workDirExists: true,
+ }],
+ activeSessionId: sessionId,
+ isLoading: false,
+ error: null,
+ })
+ useTabStore.setState({
+ tabs: [{ sessionId, title: 'Goal Visible Session', type: 'session', status: 'running' }],
+ activeTabId: sessionId,
+ })
+ useChatStore.setState({
+ sessions: {
+ [sessionId]: {
+ messages: [{
+ id: 'goal-event',
+ type: 'goal_event',
+ action: 'created',
+ status: 'active',
+ objective: 'ship the smoke test',
+ budget: '0 / 2,000 tokens',
+ continuations: '0',
+ timestamp: 1,
+ }],
+ activeGoal: {
+ action: 'created',
+ status: 'active',
+ objective: 'ship the smoke test',
+ budget: '0 / 2,000 tokens',
+ continuations: '0',
+ updatedAt: 1,
+ },
+ chatState: 'thinking',
+ connectionState: 'connected',
+ streamingText: '',
+ streamingToolInput: '',
+ activeToolUseId: null,
+ activeToolName: null,
+ activeThinkingId: null,
+ pendingPermission: null,
+ pendingComputerUsePermission: null,
+ tokenUsage: { input_tokens: 0, output_tokens: 0 },
+ elapsedSeconds: 0,
+ statusVerb: '',
+ slashCommands: [],
+ agentTaskNotifications: {},
+ elapsedTimer: null,
+ },
+ },
+ })
+
+ render()
+
+ const panel = screen.getByTestId('active-goal-panel')
+ expect(panel).toHaveTextContent('当前目标')
+ expect(panel).toHaveTextContent('自循环运行中')
+ expect(panel).toHaveTextContent('ship the smoke test')
+ expect(panel).toHaveTextContent('预算 0 / 2,000 tokens')
+ expect(panel).toHaveTextContent('续作次数 0')
+ })
+
it('refreshes CLI tasks repeatedly while a turn is active', async () => {
vi.useFakeTimers()
diff --git a/desktop/src/pages/ActiveSession.tsx b/desktop/src/pages/ActiveSession.tsx
index 60198917..1bac497a 100644
--- a/desktop/src/pages/ActiveSession.tsx
+++ b/desktop/src/pages/ActiveSession.tsx
@@ -28,6 +28,7 @@ import { TerminalSettings } from './TerminalSettings'
import type { SessionListItem } from '../types/session'
import { useMobileViewport } from '../hooks/useMobileViewport'
import { isTauriRuntime } from '../lib/desktopRuntime'
+import type { ActiveGoalState } from '../types/chat'
const TASK_POLL_INTERVAL_MS = 1000
const WORKSPACE_RESIZE_STEP = 32
@@ -199,6 +200,62 @@ function TerminalResizeHandle() {
)
}
+function GoalStatusPanel({
+ goal,
+ isRunning,
+ compact,
+}: {
+ goal: ActiveGoalState
+ isRunning: boolean
+ compact: boolean
+}) {
+ const t = useTranslation()
+ const stateLabel = goal.action === 'completed'
+ ? t('chat.activeGoal.completed')
+ : goal.action === 'paused' || goal.status === 'paused'
+ ? t('chat.activeGoal.paused')
+ : isRunning && goal.status !== 'complete'
+ ? t('chat.activeGoal.running')
+ : t('chat.activeGoal.active')
+
+ return (
+
+
+
track_changes
+
+
+
+ {t('chat.activeGoal.title')}
+
+
+ {stateLabel}
+
+ {goal.objective && (
+
+ {goal.objective}
+
+ )}
+
+ {(goal.budget || goal.continuations || goal.elapsed) && (
+
+ {goal.budget && {t('chat.activeGoal.budget', { value: goal.budget })}}
+ {goal.continuations && {t('chat.activeGoal.continuations', { value: goal.continuations })}}
+ {goal.elapsed && {t('chat.activeGoal.elapsed', { value: goal.elapsed })}}
+
+ )}
+
+
+
+ )
+}
+
export function ActiveSession() {
const isMobileLayout = useMobileViewport() && !isTauriRuntime()
const activeTabId = useTabStore((s) => s.activeTabId)
@@ -211,6 +268,7 @@ export function ActiveSession() {
const trackedTaskSessionId = useCLITaskStore((s) => s.sessionId)
const hasIncompleteTasks = useCLITaskStore((s) => s.tasks.some((task) => task.status !== 'completed'))
const chatState = sessionState?.chatState ?? 'idle'
+ const activeGoal = sessionState?.activeGoal ?? null
const tokenUsage = sessionState?.tokenUsage ?? { input_tokens: 0, output_tokens: 0 }
const session = sessions.find((s) => s.id === activeTabId)
@@ -420,6 +478,14 @@ export function ActiveSession() {
)}
+ {activeGoal && (
+