@steerable/agent-shell-web 0.6.52 → 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 (93) 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 +138 -42
  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 +193 -71
  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 +24 -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 +19 -6
  82. package/src/lib/host-file-drop.ts +68 -0
  83. package/src/lib/host-paste.ts +38 -9
  84. package/src/lib/http-bridge.ts +4 -1
  85. package/src/lib/portable.ts +8 -7
  86. package/src/lib/show-thinking-content.ts +15 -3
  87. package/src/lib/tauri-bridge.ts +5 -0
  88. package/src/main.tsx +25 -2
  89. package/src/pages/AgentPage.tsx +17 -12
  90. package/src/pages/ChatPanelPreviewPage.tsx +46 -44
  91. package/src/pages/SettingsPage.tsx +48 -42
  92. package/src/vite-env.d.ts +3 -0
  93. 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" />
@@ -32,9 +32,12 @@ import {
32
32
  import type { LocalChat, LocalChatAgent } from '@/lib/local-api';
33
33
  import type { ExecPolicy } from '@/lib/exec-policy';
34
34
  import { getWebChatModes, hostToolCapability, settingsChrome } from '@/lib/host-tools';
35
- import { hostClipboardAvailable, requestHostPaste } from '@/lib/host-paste';
36
- import type { AttachmentFile } from '@/lib/attachments';
35
+ import { hostClipboardAvailable, requestHostPaste, type HostClipboardFile } from '@/lib/host-paste';
36
+ import { attachmentFromPath, type AttachmentFile } from '@/lib/attachments';
37
+ import { dropPointHitsRect, hostFileDropAvailable, listenHostFileDrop } from '@/lib/host-file-drop';
37
38
  import type { SteerOutcome } from '@steerable/agent-ui';
39
+ import { t } from '@/i18n';
40
+ import { agentLabel } from '@/i18n/agent-label';
38
41
  import {
39
42
  isHiddenSlashSkill,
40
43
  resolveSlashTool,
@@ -277,11 +280,11 @@ function parseMentionSegments(
277
280
  if (!ranges.some((range) => offset < range.end && end > range.start)) {
278
281
  const name = match.slice(1);
279
282
  const agent = agents.find((a) => a.name === name || a.slug === name);
280
- const chat = chats.find((c) => (c.title || '未命名对话') === name);
283
+ const chat = chats.find((c) => (c.title || t('Untitled chat')) === name);
281
284
  const ref: MentionReference | undefined = agent
282
285
  ? { type: 'agent', id: agent.id, label: agent.name }
283
286
  : chat
284
- ? { type: 'chat', id: chat.id, label: chat.title || '未命名对话' }
287
+ ? { type: 'chat', id: chat.id, label: chat.title || t('Untitled chat') }
285
288
  : undefined;
286
289
 
287
290
  ranges.push({ start: offset, end, text: match, ref });
@@ -372,7 +375,7 @@ function writeEditorValue(
372
375
  const isChat = seg.ref?.type === 'chat';
373
376
  const isSkill = seg.ref?.type === 'skill';
374
377
  const isMcp = seg.ref?.type === 'mcp';
375
- const removeLabel = `移除 ${seg.text}`;
378
+ const removeLabel = t('Remove {name}', { name: seg.text });
376
379
 
377
380
  const chip = document.createElement('button');
378
381
  chip.type = 'button';
@@ -714,6 +717,23 @@ function clipboardOf(event: ClipboardEvent<HTMLDivElement>): DataTransfer | null
714
717
  return native.clipboardData ?? event.clipboardData ?? null;
715
718
  }
716
719
 
720
+ function fileFromHostClipboard(file: HostClipboardFile): File | null {
721
+ if (!file.dataBase64) return null;
722
+ let binary: string;
723
+ try {
724
+ binary = atob(file.dataBase64);
725
+ } catch {
726
+ return null;
727
+ }
728
+ const bytes = new Uint8Array(binary.length);
729
+ for (let index = 0; index < binary.length; index += 1) {
730
+ bytes[index] = binary.charCodeAt(index);
731
+ }
732
+ return new File([bytes], file.name || 'image.png', {
733
+ type: file.mime || 'application/octet-stream',
734
+ });
735
+ }
736
+
717
737
  function namePastedImage(file: File, serial: number, now: Date): File {
718
738
  if (!file.type.startsWith('image/')) return file;
719
739
  if (file.name.trim() && !GENERIC_CLIPBOARD_IMAGE_NAME.test(file.name.trim())) return file;
@@ -760,7 +780,7 @@ function AttachmentChip({
760
780
  type="button"
761
781
  onClick={onRemove}
762
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"
763
- aria-label={`移除文件 ${file.name}`}
783
+ aria-label={t('Remove file {name}', { name: file.name })}
764
784
  >
765
785
  <LuX className="h-3 w-3" />
766
786
  </button>
@@ -781,7 +801,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
781
801
  onRemoveFollowUp,
782
802
  isStreaming = false,
783
803
  disabled = false,
784
- placeholder = '向 Agent 发送消息…',
804
+ placeholder = t('Message the Agent...'),
785
805
  currentAgent,
786
806
  agents = [],
787
807
  chats = [],
@@ -835,7 +855,11 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
835
855
  const compositionEndAtRef = useRef<number>(0);
836
856
 
837
857
  const fileInputRef = useRef<HTMLInputElement>(null);
858
+ const dropBoxRef = useRef<HTMLDivElement>(null);
838
859
  const pasteSerialRef = useRef(0);
860
+ const attachDroppedRef = useRef<(paths: string[]) => void>(() => {});
861
+ const dragHoverRef = useRef<(hovering: boolean) => void>(() => {});
862
+ const hostPasteFilesRef = useRef<(files: HostClipboardFile[]) => void>(() => {});
839
863
  const adoptPastedTextRef = useRef<(text: string) => void>(() => {});
840
864
  // 工具栏"空间不足时优先隐藏快捷键提示"的测量 refs,见下方 useLayoutEffect。
841
865
  const toolbarRowRef = useRef<HTMLDivElement>(null);
@@ -919,8 +943,10 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
919
943
  .map((chat) => ({
920
944
  type: 'chat',
921
945
  id: chat.id,
922
- label: chat.title || '未命名对话',
923
- 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'),
924
950
  }));
925
951
 
926
952
  return [...agentSuggestions, ...chatSuggestions].slice(0, MAX_MENTION_SUGGESTIONS);
@@ -1020,11 +1046,11 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1020
1046
  const mcpGroup = suggestions.filter((s) => s.kind === 'mcp');
1021
1047
  let idx = 0;
1022
1048
  if (skillGroup.length > 0) {
1023
- rows.push({ type: 'header', label: '指定运行的本地技能 (Skill)' });
1049
+ rows.push({ type: 'header', label: t('Run a local skill (Skill)') });
1024
1050
  for (const s of skillGroup) rows.push({ type: 'item', item: s, idx: idx++ });
1025
1051
  }
1026
1052
  if (mcpGroup.length > 0) {
1027
- rows.push({ type: 'header', label: '指定调用的 MCP 工具 (MCP Tool)' });
1053
+ rows.push({ type: 'header', label: t('Call an MCP tool (MCP Tool)') });
1028
1054
  for (const s of mcpGroup) rows.push({ type: 'item', item: s, idx: idx++ });
1029
1055
  }
1030
1056
  return rows;
@@ -1240,6 +1266,33 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1240
1266
  path: (file as { path?: string }).path || '',
1241
1267
  file,
1242
1268
  });
1269
+ attachDroppedRef.current = (paths) => {
1270
+ if (disabled || !allowFileAttach) return;
1271
+ addFiles(paths.map(attachmentFromPath));
1272
+ };
1273
+ dragHoverRef.current = (hovering) => {
1274
+ if (disabled || !allowFileAttach) {
1275
+ setIsDragging(false);
1276
+ return;
1277
+ }
1278
+ setIsDragging(hovering);
1279
+ };
1280
+ hostPasteFilesRef.current = (incoming) => {
1281
+ if (disabled || !allowFileAttach) return;
1282
+ const now = new Date();
1283
+ const next: AttachmentFile[] = [];
1284
+ for (const item of incoming) {
1285
+ if (item.path) {
1286
+ next.push(attachmentFromPath(item.path));
1287
+ continue;
1288
+ }
1289
+ const blob = fileFromHostClipboard(item);
1290
+ if (!blob) continue;
1291
+ pasteSerialRef.current += 1;
1292
+ next.push(toAttachment(namePastedImage(blob, pasteSerialRef.current, now)));
1293
+ }
1294
+ if (next.length > 0) addFiles(next);
1295
+ };
1243
1296
 
1244
1297
  // 按钮点击 → 触发隐藏的 <input type="file" multiple>,由浏览器/Electron
1245
1298
  // 直接弹出系统文件选择框(不依赖 IPC,避免「点击无反应」)。
@@ -1449,14 +1502,44 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1449
1502
  event.preventDefault();
1450
1503
  adoptPastedTextRef.current(text);
1451
1504
  };
1505
+ const onHostPasteFiles = (event: Event) => {
1506
+ const files = (event as CustomEvent<HostClipboardFile[]>).detail;
1507
+ if (!Array.isArray(files) || files.length === 0) return;
1508
+ event.preventDefault();
1509
+ hostPasteFilesRef.current(files);
1510
+ };
1452
1511
  editor.addEventListener('compositionstart', onCompositionStart, true);
1453
1512
  editor.addEventListener('hostpaste', onHostPaste);
1513
+ editor.addEventListener('hostpastefiles', onHostPasteFiles);
1454
1514
  return () => {
1455
1515
  editor.removeEventListener('compositionstart', onCompositionStart, true);
1456
1516
  editor.removeEventListener('hostpaste', onHostPaste);
1517
+ editor.removeEventListener('hostpastefiles', onHostPasteFiles);
1457
1518
  };
1458
1519
  }, []);
1459
1520
 
1521
+ useEffect(() => {
1522
+ if (!hostFileDropAvailable()) return undefined;
1523
+ return listenHostFileDrop((event) => {
1524
+ const box = dropBoxRef.current;
1525
+ if (event.type === 'leave' || !box || event.x == null || event.y == null) {
1526
+ dragHoverRef.current(false);
1527
+ return;
1528
+ }
1529
+ const hit = dropPointHitsRect(
1530
+ { x: event.x, y: event.y },
1531
+ box.getBoundingClientRect(),
1532
+ window.devicePixelRatio,
1533
+ );
1534
+ if (event.type === 'drop') {
1535
+ dragHoverRef.current(false);
1536
+ if (hit) attachDroppedRef.current(event.paths);
1537
+ return;
1538
+ }
1539
+ dragHoverRef.current(hit);
1540
+ });
1541
+ }, []);
1542
+
1460
1543
  const handlePaste = (event: ClipboardEvent<HTMLDivElement>) => {
1461
1544
  // Read before preventDefault. WebKit clears clipboardData once the paste is canceled.
1462
1545
  const data = clipboardOf(event);
@@ -1608,7 +1691,11 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1608
1691
  // 进入待发队列 / 作为新回合发出),草稿都可以清;queued 额外
1609
1692
  // 提示用户"不是追加进当前回合"。
1610
1693
  if (outcome === 'queued') {
1611
- 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
+ );
1612
1699
  }
1613
1700
  onChange('');
1614
1701
  });
@@ -1695,7 +1782,9 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1695
1782
  onAutoContinue={() => askUserPrompt.answer({})}
1696
1783
  bottomHint={
1697
1784
  askUserPrompt.pendingCount > 0
1698
- ? `还有 ${askUserPrompt.pendingCount} 组问题待回答`
1785
+ ? t('{count} more question groups to answer', {
1786
+ count: askUserPrompt.pendingCount,
1787
+ })
1699
1788
  : undefined
1700
1789
  }
1701
1790
  />
@@ -1735,7 +1824,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1735
1824
  {mentionQuery && mentionSuggestions.length > 0 && (
1736
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">
1737
1826
  <div className="px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-agent-muted-foreground">
1738
- @ 选择 Agent 或历史对话
1827
+ {t('@ Choose an Agent or past chat')}
1739
1828
  </div>
1740
1829
  {mentionSuggestions.map((item, idx) => (
1741
1830
  <button
@@ -1769,7 +1858,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1769
1858
  <span className="flex items-center gap-1.5">
1770
1859
  <span className="truncate text-xs font-medium">@{item.label}</span>
1771
1860
  <span className="shrink-0 rounded-full bg-agent-muted px-1.5 py-0.5 text-[10px] text-agent-muted-foreground">
1772
- {item.type === 'agent' ? 'Agent' : '对话'}
1861
+ {item.type === 'agent' ? 'Agent' : t('Chat')}
1773
1862
  </span>
1774
1863
  </span>
1775
1864
  {item.description && (
@@ -1822,7 +1911,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1822
1911
  <div className="text-xs font-medium truncate">
1823
1912
  {row.item.skill.displayName ? (
1824
1913
  <>
1825
- {row.item.skill.displayName}
1914
+ {t(row.item.skill.displayName)}
1826
1915
  <span className="ml-1.5 text-[10px] font-normal text-agent-muted-foreground/70">
1827
1916
  /{row.item.skill.name}
1828
1917
  </span>
@@ -1862,6 +1951,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1862
1951
  </div>
1863
1952
  )}
1864
1953
  <div
1954
+ ref={dropBoxRef}
1865
1955
  className={`chat-input-box flex flex-col rounded-agent-lg border bg-agent-canvas shadow-sm transition-all duration-200 ${
1866
1956
  isDragging
1867
1957
  ? 'border-blue-500 ring-2 ring-blue-500/20'
@@ -1876,7 +1966,10 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1876
1966
  {pendingFollowUps.length > 0 && (
1877
1967
  <div className="flex flex-col gap-1 border-b border-agent-border bg-agent-muted/30 px-2.5 py-1.5">
1878
1968
  <div className="text-[11px] font-medium text-agent-muted-foreground">
1879
- 排队中({pendingFollowUps.length})· 本轮结束后自动发出 · 停止后恢复到输入框
1969
+ {t(
1970
+ 'Queued ({count}) · Sent when this turn ends · Restored to the input box if stopped',
1971
+ { count: pendingFollowUps.length },
1972
+ )}
1880
1973
  </div>
1881
1974
  {pendingFollowUps.map((text, i) => (
1882
1975
  <div
@@ -1887,7 +1980,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1887
1980
  {onRemoveFollowUp && (
1888
1981
  <button
1889
1982
  type="button"
1890
- aria-label={`撤回排队消息 ${i + 1}`}
1983
+ aria-label={t('Withdraw queued message {index}', { index: i + 1 })}
1891
1984
  onClick={() => onRemoveFollowUp(i)}
1892
1985
  className="shrink-0 text-agent-muted-foreground opacity-0 transition-opacity hover:text-agent-destructive group-hover:opacity-100"
1893
1986
  >
@@ -1979,8 +2072,8 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1979
2072
  onClick={handlePickFiles}
1980
2073
  disabled={disabled || isStreaming}
1981
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"
1982
- title="上传文件,或在输入框粘贴图片"
1983
- aria-label="上传文件"
2075
+ title={t('Upload files, or paste images into the input box')}
2076
+ aria-label={t('Upload files')}
1984
2077
  data-testid="chat-attach"
1985
2078
  >
1986
2079
  <LuPaperclip className="h-3 w-3" />
@@ -2013,7 +2106,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
2013
2106
  <kbd className="rounded border border-agent-border bg-agent-muted px-1 font-sans text-[10px]">
2014
2107
  Enter
2015
2108
  </kbd>
2016
- <span className="ml-1">排队</span>
2109
+ <span className="ml-1">{t('Queue')}</span>
2017
2110
  </span>
2018
2111
  )}
2019
2112
  {onSteer && (
@@ -2025,14 +2118,14 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
2025
2118
  <kbd className="rounded border border-agent-border bg-agent-muted px-1 font-sans text-[10px]">
2026
2119
  Enter
2027
2120
  </kbd>
2028
- <span className="ml-1">插队</span>
2121
+ <span className="ml-1">{t('Interject')}</span>
2029
2122
  </span>
2030
2123
  )}
2031
2124
  <span className="ml-2">
2032
2125
  <kbd className="rounded border border-agent-border bg-agent-muted px-1 font-sans text-[10px]">
2033
2126
  Esc
2034
2127
  </kbd>
2035
- <span className="ml-1">停止</span>
2128
+ <span className="ml-1">{t('Stop')}</span>
2036
2129
  </span>
2037
2130
  </>
2038
2131
  ) : (
@@ -2040,7 +2133,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
2040
2133
  <kbd className="rounded border border-agent-border bg-agent-muted px-1 font-sans text-[10px]">
2041
2134
  Enter
2042
2135
  </kbd>
2043
- <span className="ml-1">发送</span>
2136
+ <span className="ml-1">{t('Send')}</span>
2044
2137
  <span className="ml-2">
2045
2138
  <kbd className="rounded border border-agent-border bg-agent-muted px-1 font-sans text-[10px]">
2046
2139
  Shift
@@ -2049,7 +2142,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
2049
2142
  <kbd className="rounded border border-agent-border bg-agent-muted px-1 font-sans text-[10px]">
2050
2143
  Enter
2051
2144
  </kbd>
2052
- <span className="ml-1">换行</span>
2145
+ <span className="ml-1">{t('New line')}</span>
2053
2146
  </span>
2054
2147
  </>
2055
2148
  )}
@@ -2069,11 +2162,14 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
2069
2162
  title={
2070
2163
  isStreaming
2071
2164
  ? pendingFollowUps.length > 0
2072
- ? `停止生成 (Esc) · ${pendingFollowUps.length} 条排队消息将恢复到输入框`
2073
- : '停止生成 (Esc)'
2074
- : '发送 (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)')
2075
2171
  }
2076
- aria-label={isStreaming ? '停止生成' : '发送消息'}
2172
+ aria-label={isStreaming ? t('Stop generating') : t('Send message')}
2077
2173
  data-testid="chat-send"
2078
2174
  >
2079
2175
  {isStreaming ? (
@@ -2091,7 +2187,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
2091
2187
  );
2092
2188
 
2093
2189
  function agentInitial(agent: LocalChatAgent): string {
2094
- return agent.name.trim()[0]?.toUpperCase() ?? 'A';
2190
+ return agentLabel(agent).trim()[0]?.toUpperCase() ?? 'A';
2095
2191
  }
2096
2192
 
2097
2193
  /**
@@ -2113,7 +2209,7 @@ function ModeToggle({
2113
2209
  <div
2114
2210
  className="inline-flex h-6 shrink-0 items-center rounded-full border border-agent-border bg-agent-canvas p-0.5"
2115
2211
  role="radiogroup"
2116
- aria-label="对话模式"
2212
+ aria-label={t('Chat mode')}
2117
2213
  data-testid="mode-toggle"
2118
2214
  >
2119
2215
  <button
@@ -2122,7 +2218,7 @@ function ModeToggle({
2122
2218
  aria-checked={mode === 'agent'}
2123
2219
  disabled={disabled}
2124
2220
  onClick={() => onChange('agent')}
2125
- title="Agent 模式:直接执行任务"
2221
+ title={t('Agent mode: carry out the task directly')}
2126
2222
  data-testid="mode-agent"
2127
2223
  className={[
2128
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',
@@ -2140,7 +2236,7 @@ function ModeToggle({
2140
2236
  aria-checked={mode === 'plan'}
2141
2237
  disabled={disabled}
2142
2238
  onClick={() => onChange('plan')}
2143
- title="Plan 模式:先制定计划,只读不执行"
2239
+ title={t('Plan mode: make a plan first, read only and no execution')}
2144
2240
  data-testid="mode-plan"
2145
2241
  className={[
2146
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',
@@ -2187,14 +2283,14 @@ function AgentSelect({
2187
2283
 
2188
2284
  const buttonTitle = useMemo(() => {
2189
2285
  const activeNames = [
2190
- agent.name,
2286
+ agentLabel(agent),
2191
2287
  ...mentionReferences
2192
2288
  .filter((ref) => ref.type === 'agent' && ref.id !== agent.id)
2193
2289
  .map((ref) => ref.label),
2194
2290
  ].filter(Boolean) as string[];
2195
2291
 
2196
- return `当前激活专家:${activeNames.join(', ')}`;
2197
- }, [agent.name, agent.id, mentionReferences]);
2292
+ return t('Active experts: {names}', { names: activeNames.join(', ') });
2293
+ }, [agent, mentionReferences]);
2198
2294
 
2199
2295
  return (
2200
2296
  <div
@@ -2213,7 +2309,7 @@ function AgentSelect({
2213
2309
  >
2214
2310
  <AgentDot agent={agent} />
2215
2311
  <span className="truncate font-medium">
2216
- {agent.name}
2312
+ {agentLabel(agent)}
2217
2313
  {mentionedAgentCount > 0 ? ` +${mentionedAgentCount}` : ''}
2218
2314
  </span>
2219
2315
  <LuChevronDown className="h-3 w-3 shrink-0 text-agent-muted-foreground" />
@@ -2225,7 +2321,7 @@ function AgentSelect({
2225
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"
2226
2322
  >
2227
2323
  <div className="px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-agent-muted-foreground">
2228
- 选择专家
2324
+ {t('Choose an expert')}
2229
2325
  </div>
2230
2326
  {agents.map((item) => {
2231
2327
  const isPrimary = item.id === agent.id;
@@ -2252,16 +2348,16 @@ function AgentSelect({
2252
2348
  <span className="min-w-0 flex-1">
2253
2349
  <span className="flex items-center gap-1.5">
2254
2350
  <span className="block truncate text-xs font-medium">
2255
- {item.name}
2351
+ {agentLabel(item)}
2256
2352
  </span>
2257
2353
  {isPrimary && (
2258
2354
  <span className="inline-flex items-center rounded bg-agent-foreground/10 px-1 py-0.5 text-[9px] font-medium text-agent-foreground">
2259
- 当前
2355
+ {t('Current')}
2260
2356
  </span>
2261
2357
  )}
2262
2358
  {!isPrimary && isMentioned && (
2263
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">
2264
- 已引入
2360
+ {t('Mentioned')}
2265
2361
  </span>
2266
2362
  )}
2267
2363
  </span>
@@ -2286,7 +2382,7 @@ function AgentSelect({
2286
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"
2287
2383
  >
2288
2384
  <LuBot className="h-3.5 w-3.5 shrink-0" />
2289
- <span className="text-xs font-medium">管理智能体</span>
2385
+ <span className="text-xs font-medium">{t('Manage agents')}</span>
2290
2386
  </button>
2291
2387
  )}
2292
2388
  </div>