@steerable/agent-shell-web 0.6.53 → 0.6.56

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 (100) hide show
  1. package/README.md +3 -4
  2. package/package.json +4 -4
  3. package/src/AppShell.tsx +3 -3
  4. package/src/brand.ts +1 -1
  5. package/src/components/AgentSidebar.tsx +64 -56
  6. package/src/components/ChatHeader.tsx +41 -25
  7. package/src/components/ConfirmDialog.tsx +4 -3
  8. package/src/components/CreateProjectModal.tsx +31 -21
  9. package/src/components/LocalLlmSettingsModal.tsx +4 -3
  10. package/src/components/SidebarRelease.tsx +6 -5
  11. package/src/components/TerminalPanel.tsx +6 -5
  12. package/src/components/TerminalView.tsx +15 -10
  13. package/src/components/chat/ApprovalModal.tsx +40 -26
  14. package/src/components/chat/ApprovalPromptProvider.tsx +5 -5
  15. package/src/components/chat/AskUserPromptProvider.tsx +4 -4
  16. package/src/components/chat/AskUserQuestionMenu.tsx +19 -14
  17. package/src/components/chat/AssistantMessage.tsx +18 -13
  18. package/src/components/chat/ChatInput.tsx +56 -40
  19. package/src/components/chat/ChatProjectBadge.tsx +27 -22
  20. package/src/components/chat/EmptyChatHero.tsx +2 -1
  21. package/src/components/chat/ExecPolicyPicker.tsx +12 -11
  22. package/src/components/chat/ExecutedActionsCard.tsx +45 -33
  23. package/src/components/chat/FilePathCode.tsx +8 -3
  24. package/src/components/chat/InterruptedTurnCard.tsx +4 -3
  25. package/src/components/chat/LocalChatPanel.tsx +2 -1
  26. package/src/components/chat/Markdown.tsx +2 -1
  27. package/src/components/chat/MessageList.tsx +3 -2
  28. package/src/components/chat/ModelPicker.tsx +25 -16
  29. package/src/components/chat/OrchestrationChildrenCard.tsx +2 -1
  30. package/src/components/chat/OrchestrationFlowCard.tsx +23 -22
  31. package/src/components/chat/PlanTodoList.tsx +2 -1
  32. package/src/components/chat/ProjectTrustBanner.tsx +19 -5
  33. package/src/components/chat/SessionOrchestrationFlow.tsx +27 -26
  34. package/src/components/chat/SessionTodoList.tsx +8 -7
  35. package/src/components/chat/SessionTreeModal.tsx +10 -9
  36. package/src/components/chat/StreamingStatus.tsx +4 -3
  37. package/src/components/chat/SuggestedReplies.tsx +3 -2
  38. package/src/components/chat/TaskOutcomeCards.tsx +8 -5
  39. package/src/components/chat/TaskPanelModal.tsx +33 -23
  40. package/src/components/chat/TaskProcessPanel.tsx +16 -15
  41. package/src/components/chat/TurnFilesCard.tsx +14 -13
  42. package/src/components/chat/UserMessage.tsx +12 -11
  43. package/src/components/chat/executed-actions-model.ts +47 -35
  44. package/src/components/chat/orchestration-flow-model.ts +16 -5
  45. package/src/components/chat/process-status.ts +8 -7
  46. package/src/components/chat/todo-list-model.ts +5 -4
  47. package/src/components/chat/turn-files.ts +8 -8
  48. package/src/components/chat/useChatTasks.ts +3 -3
  49. package/src/components/settings/AgentCapabilityFields.tsx +26 -23
  50. package/src/components/settings/AgentsSettingsPanel.tsx +33 -28
  51. package/src/components/settings/AppUpdateSettingsPanel.tsx +3 -2
  52. package/src/components/settings/AppearanceSettingsPanel.tsx +81 -37
  53. package/src/components/settings/DiagnoseSettingsPanel.tsx +11 -8
  54. package/src/components/settings/InsightsSettingsPanel.tsx +58 -43
  55. package/src/components/settings/LlmSettingsPanel.tsx +107 -86
  56. package/src/components/settings/McpSettingsPanel.tsx +72 -46
  57. package/src/components/settings/ModelIdCombobox.tsx +2 -1
  58. package/src/components/settings/OrchestrationSettingsPanel.tsx +8 -5
  59. package/src/components/settings/PortableSettingsPanel.tsx +83 -55
  60. package/src/components/settings/PythonRunnerSettingsPanel.tsx +26 -23
  61. package/src/components/settings/SecuritySettingsPanel.tsx +58 -34
  62. package/src/components/settings/SettingsNavMenu.tsx +3 -2
  63. package/src/components/settings/SettingsSaveButton.tsx +2 -1
  64. package/src/components/settings/SkillsSettingsPanel.tsx +43 -38
  65. package/src/components/settings/TelemetrySettingsPanel.tsx +20 -17
  66. package/src/components/settings/UsagePanel.tsx +22 -23
  67. package/src/components/settings/WebSearchSettingsPanel.tsx +28 -19
  68. package/src/components/settings/llm-vendors.ts +12 -7
  69. package/src/components/settings/model-capabilities.ts +16 -15
  70. package/src/components/settings/preset-choice-model.ts +6 -3
  71. package/src/fixtures/browser-dev-data.json +24 -24
  72. package/src/hooks/useChatsAndAgents.ts +9 -9
  73. package/src/hooks/useProjects.ts +3 -3
  74. package/src/i18n/agent-label.ts +15 -0
  75. package/src/i18n/index.tsx +68 -0
  76. package/src/i18n/runtime.ts +117 -0
  77. package/src/i18n/translate.ts +65 -0
  78. package/src/layouts/AgentLayout.tsx +13 -12
  79. package/src/lib/app-release.ts +16 -11
  80. package/src/lib/attachments.ts +18 -9
  81. package/src/lib/{browser-dev-electron-mock.ts → browser-dev-host-mock.ts} +42 -42
  82. package/src/lib/chat-title.ts +3 -2
  83. package/src/lib/chat-transport.ts +16 -12
  84. package/src/lib/date-groups.ts +23 -15
  85. package/src/lib/host-bridge.ts +371 -4
  86. package/src/lib/host-paste.ts +1 -1
  87. package/src/lib/http-bridge.ts +12 -9
  88. package/src/lib/insights.ts +2 -2
  89. package/src/lib/local-api.ts +5 -5
  90. package/src/lib/portable.ts +11 -10
  91. package/src/lib/show-thinking-content.ts +15 -3
  92. package/src/lib/slash-sources.ts +3 -3
  93. package/src/lib/tauri-bridge.ts +1 -1
  94. package/src/main.tsx +27 -4
  95. package/src/pages/AgentPage.tsx +39 -34
  96. package/src/pages/ChatPanelPreviewPage.tsx +46 -44
  97. package/src/pages/SettingsPage.tsx +51 -45
  98. package/src/vite-env.d.ts +3 -0
  99. package/vite.base.js +6 -2
  100. package/src/lib/electron-bridge.ts +0 -390
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { LuCheck, LuChevronLeft, LuPencil, LuWand } from 'react-icons/lu';
3
+ import { t } from '@/i18n';
3
4
 
4
5
  /**
5
6
  * AskUserQuestionMenu — W8 结构化提问的分步菜单卡片。
@@ -94,7 +95,7 @@ function initialDraft(question: AskQuestion, answer: AnswerValue | undefined): {
94
95
  return {
95
96
  selected,
96
97
  customMode: customBits.length > 0,
97
- customText: customBits.join('、'),
98
+ customText: customBits.join(t(', ')),
98
99
  };
99
100
  }
100
101
  if (typeof answer === 'string' && answer.length > 0) {
@@ -129,7 +130,7 @@ function Footer({
129
130
  className="inline-flex items-center gap-1 rounded-agent-md border border-agent-border px-2.5 py-1.5 text-xs font-medium text-agent-foreground transition hover:bg-agent-muted"
130
131
  >
131
132
  <LuChevronLeft size={12} />
132
- 上一题
133
+ {t('Previous')}
133
134
  </button>
134
135
  )}
135
136
  {onAutoContinue && (
@@ -139,7 +140,7 @@ function Footer({
139
140
  className="inline-flex items-center gap-1 rounded-agent-md px-2.5 py-1.5 text-xs font-medium text-agent-muted-foreground transition hover:bg-agent-muted hover:text-agent-foreground"
140
141
  >
141
142
  <LuWand size={12} />
142
- 交给AI决定
143
+ {t('Let AI decide')}
143
144
  </button>
144
145
  )}
145
146
  </div>
@@ -175,7 +176,7 @@ function PrimaryButton({
175
176
  ].join(' ')}
176
177
  >
177
178
  <LuCheck size={12} />
178
- {isLast ? '提交' : '下一题'}
179
+ {isLast ? t('Submit') : t('Next')}
179
180
  </button>
180
181
  );
181
182
  }
@@ -352,7 +353,7 @@ function SelectQuestionStep({
352
353
  <LuCheck size={10} />
353
354
  </span>
354
355
  <LuPencil size={12} className="shrink-0" />
355
- <span className="min-w-0 flex-1">其他 / 自定义</span>
356
+ <span className="min-w-0 flex-1">{t('Other / custom')}</span>
356
357
  </button>
357
358
  );
358
359
  };
@@ -395,7 +396,7 @@ function SelectQuestionStep({
395
396
  commitMulti();
396
397
  }
397
398
  }}
398
- placeholder={question.placeholder || '输入你的补充...'}
399
+ placeholder={question.placeholder || t('Add your details...')}
399
400
  className="w-full rounded-agent-md border border-agent-border bg-agent-canvas px-3 py-2 text-xs text-agent-foreground outline-none placeholder:text-agent-muted-foreground focus:border-agent-foreground focus:ring-1 focus:ring-agent-foreground"
400
401
  />
401
402
  )}
@@ -413,7 +414,7 @@ function SelectQuestionStep({
413
414
  commitCustom();
414
415
  }
415
416
  }}
416
- placeholder={question.placeholder || '输入你的回答...'}
417
+ placeholder={question.placeholder || t('Type your answer...')}
417
418
  className="w-full rounded-agent-md border border-agent-border bg-agent-canvas px-3 py-2 text-xs text-agent-foreground outline-none placeholder:text-agent-muted-foreground focus:border-agent-foreground focus:ring-1 focus:ring-agent-foreground"
418
419
  />
419
420
  <button
@@ -424,7 +425,7 @@ function SelectQuestionStep({
424
425
  }}
425
426
  className="text-xs text-agent-muted-foreground transition hover:text-agent-foreground"
426
427
  >
427
- ← 返回选项
428
+ {t('← Back to options')}
428
429
  </button>
429
430
  </div>
430
431
  )}
@@ -434,7 +435,11 @@ function SelectQuestionStep({
434
435
  canGoBack={Boolean(onBack)}
435
436
  onBack={onBack}
436
437
  onAutoContinue={onAutoContinue}
437
- hint={question.multiSelect ? '↑↓ 移动,Enter 勾选' : '↑↓ 选择,Enter 确认'}
438
+ hint={
439
+ question.multiSelect
440
+ ? t('↑↓ to move, Enter to check')
441
+ : t('↑↓ to choose, Enter to confirm')
442
+ }
438
443
  >
439
444
  {question.multiSelect ? (
440
445
  <PrimaryButton
@@ -511,7 +516,7 @@ function TextQuestionStep({
511
516
  commit();
512
517
  }
513
518
  }}
514
- placeholder={question.placeholder || '输入你的回答...'}
519
+ placeholder={question.placeholder || t('Type your answer...')}
515
520
  className="w-full rounded-agent-md border border-agent-border bg-agent-canvas px-3 py-2 text-xs text-agent-foreground outline-none placeholder:text-agent-muted-foreground focus:border-agent-foreground focus:ring-1 focus:ring-agent-foreground"
516
521
  />
517
522
  </div>
@@ -520,7 +525,7 @@ function TextQuestionStep({
520
525
  canGoBack={Boolean(onBack)}
521
526
  onBack={onBack}
522
527
  onAutoContinue={onAutoContinue}
523
- hint="Enter 确认"
528
+ hint={t('Enter to confirm')}
524
529
  >
525
530
  <PrimaryButton disabled={!value.trim()} isLast={isLast} onClick={commit} />
526
531
  </Footer>
@@ -581,16 +586,16 @@ export function AskUserQuestionMenu({
581
586
  <div className="flex items-start justify-between gap-2 border-b border-agent-border px-3 py-2">
582
587
  <div className="min-w-0 flex-1">
583
588
  <p className="text-xs font-medium text-agent-foreground">
584
- {intro || '需要你的输入'}
589
+ {intro || t('Your input is needed')}
585
590
  </p>
586
591
  <p className="mt-0.5 text-[11px] text-agent-muted-foreground">
587
- 问题 {step + 1} / {normalized.length}
592
+ {t('Question {current} / {total}', { current: step + 1, total: normalized.length })}
588
593
  {bottomHint ? ` · ${bottomHint}` : ''}
589
594
  </p>
590
595
  </div>
591
596
  {current.type === 'password' || current.multiSelect ? (
592
597
  <span className="rounded-full border border-agent-border px-2 py-0.5 text-[10px] font-medium text-agent-muted-foreground">
593
- {current.type === 'password' ? '密码输入' : '可多选'}
598
+ {current.type === 'password' ? t('Password input') : t('Multiple choice')}
594
599
  </span>
595
600
  ) : null}
596
601
  </div>
@@ -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>