@steerable/agent-shell-web 0.6.53 → 0.6.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/package.json +4 -4
  2. package/src/brand.ts +1 -1
  3. package/src/components/AgentSidebar.tsx +60 -52
  4. package/src/components/ChatHeader.tsx +41 -25
  5. package/src/components/ConfirmDialog.tsx +4 -3
  6. package/src/components/CreateProjectModal.tsx +29 -19
  7. package/src/components/LocalLlmSettingsModal.tsx +4 -3
  8. package/src/components/SidebarRelease.tsx +2 -1
  9. package/src/components/TerminalPanel.tsx +3 -2
  10. package/src/components/TerminalView.tsx +12 -7
  11. package/src/components/chat/ApprovalModal.tsx +39 -25
  12. package/src/components/chat/ApprovalPromptProvider.tsx +1 -1
  13. package/src/components/chat/AskUserPromptProvider.tsx +1 -1
  14. package/src/components/chat/AskUserQuestionMenu.tsx +19 -14
  15. package/src/components/chat/AssistantMessage.tsx +18 -13
  16. package/src/components/chat/ChatInput.tsx +56 -40
  17. package/src/components/chat/ChatProjectBadge.tsx +24 -19
  18. package/src/components/chat/EmptyChatHero.tsx +2 -1
  19. package/src/components/chat/ExecPolicyPicker.tsx +12 -11
  20. package/src/components/chat/ExecutedActionsCard.tsx +45 -33
  21. package/src/components/chat/FilePathCode.tsx +8 -3
  22. package/src/components/chat/InterruptedTurnCard.tsx +4 -3
  23. package/src/components/chat/LocalChatPanel.tsx +2 -1
  24. package/src/components/chat/Markdown.tsx +2 -1
  25. package/src/components/chat/MessageList.tsx +3 -2
  26. package/src/components/chat/ModelPicker.tsx +25 -16
  27. package/src/components/chat/OrchestrationChildrenCard.tsx +2 -1
  28. package/src/components/chat/OrchestrationFlowCard.tsx +23 -22
  29. package/src/components/chat/PlanTodoList.tsx +2 -1
  30. package/src/components/chat/ProjectTrustBanner.tsx +19 -5
  31. package/src/components/chat/SessionOrchestrationFlow.tsx +27 -26
  32. package/src/components/chat/SessionTodoList.tsx +8 -7
  33. package/src/components/chat/SessionTreeModal.tsx +10 -9
  34. package/src/components/chat/StreamingStatus.tsx +4 -3
  35. package/src/components/chat/SuggestedReplies.tsx +3 -2
  36. package/src/components/chat/TaskOutcomeCards.tsx +8 -5
  37. package/src/components/chat/TaskPanelModal.tsx +31 -21
  38. package/src/components/chat/TaskProcessPanel.tsx +14 -13
  39. package/src/components/chat/TurnFilesCard.tsx +14 -13
  40. package/src/components/chat/UserMessage.tsx +12 -11
  41. package/src/components/chat/executed-actions-model.ts +47 -35
  42. package/src/components/chat/orchestration-flow-model.ts +16 -5
  43. package/src/components/chat/process-status.ts +8 -7
  44. package/src/components/chat/todo-list-model.ts +5 -4
  45. package/src/components/chat/turn-files.ts +8 -8
  46. package/src/components/settings/AgentCapabilityFields.tsx +24 -21
  47. package/src/components/settings/AgentsSettingsPanel.tsx +30 -25
  48. package/src/components/settings/AppUpdateSettingsPanel.tsx +3 -2
  49. package/src/components/settings/AppearanceSettingsPanel.tsx +81 -37
  50. package/src/components/settings/DiagnoseSettingsPanel.tsx +11 -8
  51. package/src/components/settings/InsightsSettingsPanel.tsx +43 -28
  52. package/src/components/settings/LlmSettingsPanel.tsx +104 -83
  53. package/src/components/settings/McpSettingsPanel.tsx +60 -34
  54. package/src/components/settings/ModelIdCombobox.tsx +2 -1
  55. package/src/components/settings/OrchestrationSettingsPanel.tsx +8 -5
  56. package/src/components/settings/PortableSettingsPanel.tsx +83 -55
  57. package/src/components/settings/PythonRunnerSettingsPanel.tsx +22 -19
  58. package/src/components/settings/SecuritySettingsPanel.tsx +55 -31
  59. package/src/components/settings/SettingsNavMenu.tsx +3 -2
  60. package/src/components/settings/SettingsSaveButton.tsx +2 -1
  61. package/src/components/settings/SkillsSettingsPanel.tsx +36 -31
  62. package/src/components/settings/TelemetrySettingsPanel.tsx +15 -12
  63. package/src/components/settings/UsagePanel.tsx +19 -20
  64. package/src/components/settings/WebSearchSettingsPanel.tsx +23 -14
  65. package/src/components/settings/llm-vendors.ts +12 -7
  66. package/src/components/settings/model-capabilities.ts +16 -15
  67. package/src/components/settings/preset-choice-model.ts +6 -3
  68. package/src/fixtures/browser-dev-data.json +24 -24
  69. package/src/hooks/useProjects.ts +1 -1
  70. package/src/i18n/agent-label.ts +15 -0
  71. package/src/i18n/index.tsx +68 -0
  72. package/src/i18n/runtime.ts +117 -0
  73. package/src/i18n/translate.ts +65 -0
  74. package/src/layouts/AgentLayout.tsx +7 -6
  75. package/src/lib/app-release.ts +15 -10
  76. package/src/lib/attachments.ts +16 -7
  77. package/src/lib/browser-dev-electron-mock.ts +34 -34
  78. package/src/lib/chat-title.ts +3 -2
  79. package/src/lib/chat-transport.ts +6 -2
  80. package/src/lib/date-groups.ts +23 -15
  81. package/src/lib/electron-bridge.ts +4 -5
  82. package/src/lib/http-bridge.ts +4 -1
  83. package/src/lib/portable.ts +8 -7
  84. package/src/lib/show-thinking-content.ts +15 -3
  85. package/src/main.tsx +25 -2
  86. package/src/pages/AgentPage.tsx +17 -12
  87. package/src/pages/ChatPanelPreviewPage.tsx +46 -44
  88. package/src/pages/SettingsPage.tsx +48 -42
  89. package/src/vite-env.d.ts +3 -0
  90. package/vite.base.js +6 -2
@@ -3,6 +3,7 @@ import { motion } from 'framer-motion';
3
3
  import { LuCheck, LuCopy, LuListChecks, LuLoaderCircle, LuRefreshCw, LuShare2 } from 'react-icons/lu';
4
4
  import type { ChatMessage } from '@steerable/agent-protocol';
5
5
  import type { LocalChat, LocalChatAgent } from '@/lib/local-api';
6
+ import { t } from '@/i18n';
6
7
  import { Markdown } from './Markdown';
7
8
  import { ExecutedActionsCard, type ExecutedAction } from './ExecutedActionsCard';
8
9
  import type { ChildInfo } from './OrchestrationChildrenCard';
@@ -228,7 +229,7 @@ function readPersistedAgentId(message: ChatMessage): string | undefined {
228
229
  function TurnErrorBubble({ reason }: { reason: string }) {
229
230
  return (
230
231
  <div className="rounded-agent-lg border border-agent-destructive/40 bg-agent-destructive/5 px-2.5 py-2 text-xs leading-relaxed text-agent-destructive shadow-sm">
231
- 请求失败:{reason}
232
+ {t('Request failed: {reason}', { reason })}
232
233
  </div>
233
234
  );
234
235
  }
@@ -339,7 +340,7 @@ export function AssistantMessage({
339
340
  {isPlanMode && (
340
341
  <span className="inline-flex items-center gap-1 rounded-full border border-amber-200 dark:border-amber-800/50 bg-amber-100/70 dark:bg-amber-900/30 px-1.5 py-0.5 text-[10px] font-medium text-amber-700 dark:text-amber-300 select-none">
341
342
  <LuListChecks className="h-3.5 w-3.5" />
342
- <span>计划模式 (Plan)</span>
343
+ <span>{t('Plan mode (Plan)')}</span>
343
344
  </span>
344
345
  )}
345
346
  </div>
@@ -369,7 +370,7 @@ export function AssistantMessage({
369
370
  </div>
370
371
  ) : (
371
372
  <div className={`${bubbleClass(isPlanMode)} text-xs italic text-agent-muted-foreground`}>
372
- (空消息)
373
+ {t('(empty message)')}
373
374
  </div>
374
375
  )
375
376
  }
@@ -427,7 +428,7 @@ export function AssistantMessage({
427
428
  </div>
428
429
  ) : (
429
430
  <div className={`${bubbleClass(isPlanMode)} text-xs italic text-agent-muted-foreground`}>
430
- (空消息)
431
+ {t('(empty message)')}
431
432
  </div>
432
433
  )}
433
434
  {displayContent && failure ? (
@@ -458,14 +459,14 @@ export function AssistantMessage({
458
459
  type="button"
459
460
  onClick={() => void copy()}
460
461
  className={META_ACTION}
461
- title={copied ? '已复制' : '复制消息'}
462
- aria-label={copied ? '已复制' : '复制消息'}
462
+ title={copied ? t('Copied') : t('Copy message')}
463
+ aria-label={copied ? t('Copied') : t('Copy message')}
463
464
  >
464
465
  {copied ? (
465
466
  <>
466
467
  <LuCheck className="h-3 w-3 text-emerald-600 dark:text-emerald-400" />
467
468
  <span className="text-emerald-600 dark:text-emerald-400">
468
- 已复制
469
+ {t('Copied')}
469
470
  </span>
470
471
  </>
471
472
  ) : (
@@ -493,8 +494,12 @@ export function AssistantMessage({
493
494
  .finally(() => setRegenerating(false));
494
495
  }}
495
496
  className={`${META_ACTION} disabled:cursor-not-allowed`}
496
- title={regenerating ? '正在重新生成…' : '重新生成(旧回复保留为分支)'}
497
- aria-label={regenerating ? '正在重新生成' : '重新生成'}
497
+ title={
498
+ regenerating
499
+ ? t('Regenerating...')
500
+ : t('Regenerate (the old reply is kept as a branch)')
501
+ }
502
+ aria-label={regenerating ? t('Regenerating') : t('Regenerate')}
498
503
  data-action="regenerate"
499
504
  >
500
505
  <LuRefreshCw
@@ -559,12 +564,12 @@ function ShareChatButton({ onShare }: { onShare: () => Promise<boolean> }) {
559
564
  }`}
560
565
  title={
561
566
  state === 'done'
562
- ? '截图已复制到剪贴板'
567
+ ? t('Screenshot copied to clipboard')
563
568
  : state === 'error'
564
- ? '截图失败,请重试'
565
- : '分享对话(截图复制到剪贴板)'
569
+ ? t('Screenshot failed. Try again.')
570
+ : t('Share chat (copy a screenshot to the clipboard)')
566
571
  }
567
- aria-label="分享对话截图"
572
+ aria-label={t('Share chat screenshot')}
568
573
  >
569
574
  {state === 'busy' ? (
570
575
  <LuLoaderCircle className="h-3 w-3 animate-spin" />
@@ -36,6 +36,8 @@ import { hostClipboardAvailable, requestHostPaste, type HostClipboardFile } from
36
36
  import { attachmentFromPath, type AttachmentFile } from '@/lib/attachments';
37
37
  import { dropPointHitsRect, hostFileDropAvailable, listenHostFileDrop } from '@/lib/host-file-drop';
38
38
  import type { SteerOutcome } from '@steerable/agent-ui';
39
+ import { t } from '@/i18n';
40
+ import { agentLabel } from '@/i18n/agent-label';
39
41
  import {
40
42
  isHiddenSlashSkill,
41
43
  resolveSlashTool,
@@ -278,11 +280,11 @@ function parseMentionSegments(
278
280
  if (!ranges.some((range) => offset < range.end && end > range.start)) {
279
281
  const name = match.slice(1);
280
282
  const agent = agents.find((a) => a.name === name || a.slug === name);
281
- const chat = chats.find((c) => (c.title || '未命名对话') === name);
283
+ const chat = chats.find((c) => (c.title || t('Untitled chat')) === name);
282
284
  const ref: MentionReference | undefined = agent
283
285
  ? { type: 'agent', id: agent.id, label: agent.name }
284
286
  : chat
285
- ? { type: 'chat', id: chat.id, label: chat.title || '未命名对话' }
287
+ ? { type: 'chat', id: chat.id, label: chat.title || t('Untitled chat') }
286
288
  : undefined;
287
289
 
288
290
  ranges.push({ start: offset, end, text: match, ref });
@@ -373,7 +375,7 @@ function writeEditorValue(
373
375
  const isChat = seg.ref?.type === 'chat';
374
376
  const isSkill = seg.ref?.type === 'skill';
375
377
  const isMcp = seg.ref?.type === 'mcp';
376
- const removeLabel = `移除 ${seg.text}`;
378
+ const removeLabel = t('Remove {name}', { name: seg.text });
377
379
 
378
380
  const chip = document.createElement('button');
379
381
  chip.type = 'button';
@@ -778,7 +780,7 @@ function AttachmentChip({
778
780
  type="button"
779
781
  onClick={onRemove}
780
782
  className="flex h-4 w-4 items-center justify-center rounded-full text-agent-muted-foreground transition-colors hover:bg-agent-foreground/10 hover:text-agent-foreground"
781
- aria-label={`移除文件 ${file.name}`}
783
+ aria-label={t('Remove file {name}', { name: file.name })}
782
784
  >
783
785
  <LuX className="h-3 w-3" />
784
786
  </button>
@@ -799,7 +801,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
799
801
  onRemoveFollowUp,
800
802
  isStreaming = false,
801
803
  disabled = false,
802
- placeholder = '向 Agent 发送消息…',
804
+ placeholder = t('Message the Agent...'),
803
805
  currentAgent,
804
806
  agents = [],
805
807
  chats = [],
@@ -941,8 +943,10 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
941
943
  .map((chat) => ({
942
944
  type: 'chat',
943
945
  id: chat.id,
944
- label: chat.title || '未命名对话',
945
- description: chat.updatedAt ? `历史对话 · ${new Date(chat.updatedAt).toLocaleString()}` : '历史对话',
946
+ label: chat.title || t('Untitled chat'),
947
+ description: chat.updatedAt
948
+ ? t('Past chat · {time}', { time: new Date(chat.updatedAt).toLocaleString() })
949
+ : t('Past chat'),
946
950
  }));
947
951
 
948
952
  return [...agentSuggestions, ...chatSuggestions].slice(0, MAX_MENTION_SUGGESTIONS);
@@ -1042,11 +1046,11 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1042
1046
  const mcpGroup = suggestions.filter((s) => s.kind === 'mcp');
1043
1047
  let idx = 0;
1044
1048
  if (skillGroup.length > 0) {
1045
- rows.push({ type: 'header', label: '指定运行的本地技能 (Skill)' });
1049
+ rows.push({ type: 'header', label: t('Run a local skill (Skill)') });
1046
1050
  for (const s of skillGroup) rows.push({ type: 'item', item: s, idx: idx++ });
1047
1051
  }
1048
1052
  if (mcpGroup.length > 0) {
1049
- rows.push({ type: 'header', label: '指定调用的 MCP 工具 (MCP Tool)' });
1053
+ rows.push({ type: 'header', label: t('Call an MCP tool (MCP Tool)') });
1050
1054
  for (const s of mcpGroup) rows.push({ type: 'item', item: s, idx: idx++ });
1051
1055
  }
1052
1056
  return rows;
@@ -1687,7 +1691,11 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1687
1691
  // 进入待发队列 / 作为新回合发出),草稿都可以清;queued 额外
1688
1692
  // 提示用户"不是追加进当前回合"。
1689
1693
  if (outcome === 'queued') {
1690
- showSteerNotice('当前回合无法插队,已改为排队,本轮结束后自动发出');
1694
+ showSteerNotice(
1695
+ t(
1696
+ 'This turn cannot take an interjection. The message was queued and will be sent when this turn ends.',
1697
+ ),
1698
+ );
1691
1699
  }
1692
1700
  onChange('');
1693
1701
  });
@@ -1774,7 +1782,9 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1774
1782
  onAutoContinue={() => askUserPrompt.answer({})}
1775
1783
  bottomHint={
1776
1784
  askUserPrompt.pendingCount > 0
1777
- ? `还有 ${askUserPrompt.pendingCount} 组问题待回答`
1785
+ ? t('{count} more question groups to answer', {
1786
+ count: askUserPrompt.pendingCount,
1787
+ })
1778
1788
  : undefined
1779
1789
  }
1780
1790
  />
@@ -1814,7 +1824,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1814
1824
  {mentionQuery && mentionSuggestions.length > 0 && (
1815
1825
  <div className="absolute bottom-full left-3 z-50 mb-1 max-h-72 w-80 max-w-[calc(100%-0.75rem)] overflow-y-auto rounded-agent-md border border-agent-border bg-agent-canvas p-1 shadow-lg">
1816
1826
  <div className="px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-agent-muted-foreground">
1817
- @ 选择 Agent 或历史对话
1827
+ {t('@ Choose an Agent or past chat')}
1818
1828
  </div>
1819
1829
  {mentionSuggestions.map((item, idx) => (
1820
1830
  <button
@@ -1848,7 +1858,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1848
1858
  <span className="flex items-center gap-1.5">
1849
1859
  <span className="truncate text-xs font-medium">@{item.label}</span>
1850
1860
  <span className="shrink-0 rounded-full bg-agent-muted px-1.5 py-0.5 text-[10px] text-agent-muted-foreground">
1851
- {item.type === 'agent' ? 'Agent' : '对话'}
1861
+ {item.type === 'agent' ? 'Agent' : t('Chat')}
1852
1862
  </span>
1853
1863
  </span>
1854
1864
  {item.description && (
@@ -1901,7 +1911,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1901
1911
  <div className="text-xs font-medium truncate">
1902
1912
  {row.item.skill.displayName ? (
1903
1913
  <>
1904
- {row.item.skill.displayName}
1914
+ {t(row.item.skill.displayName)}
1905
1915
  <span className="ml-1.5 text-[10px] font-normal text-agent-muted-foreground/70">
1906
1916
  /{row.item.skill.name}
1907
1917
  </span>
@@ -1956,7 +1966,10 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1956
1966
  {pendingFollowUps.length > 0 && (
1957
1967
  <div className="flex flex-col gap-1 border-b border-agent-border bg-agent-muted/30 px-2.5 py-1.5">
1958
1968
  <div className="text-[11px] font-medium text-agent-muted-foreground">
1959
- 排队中({pendingFollowUps.length})· 本轮结束后自动发出 · 停止后恢复到输入框
1969
+ {t(
1970
+ 'Queued ({count}) · Sent when this turn ends · Restored to the input box if stopped',
1971
+ { count: pendingFollowUps.length },
1972
+ )}
1960
1973
  </div>
1961
1974
  {pendingFollowUps.map((text, i) => (
1962
1975
  <div
@@ -1967,7 +1980,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1967
1980
  {onRemoveFollowUp && (
1968
1981
  <button
1969
1982
  type="button"
1970
- aria-label={`撤回排队消息 ${i + 1}`}
1983
+ aria-label={t('Withdraw queued message {index}', { index: i + 1 })}
1971
1984
  onClick={() => onRemoveFollowUp(i)}
1972
1985
  className="shrink-0 text-agent-muted-foreground opacity-0 transition-opacity hover:text-agent-destructive group-hover:opacity-100"
1973
1986
  >
@@ -2059,8 +2072,8 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
2059
2072
  onClick={handlePickFiles}
2060
2073
  disabled={disabled || isStreaming}
2061
2074
  className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-agent-muted-foreground transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground disabled:cursor-not-allowed disabled:opacity-50"
2062
- title="上传文件,或在输入框粘贴图片"
2063
- aria-label="上传文件"
2075
+ title={t('Upload files, or paste images into the input box')}
2076
+ aria-label={t('Upload files')}
2064
2077
  data-testid="chat-attach"
2065
2078
  >
2066
2079
  <LuPaperclip className="h-3 w-3" />
@@ -2093,7 +2106,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
2093
2106
  <kbd className="rounded border border-agent-border bg-agent-muted px-1 font-sans text-[10px]">
2094
2107
  Enter
2095
2108
  </kbd>
2096
- <span className="ml-1">排队</span>
2109
+ <span className="ml-1">{t('Queue')}</span>
2097
2110
  </span>
2098
2111
  )}
2099
2112
  {onSteer && (
@@ -2105,14 +2118,14 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
2105
2118
  <kbd className="rounded border border-agent-border bg-agent-muted px-1 font-sans text-[10px]">
2106
2119
  Enter
2107
2120
  </kbd>
2108
- <span className="ml-1">插队</span>
2121
+ <span className="ml-1">{t('Interject')}</span>
2109
2122
  </span>
2110
2123
  )}
2111
2124
  <span className="ml-2">
2112
2125
  <kbd className="rounded border border-agent-border bg-agent-muted px-1 font-sans text-[10px]">
2113
2126
  Esc
2114
2127
  </kbd>
2115
- <span className="ml-1">停止</span>
2128
+ <span className="ml-1">{t('Stop')}</span>
2116
2129
  </span>
2117
2130
  </>
2118
2131
  ) : (
@@ -2120,7 +2133,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
2120
2133
  <kbd className="rounded border border-agent-border bg-agent-muted px-1 font-sans text-[10px]">
2121
2134
  Enter
2122
2135
  </kbd>
2123
- <span className="ml-1">发送</span>
2136
+ <span className="ml-1">{t('Send')}</span>
2124
2137
  <span className="ml-2">
2125
2138
  <kbd className="rounded border border-agent-border bg-agent-muted px-1 font-sans text-[10px]">
2126
2139
  Shift
@@ -2129,7 +2142,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
2129
2142
  <kbd className="rounded border border-agent-border bg-agent-muted px-1 font-sans text-[10px]">
2130
2143
  Enter
2131
2144
  </kbd>
2132
- <span className="ml-1">换行</span>
2145
+ <span className="ml-1">{t('New line')}</span>
2133
2146
  </span>
2134
2147
  </>
2135
2148
  )}
@@ -2149,11 +2162,14 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
2149
2162
  title={
2150
2163
  isStreaming
2151
2164
  ? pendingFollowUps.length > 0
2152
- ? `停止生成 (Esc) · ${pendingFollowUps.length} 条排队消息将恢复到输入框`
2153
- : '停止生成 (Esc)'
2154
- : '发送 (Enter)'
2165
+ ? t(
2166
+ 'Stop generating (Esc) · {count} queued messages will be restored to the input box',
2167
+ { count: pendingFollowUps.length },
2168
+ )
2169
+ : t('Stop generating (Esc)')
2170
+ : t('Send (Enter)')
2155
2171
  }
2156
- aria-label={isStreaming ? '停止生成' : '发送消息'}
2172
+ aria-label={isStreaming ? t('Stop generating') : t('Send message')}
2157
2173
  data-testid="chat-send"
2158
2174
  >
2159
2175
  {isStreaming ? (
@@ -2171,7 +2187,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
2171
2187
  );
2172
2188
 
2173
2189
  function agentInitial(agent: LocalChatAgent): string {
2174
- return agent.name.trim()[0]?.toUpperCase() ?? 'A';
2190
+ return agentLabel(agent).trim()[0]?.toUpperCase() ?? 'A';
2175
2191
  }
2176
2192
 
2177
2193
  /**
@@ -2193,7 +2209,7 @@ function ModeToggle({
2193
2209
  <div
2194
2210
  className="inline-flex h-6 shrink-0 items-center rounded-full border border-agent-border bg-agent-canvas p-0.5"
2195
2211
  role="radiogroup"
2196
- aria-label="对话模式"
2212
+ aria-label={t('Chat mode')}
2197
2213
  data-testid="mode-toggle"
2198
2214
  >
2199
2215
  <button
@@ -2202,7 +2218,7 @@ function ModeToggle({
2202
2218
  aria-checked={mode === 'agent'}
2203
2219
  disabled={disabled}
2204
2220
  onClick={() => onChange('agent')}
2205
- title="Agent 模式:直接执行任务"
2221
+ title={t('Agent mode: carry out the task directly')}
2206
2222
  data-testid="mode-agent"
2207
2223
  className={[
2208
2224
  'inline-flex h-5 items-center gap-1 rounded-full px-1.5 text-[12px] leading-[1.45] transition-colors disabled:cursor-not-allowed disabled:opacity-70',
@@ -2220,7 +2236,7 @@ function ModeToggle({
2220
2236
  aria-checked={mode === 'plan'}
2221
2237
  disabled={disabled}
2222
2238
  onClick={() => onChange('plan')}
2223
- title="Plan 模式:先制定计划,只读不执行"
2239
+ title={t('Plan mode: make a plan first, read only and no execution')}
2224
2240
  data-testid="mode-plan"
2225
2241
  className={[
2226
2242
  'inline-flex h-5 items-center gap-1 rounded-full px-1.5 text-[12px] leading-[1.45] transition-colors disabled:cursor-not-allowed disabled:opacity-70',
@@ -2267,14 +2283,14 @@ function AgentSelect({
2267
2283
 
2268
2284
  const buttonTitle = useMemo(() => {
2269
2285
  const activeNames = [
2270
- agent.name,
2286
+ agentLabel(agent),
2271
2287
  ...mentionReferences
2272
2288
  .filter((ref) => ref.type === 'agent' && ref.id !== agent.id)
2273
2289
  .map((ref) => ref.label),
2274
2290
  ].filter(Boolean) as string[];
2275
2291
 
2276
- return `当前激活专家:${activeNames.join(', ')}`;
2277
- }, [agent.name, agent.id, mentionReferences]);
2292
+ return t('Active experts: {names}', { names: activeNames.join(', ') });
2293
+ }, [agent, mentionReferences]);
2278
2294
 
2279
2295
  return (
2280
2296
  <div
@@ -2293,7 +2309,7 @@ function AgentSelect({
2293
2309
  >
2294
2310
  <AgentDot agent={agent} />
2295
2311
  <span className="truncate font-medium">
2296
- {agent.name}
2312
+ {agentLabel(agent)}
2297
2313
  {mentionedAgentCount > 0 ? ` +${mentionedAgentCount}` : ''}
2298
2314
  </span>
2299
2315
  <LuChevronDown className="h-3 w-3 shrink-0 text-agent-muted-foreground" />
@@ -2305,7 +2321,7 @@ function AgentSelect({
2305
2321
  className="absolute bottom-full left-0 z-50 mb-1 max-h-64 w-72 overflow-y-auto rounded-agent-md border border-agent-border bg-agent-canvas p-1 shadow-lg"
2306
2322
  >
2307
2323
  <div className="px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-agent-muted-foreground">
2308
- 选择专家
2324
+ {t('Choose an expert')}
2309
2325
  </div>
2310
2326
  {agents.map((item) => {
2311
2327
  const isPrimary = item.id === agent.id;
@@ -2332,16 +2348,16 @@ function AgentSelect({
2332
2348
  <span className="min-w-0 flex-1">
2333
2349
  <span className="flex items-center gap-1.5">
2334
2350
  <span className="block truncate text-xs font-medium">
2335
- {item.name}
2351
+ {agentLabel(item)}
2336
2352
  </span>
2337
2353
  {isPrimary && (
2338
2354
  <span className="inline-flex items-center rounded bg-agent-foreground/10 px-1 py-0.5 text-[9px] font-medium text-agent-foreground">
2339
- 当前
2355
+ {t('Current')}
2340
2356
  </span>
2341
2357
  )}
2342
2358
  {!isPrimary && isMentioned && (
2343
2359
  <span className="inline-flex items-center rounded bg-agent-muted px-1 py-0.5 text-[9px] font-medium text-agent-muted-foreground border border-agent-border">
2344
- 已引入
2360
+ {t('Mentioned')}
2345
2361
  </span>
2346
2362
  )}
2347
2363
  </span>
@@ -2366,7 +2382,7 @@ function AgentSelect({
2366
2382
  className="mt-0.5 flex w-full items-center gap-2 rounded border-t border-agent-border/50 px-2 py-2 text-left text-agent-muted-foreground transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground"
2367
2383
  >
2368
2384
  <LuBot className="h-3.5 w-3.5 shrink-0" />
2369
- <span className="text-xs font-medium">管理智能体</span>
2385
+ <span className="text-xs font-medium">{t('Manage agents')}</span>
2370
2386
  </button>
2371
2387
  )}
2372
2388
  </div>
@@ -17,6 +17,7 @@ import {
17
17
  updateProject,
18
18
  type LocalProject,
19
19
  } from '@/lib/local-api';
20
+ import { t } from '@/i18n';
20
21
 
21
22
  /**
22
23
  * 项目徽章 / 选择器(Codex 式 cwd 指示),渲染在输入框上方的 meta 行里,
@@ -113,8 +114,8 @@ function MenuSectionLabel({
113
114
  type="button"
114
115
  onClick={onAdd}
115
116
  className="flex h-5 w-5 items-center justify-center rounded-full text-agent-muted-foreground transition-colors hover:bg-agent-foreground/10 hover:text-agent-foreground"
116
- title="新建项目"
117
- aria-label="新建项目"
117
+ title={t('New project')}
118
+ aria-label={t('New project')}
118
119
  >
119
120
  <LuPlus className="h-3 w-3" />
120
121
  </button>
@@ -132,7 +133,7 @@ function CreateProjectMenuItem({ onClick }: { onClick: () => void }) {
132
133
  className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs text-agent-foreground transition-colors hover:bg-agent-foreground/5"
133
134
  >
134
135
  <LuFolderPlus className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
135
- 新建项目
136
+ {t('New project')}
136
137
  </button>
137
138
  );
138
139
  }
@@ -220,7 +221,7 @@ export function ChatProjectBadge({
220
221
  run(async () => {
221
222
  if (!isElectron() || !project) return;
222
223
  const result = await getElectronBridge()!.local?.selectDirectory({
223
- title: `重新选择「${project.name}」绑定的文件夹`,
224
+ title: t('Choose a new folder for "{name}"', { name: project.name }),
224
225
  });
225
226
  if (!result || result.canceled || result.filePaths.length === 0) return;
226
227
  await updateProject(project.id, { folderPath: result.filePaths[0] });
@@ -230,7 +231,7 @@ export function ChatProjectBadge({
230
231
  return (
231
232
  <div className="relative">
232
233
  <BadgeButton
233
- label={project ? project.name : '选择项目'}
234
+ label={project ? project.name : t('Choose a project')}
234
235
  active={Boolean(project)}
235
236
  open={open}
236
237
  busy={busy}
@@ -240,8 +241,10 @@ export function ChatProjectBadge({
240
241
  }}
241
242
  title={
242
243
  project
243
- ? `${project.folderPath}\n点击管理项目归属 / 修改绑定目录`
244
- : '把当前对话关联到一个项目(文件写入限制在项目家目录、源文件夹,以及这些目录的子目录内)'
244
+ ? `${project.folderPath}\n${t('Click to manage the project or change its folder')}`
245
+ : t(
246
+ 'Link this chat to a project (file writes are limited to the project home folder, its source folders, and their subfolders)',
247
+ )
245
248
  }
246
249
  />
247
250
 
@@ -249,7 +252,7 @@ export function ChatProjectBadge({
249
252
  <MenuShell onClose={() => setOpen(false)}>
250
253
  {project ? (
251
254
  <>
252
- <MenuSectionLabel>当前项目</MenuSectionLabel>
255
+ <MenuSectionLabel>{t('Current project')}</MenuSectionLabel>
253
256
  <div
254
257
  className="flex items-center gap-2 rounded px-2 py-1.5 text-xs text-agent-foreground"
255
258
  title={project.folderPath}
@@ -268,7 +271,7 @@ export function ChatProjectBadge({
268
271
  </div>
269
272
  </>
270
273
  ) : (
271
- <MenuSectionLabel onAdd={openCreate}>关联到项目</MenuSectionLabel>
274
+ <MenuSectionLabel onAdd={openCreate}>{t('Link to a project')}</MenuSectionLabel>
272
275
  )}
273
276
 
274
277
  {otherProjects.length > 0 && (
@@ -276,7 +279,7 @@ export function ChatProjectBadge({
276
279
  {project && (
277
280
  <div className="mx-1 my-1 border-t border-agent-border/60" />
278
281
  )}
279
- {project && <MenuSectionLabel>移动到</MenuSectionLabel>}
282
+ {project && <MenuSectionLabel>{t('Move to')}</MenuSectionLabel>}
280
283
  <div className="max-h-40 overflow-y-auto">
281
284
  {otherProjects.map((p) => (
282
285
  <ProjectMenuRow
@@ -291,7 +294,7 @@ export function ChatProjectBadge({
291
294
  {!project && projects.length === 0 && (
292
295
  <>
293
296
  <div className="px-2 py-1.5 text-[11px] text-agent-muted-foreground/70">
294
- 还没有项目
297
+ {t('No projects yet')}
295
298
  </div>
296
299
  <CreateProjectMenuItem onClick={openCreate} />
297
300
  </>
@@ -307,7 +310,7 @@ export function ChatProjectBadge({
307
310
  className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs text-agent-foreground transition-colors hover:bg-agent-foreground/5"
308
311
  >
309
312
  <LuFolderPen className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
310
- 修改项目目录…
313
+ {t('Change project folder...')}
311
314
  </button>
312
315
  <button
313
316
  type="button"
@@ -316,7 +319,7 @@ export function ChatProjectBadge({
316
319
  className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs text-agent-foreground transition-colors hover:bg-agent-foreground/5"
317
320
  >
318
321
  <LuFolderX className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
319
- 移出项目(变为无项目对话)
322
+ {t('Remove from project (becomes a chat without a project)')}
320
323
  </button>
321
324
  </>
322
325
  )}
@@ -375,21 +378,23 @@ export function ProjectPickerButton({
375
378
  return (
376
379
  <div className="relative">
377
380
  <BadgeButton
378
- label={selected ? selected.name : '选择项目'}
381
+ label={selected ? selected.name : t('Choose a project')}
379
382
  active={Boolean(selected)}
380
383
  open={open}
381
384
  busy={false}
382
385
  onClick={() => setOpen((v) => !v)}
383
386
  title={
384
387
  selected
385
- ? `${selected.folderPath}\n新对话将绑定到此项目`
386
- : '为新对话选择项目。不选则在文稿/<应用名>/conversations/ 下建立本对话工作区'
388
+ ? `${selected.folderPath}\n${t('New chats will be linked to this project')}`
389
+ : t(
390
+ 'Choose a project for the new chat. Without one, a workspace for this chat is created under Documents/<app name>/conversations/',
391
+ )
387
392
  }
388
393
  />
389
394
 
390
395
  {open && (
391
396
  <MenuShell onClose={() => setOpen(false)}>
392
- <MenuSectionLabel onAdd={openCreate}>新对话所属项目</MenuSectionLabel>
397
+ <MenuSectionLabel onAdd={openCreate}>{t('Project for new chat')}</MenuSectionLabel>
393
398
  <button
394
399
  type="button"
395
400
  role="menuitem"
@@ -400,7 +405,7 @@ export function ProjectPickerButton({
400
405
  className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs text-agent-foreground transition-colors hover:bg-agent-foreground/5"
401
406
  >
402
407
  <LuFolderX className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
403
- <span className="min-w-0 flex-1 truncate">不关联项目</span>
408
+ <span className="min-w-0 flex-1 truncate">{t('No project')}</span>
404
409
  {!selected && (
405
410
  <LuCheck className="h-3 w-3 shrink-0 text-agent-muted-foreground" />
406
411
  )}
@@ -421,7 +426,7 @@ export function ProjectPickerButton({
421
426
  {projects.length === 0 && (
422
427
  <>
423
428
  <div className="px-2 py-1.5 text-[11px] text-agent-muted-foreground/70">
424
- 还没有项目
429
+ {t('No projects yet')}
425
430
  </div>
426
431
  <CreateProjectMenuItem onClick={openCreate} />
427
432
  </>
@@ -1,4 +1,5 @@
1
1
  import { BRAND_HOME_HINT, BRAND_NAME, getBrandLogoUrl } from '@/brand';
2
+ import { t } from '@/i18n';
2
3
 
3
4
  /**
4
5
  * 新对话落地页与空会话首屏的品牌区:侧栏同一枚 logo,加一句
@@ -16,7 +17,7 @@ export function EmptyChatHero() {
16
17
  draggable={false}
17
18
  />
18
19
  </h1>
19
- {hint ? <p className="mt-1.5 text-xs text-agent-muted-foreground">{hint}</p> : null}
20
+ {hint ? <p className="mt-1.5 text-xs text-agent-muted-foreground">{t(hint)}</p> : null}
20
21
  </div>
21
22
  );
22
23
  }
@@ -1,6 +1,7 @@
1
1
  import { useEffect, useRef, useState } from 'react';
2
2
  import { LuChevronDown, LuFolderLock, LuLockOpen } from 'react-icons/lu';
3
3
  import type { ExecPolicy } from '@/lib/exec-policy';
4
+ import { t } from '@/i18n';
4
5
 
5
6
  /**
6
7
  * ExecPolicyPicker — 输入框上的命令沙箱切换(类 Codex 底部权限档)。
@@ -17,14 +18,14 @@ const OPTIONS: Array<{
17
18
  }> = [
18
19
  {
19
20
  id: 'workspace',
20
- label: '工作区',
21
- description:
22
- '只能写入当前工作区,范围外的路径会被拒绝。',
21
+ label: 'Workspace',
22
+ description: 'Can only write to the current workspace. Paths outside it are blocked.',
23
23
  },
24
24
  {
25
25
  id: 'full',
26
- label: '完整权限',
27
- description: '关闭命令沙箱,可写本机任意路径。工具审批仍然生效。',
26
+ label: 'Full access',
27
+ description:
28
+ 'Turns off the command sandbox so any path on this computer can be written. Tool approvals still apply.',
28
29
  },
29
30
  ];
30
31
 
@@ -60,9 +61,9 @@ export function ExecPolicyPicker({
60
61
  disabled={disabled}
61
62
  aria-haspopup="listbox"
62
63
  aria-expanded={open}
63
- aria-label="命令沙箱"
64
+ aria-label={t('Command sandbox')}
64
65
  data-testid="exec-policy-picker"
65
- title={current.description}
66
+ title={t(current.description)}
66
67
  onClick={() => setOpen((next) => !next)}
67
68
  className={[
68
69
  'inline-flex h-6 max-w-[140px] items-center gap-1 rounded-full border px-1.5 text-[12px] leading-[1.45] transition-colors disabled:cursor-not-allowed disabled:opacity-70',
@@ -76,13 +77,13 @@ export function ExecPolicyPicker({
76
77
  ) : (
77
78
  <LuFolderLock className="h-3 w-3 shrink-0" />
78
79
  )}
79
- <span className="truncate">{current.label}</span>
80
+ <span className="truncate">{t(current.label)}</span>
80
81
  <LuChevronDown className="h-3 w-3 shrink-0 text-agent-muted-foreground" />
81
82
  </button>
82
83
  {open && (
83
84
  <div
84
85
  role="listbox"
85
- aria-label="命令沙箱"
86
+ aria-label={t('Command sandbox')}
86
87
  className="absolute bottom-full left-0 z-30 mb-1 w-64 overflow-hidden rounded-agent-md border border-agent-border bg-agent-canvas py-1 shadow-sm"
87
88
  >
88
89
  {OPTIONS.map((option) => {
@@ -106,10 +107,10 @@ export function ExecPolicyPicker({
106
107
  ].join(' ')}
107
108
  >
108
109
  <span className="text-[12px] font-medium leading-[1.45] text-agent-foreground">
109
- {option.label}
110
+ {t(option.label)}
110
111
  </span>
111
112
  <span className="text-[11px] leading-snug text-agent-muted-foreground">
112
- {option.description}
113
+ {t(option.description)}
113
114
  </span>
114
115
  </button>
115
116
  );