chenbhao commited on
Commit
e59f3a6
·
1 Parent(s): 4e35993

feat: goal dot status color cycle

Browse files

问题找到了。goalStatusColor 返回 'red', 'yellow' 等原始颜色字符串,但 ThemedText.resolveColor() 期望的是:
- 要么是 theme key(如 claude, success 等)
- 要么是带 ansi: 前缀的原始颜色(如 'ansi:red')

由于 'red' 既不是 theme key 也没有前缀,resolveColor 找不到对应颜色就返回 undefined,导致无色(灰色)。

修复方案:goalStatusColor 返回带 'ansi:' 前缀的颜色字符串。

● Update(src/components/PromptInput/GoalIndicator.tsx)
⎿  Added 6 lines, removed 6 lines
8 export function goalStatusColor(status: GoalStatus): string {
9 switch (status) {
10 case 'pursuing':
11 - return 'yellow'
11 + return 'ansi:yellow'
12 case 'paused':
13 - return 'gray'
13 + return 'ansi:gray'
14 case 'achieved':
15 - return 'green'
15 + return 'ansi:green'
16 case 'blocked':
17 - return 'red'
17 + return 'ansi:red'
18 case 'usage-limited':
19 - return 'gray'
19 + return 'ansi:gray'
20 case 'budget-limited':
21 - return 'magenta'
21 + return 'ansi:magenta'
22 }
23 }
24

● 现在验证构建是否通过。

● Bash(bun run tsc --noEmit 2>&1 | head -20)
⎿  tsconfig.json(10,5): error TS5101: Option 'baseUrl' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption 'ignoreDeprecatio

src/components/PromptInput/GoalIndicator.tsx CHANGED
@@ -8,17 +8,17 @@ const MAX_OBJECTIVE_CHARS = 40
8
  export function goalStatusColor(status: GoalStatus): string {
9
  switch (status) {
10
  case 'pursuing':
11
- return 'yellow'
12
  case 'paused':
13
- return 'gray'
14
  case 'achieved':
15
- return 'green'
16
  case 'blocked':
17
- return 'red'
18
  case 'usage-limited':
19
- return 'gray'
20
  case 'budget-limited':
21
- return 'magenta'
22
  }
23
  }
24
 
 
8
  export function goalStatusColor(status: GoalStatus): string {
9
  switch (status) {
10
  case 'pursuing':
11
+ return 'ansi:yellow'
12
  case 'paused':
13
+ return 'ansi:gray'
14
  case 'achieved':
15
+ return 'ansi:green'
16
  case 'blocked':
17
+ return 'ansi:red'
18
  case 'usage-limited':
19
+ return 'ansi:gray'
20
  case 'budget-limited':
21
+ return 'ansi:magenta'
22
  }
23
  }
24
 
src/components/PromptInput/PromptInputFooterLeftSide.tsx CHANGED
@@ -26,7 +26,7 @@ import { isInProcessEnabled } from '../../utils/swarm/backends/registry.js';
26
  import { useAppState, useAppStateStore } from 'src/state/AppState.js';
27
  import { getIsRemoteMode } from '../../bootstrap/state.js';
28
  import HistorySearchInput from './HistorySearchInput.js';
29
- import { GoalIndicator } from './GoalIndicator.js';
30
  import { usePrStatus } from '../../hooks/usePrStatus.js';
31
  import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
32
  import { Byline } from '../design-system/Byline.js';
@@ -261,6 +261,8 @@ function ModeIndicator({
261
  const expandedView = useAppState(s_3 => s_3.expandedView);
262
  const showSpinnerTree = expandedView === 'teammates';
263
  const prStatus = usePrStatus(isLoading, isPrStatusEnabled());
 
 
264
  const hasTmuxSession = useAppState(s_4 => "external" === 'ant' && s_4.tungstenActiveSession !== undefined);
265
  const nextTickAt = useSyncExternalStore(proactiveModule?.subscribeToProactiveChanges ?? NO_OP_SUBSCRIBE, proactiveModule?.getNextTickAt ?? NULL, NULL);
266
  // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
@@ -355,6 +357,19 @@ function ModeIndicator({
355
  </Text>}
356
  </Text> : null;
357
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  // Build parts array - exclude BackgroundTaskStatus when we have teammate pills
359
  // (teammate pills get their own row)
360
  const parts = [
@@ -366,18 +381,18 @@ function ModeIndicator({
366
  // its click-target Box isn't nested inside the <Text wrap="truncate">
367
  // wrapper (reconciler throws on Box-in-Text).
368
  // Tmux pill (ant-only) — appears right after tasks in nav order
369
- ...("external" === 'ant' && hasTmuxSession ? [<TungstenPill key="tmux" selected={tmuxSelected} />] : []), ...(isAgentSwarmsEnabled() && hasTeams ? [<TeamStatus key="teams" teamsSelected={teamsSelected} showHint={showHint && !hasBackgroundTasks} />] : []), ...(shouldShowPrStatus ? [<PrBadge key="pr-status" number={prStatus.number!} url={prStatus.url!} reviewState={prStatus.reviewState!} />] : [])];
370
-
371
- // Goal indicator renders on its own row at the bottom — not mixed into the
372
- // main parts line so it doesn't get truncated when the terminal is narrow.
373
- const goalPart = <GoalIndicator key="goal" />;
374
 
375
  // Check if any in-process teammates exist (for hint text cycling)
376
- const hasAnyInProcessTeammates = Object.values(tasks).some(t_2 => t_2.type === 'in_process_teammate' && t_2.status === 'running');
377
- const hasRunningAgentTasks = Object.values(tasks).some(t_3 => t_3.type === 'local_agent' && t_3.status === 'running');
378
 
379
  // Get hint parts separately for potential second-line rendering
380
  const hintParts = showHint ? getSpinnerHintParts(isLoading, escShortcut, todosShortcut, killAgentsShortcut, hasTaskItems, expandedView, hasAnyInProcessTeammates, hasRunningAgentTasks, isKillAgentsConfirmShowing) : [];
 
381
  if (isViewingCompletedTeammate) {
382
  parts.push(<Text dimColor key="esc-return">
383
  <KeyboardShortcutHint shortcut={escShortcut} action="return to team lead" />
@@ -388,22 +403,6 @@ function ModeIndicator({
388
  parts.push(...hintParts);
389
  }
390
 
391
- // When we have teammate pills, always render them on their own line above other parts
392
- if (hasTeammatePills) {
393
- // Don't append spinner hints when viewing a completed teammate —
394
- // the "esc to return to team lead" hint already replaces "esc to interrupt"
395
- const otherParts = [...(modePart ? [modePart] : []), ...parts, ...(isViewingCompletedTeammate ? [] : hintParts)];
396
- return <Box flexDirection="column">
397
- <Box>
398
- <BackgroundTaskStatus tasksSelected={tasksSelected} isViewingTeammate={isViewingTeammate} teammateFooterIndex={teammateFooterIndex} isLeaderIdle={!isLoading} onOpenDialog={onOpenTasksDialog} />
399
- </Box>
400
- {otherParts.length > 0 && <Box>
401
- <Byline>{otherParts}</Byline>
402
- </Box>}
403
- {goalPart && <Box><Byline>{goalPart}</Byline></Box>}
404
- </Box>;
405
- }
406
-
407
  // Add "↓ to manage tasks" hint when panel has visible rows
408
  const hasCoordinatorTasks = "external" === 'ant' && getVisibleAgentTasks(tasks).length > 0;
409
 
@@ -468,7 +467,7 @@ function ModeIndicator({
468
  // from 0→1 row. Always render 1 row in fullscreen; return a space when
469
  // empty so Yoga reserves the row without painting anything visible.
470
  const hasMainRowContent = modePart || tasksPart || parts.length > 0;
471
- if (!hasMainRowContent && !goalPart) {
472
  return isFullscreenEnvEnabled() ? <Text> </Text> : null;
473
  }
474
  // When there IS a goal but no other main content, we still need to render
@@ -483,19 +482,19 @@ function ModeIndicator({
483
  <Box height={1} overflow="hidden">
484
  {modePart && <Box flexShrink={0}>
485
  {modePart}
486
- {(tasksPart || parts.length > 0 || goalPart) && <Text dimColor> · </Text>}
487
  </Box>}
488
  {tasksPart && <Box flexShrink={0}>
489
  {tasksPart}
490
- {(parts.length > 0 || goalPart) && <Text dimColor> · </Text>}
491
  </Box>}
492
  {parts.length > 0 && <Text wrap="truncate">
493
  <Byline>{parts}</Byline>
494
  </Text>}
495
- {parts.length === 0 && !tasksPart && !modePart && !goalPart && isFullscreenEnvEnabled() && <Text> </Text>}
496
- {parts.length === 0 && !tasksPart && !modePart && !goalPart && !isFullscreenEnvEnabled() && null}
497
  </Box>
498
- {goalPart && <Box height={1}><Byline>{goalPart}</Byline></Box>}
499
  </Box>;
500
  }
501
  function getSpinnerHintParts(isLoading: boolean, escShortcut: string, todosShortcut: string, killAgentsShortcut: string, hasTaskItems: boolean, expandedView: 'none' | 'tasks' | 'teammates', hasTeammates: boolean, hasRunningAgentTasks: boolean, isKillAgentsConfirmShowing: boolean): React.ReactElement[] {
 
26
  import { useAppState, useAppStateStore } from 'src/state/AppState.js';
27
  import { getIsRemoteMode } from '../../bootstrap/state.js';
28
  import HistorySearchInput from './HistorySearchInput.js';
29
+ import { goalStatusColor } from './GoalIndicator.js';
30
  import { usePrStatus } from '../../hooks/usePrStatus.js';
31
  import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
32
  import { Byline } from '../design-system/Byline.js';
 
261
  const expandedView = useAppState(s_3 => s_3.expandedView);
262
  const showSpinnerTree = expandedView === 'teammates';
263
  const prStatus = usePrStatus(isLoading, isPrStatusEnabled());
264
+ const goal = useAppState(s => s.goal);
265
+ const goalMode = useAppState(s => s.toolPermissionContext.mode);
266
  const hasTmuxSession = useAppState(s_4 => "external" === 'ant' && s_4.tungstenActiveSession !== undefined);
267
  const nextTickAt = useSyncExternalStore(proactiveModule?.subscribeToProactiveChanges ?? NO_OP_SUBSCRIBE, proactiveModule?.getNextTickAt ?? NULL, NULL);
268
  // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
 
357
  </Text>}
358
  </Text> : null;
359
 
360
+ // Inline goal indicator — reads goal state directly to avoid React compiler
361
+ // caching the dot color across status changes.
362
+ const GOAL_MAX_CHARS = 40;
363
+ const goalTruncated = goal && goal.objective.length > GOAL_MAX_CHARS
364
+ ? goal.objective.slice(0, GOAL_MAX_CHARS - 1) + '…'
365
+ : goal?.objective;
366
+ const goalPlanSuppressed = goal?.status === 'pursuing' && goalMode === 'plan';
367
+ const goalLabel = goalPlanSuppressed ? 'paused: plan mode' : goal?.status;
368
+ const goalDotColor = goalPlanSuppressed ? 'yellow' : goal ? goalStatusColor(goal.status) : undefined;
369
+ const goalInline = goal
370
+ ? <Text><Text color={goalDotColor}>●</Text><Text dimColor> goal: </Text><Text>{goalTruncated}</Text><Text dimColor> [{goalLabel}]</Text></Text>
371
+ : null;
372
+
373
  // Build parts array - exclude BackgroundTaskStatus when we have teammate pills
374
  // (teammate pills get their own row)
375
  const parts = [
 
381
  // its click-target Box isn't nested inside the <Text wrap="truncate">
382
  // wrapper (reconciler throws on Box-in-Text).
383
  // Tmux pill (ant-only) — appears right after tasks in nav order
384
+ ...("external" === 'ant' && hasTmuxSession ? [<TungstenPill key="tmux" selected={tmuxSelected} />] : []),
385
+ ...(isAgentSwarmsEnabled() && hasTeams ? [<TeamStatus key="teams" teamsSelected={teamsSelected} showHint={showHint && !hasBackgroundTasks} />] : []),
386
+ ...(shouldShowPrStatus ? [<PrBadge key="pr-status" number={prStatus.number} url={prStatus.url} reviewState={prStatus.reviewState!} />] : []),
387
+ ]
 
388
 
389
  // Check if any in-process teammates exist (for hint text cycling)
390
+ const hasAnyInProcessTeammates = Object.values(tasks).some(t => t.type === 'in_process_teammate' && t.status === 'running');
391
+ const hasRunningAgentTasks = Object.values(tasks).some(t => t.type === 'local_agent' && t.status === 'running');
392
 
393
  // Get hint parts separately for potential second-line rendering
394
  const hintParts = showHint ? getSpinnerHintParts(isLoading, escShortcut, todosShortcut, killAgentsShortcut, hasTaskItems, expandedView, hasAnyInProcessTeammates, hasRunningAgentTasks, isKillAgentsConfirmShowing) : [];
395
+
396
  if (isViewingCompletedTeammate) {
397
  parts.push(<Text dimColor key="esc-return">
398
  <KeyboardShortcutHint shortcut={escShortcut} action="return to team lead" />
 
403
  parts.push(...hintParts);
404
  }
405
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
  // Add "↓ to manage tasks" hint when panel has visible rows
407
  const hasCoordinatorTasks = "external" === 'ant' && getVisibleAgentTasks(tasks).length > 0;
408
 
 
467
  // from 0→1 row. Always render 1 row in fullscreen; return a space when
468
  // empty so Yoga reserves the row without painting anything visible.
469
  const hasMainRowContent = modePart || tasksPart || parts.length > 0;
470
+ if (!hasMainRowContent && !goalInline) {
471
  return isFullscreenEnvEnabled() ? <Text> </Text> : null;
472
  }
473
  // When there IS a goal but no other main content, we still need to render
 
482
  <Box height={1} overflow="hidden">
483
  {modePart && <Box flexShrink={0}>
484
  {modePart}
485
+ {(tasksPart || parts.length > 0 || goalInline) && <Text dimColor> · </Text>}
486
  </Box>}
487
  {tasksPart && <Box flexShrink={0}>
488
  {tasksPart}
489
+ {(parts.length > 0 || goalInline) && <Text dimColor> · </Text>}
490
  </Box>}
491
  {parts.length > 0 && <Text wrap="truncate">
492
  <Byline>{parts}</Byline>
493
  </Text>}
494
+ {parts.length === 0 && !tasksPart && !modePart && !goalInline && isFullscreenEnvEnabled() && <Text> </Text>}
495
+ {parts.length === 0 && !tasksPart && !modePart && !goalInline && !isFullscreenEnvEnabled() && null}
496
  </Box>
497
+ {goalInline && <Box height={1}><Byline>{goalInline}</Byline></Box>}
498
  </Box>;
499
  }
500
  function getSpinnerHintParts(isLoading: boolean, escShortcut: string, todosShortcut: string, killAgentsShortcut: string, hasTaskItems: boolean, expandedView: 'none' | 'tasks' | 'teammates', hasTeammates: boolean, hasRunningAgentTasks: boolean, isKillAgentsConfirmShowing: boolean): React.ReactElement[] {