@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steerable/agent-shell-web",
3
- "version": "0.6.52",
3
+ "version": "0.6.55",
4
4
  "type": "module",
5
5
  "description": "Steerable framework — product-neutral renderer SPA source for @steerable/agent-shell (React + Vite). Consumed by product web entry points via the `@/` alias (anchored at this package's src/) and the createProductViteConfig factory. Published as a source package (src/), plus the compiled vite.base config factory (vite.base.js + .d.ts) for Node-side config loading.",
6
6
  "license": "Apache-2.0",
@@ -45,8 +45,8 @@
45
45
  "react-router-dom": "^6.30.0",
46
46
  "rehype-raw": "^7.0.0",
47
47
  "remark-gfm": "^4.0.1",
48
- "@steerable/agent-protocol": "0.6.52",
49
- "@steerable/agent-ui": "0.6.52"
48
+ "@steerable/agent-protocol": "0.6.55",
49
+ "@steerable/agent-ui": "0.6.55"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@tailwindcss/vite": "^4.3.0",
@@ -59,7 +59,7 @@
59
59
  "typescript": "^5.8.3",
60
60
  "vite": "^5.4.21",
61
61
  "vitest": "^3.2.4",
62
- "@steerable/pack-sdk": "0.6.52"
62
+ "@steerable/pack-sdk": "0.6.55"
63
63
  },
64
64
  "scripts": {
65
65
  "gen": "echo agent-shell-web",
package/src/brand.ts CHANGED
@@ -28,7 +28,7 @@ export const BRAND_TITLE: string =
28
28
  * 空字符串表示不显示这句。未 define 时用这句默认。
29
29
  */
30
30
  export const BRAND_HOME_HINT: string =
31
- import.meta.env.VITE_BRAND_HOME_HINT ?? '输入消息,直接开始一段新对话。';
31
+ import.meta.env.VITE_BRAND_HOME_HINT ?? 'Type a message to start a new chat.';
32
32
 
33
33
  /** shell 默认 logo(Steerable 舵轮标);包品牌 logo 由包 web 模块注册覆盖。 */
34
34
  let brandLogoUrl: string = shellLogoUrl;
@@ -117,6 +117,7 @@ import {
117
117
  useAppRelease,
118
118
  } from "@/components/SidebarRelease";
119
119
  import { CreateProjectModal } from "@/components/CreateProjectModal";
120
+ import { t } from "@/i18n";
120
121
 
121
122
  function placeProjectMenu(anchor: HTMLElement): { top: number; left: number } {
122
123
  const box = anchor.getBoundingClientRect();
@@ -169,7 +170,7 @@ function ProjectOverflowMenu({
169
170
  <span className="min-w-0 truncate">{project.name}</span>
170
171
  </div>
171
172
  <div className="mt-0.5 pl-[22px] text-[10px] text-agent-muted-foreground">
172
- {chatCount} 个会话
173
+ {t("Chats: {count}", { count: chatCount })}
173
174
  </div>
174
175
  <div
175
176
  className="mt-0.5 truncate pl-[22px] font-mono text-[10px] text-agent-muted-foreground/70"
@@ -182,22 +183,22 @@ function ProjectOverflowMenu({
182
183
  <button
183
184
  type="button"
184
185
  role="menuitem"
185
- title="重命名项目"
186
+ title={t("Rename project")}
186
187
  onClick={onRename}
187
188
  className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-xs text-agent-foreground hover:bg-agent-foreground/5"
188
189
  >
189
190
  <LuPencil className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
190
- 重命名
191
+ {t("Rename")}
191
192
  </button>
192
193
  <button
193
194
  type="button"
194
195
  role="menuitem"
195
- title="编辑项目"
196
+ title={t("Edit project")}
196
197
  onClick={onChangeFolder}
197
198
  className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-xs text-agent-foreground hover:bg-agent-foreground/5"
198
199
  >
199
200
  <LuFolderOpen className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
200
- 编辑项目
201
+ {t("Edit project")}
201
202
  </button>
202
203
  {onReveal && (
203
204
  <button
@@ -215,12 +216,12 @@ function ProjectOverflowMenu({
215
216
  <button
216
217
  type="button"
217
218
  role="menuitem"
218
- title="删除项目(会话保留为无项目对话)"
219
+ title={t("Delete project (its chats stay as chats without a project)")}
219
220
  onClick={onDelete}
220
221
  className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-xs text-agent-foreground hover:bg-agent-destructive/10"
221
222
  >
222
223
  <LuTrash2 className="h-3.5 w-3.5 shrink-0" />
223
- 删除项目
224
+ {t("Delete project")}
224
225
  </button>
225
226
  </div>
226
227
  </>,
@@ -644,7 +645,7 @@ export function AgentSidebar({
644
645
  const { displayTitle, isAutomation } = parseChatTitle(chat.title);
645
646
  return {
646
647
  id: chat.id,
647
- title: displayTitle || "新会话",
648
+ title: displayTitle || t("New conversation"),
648
649
  isAutomation,
649
650
  isPinned: chat.isPinned,
650
651
  sortDate,
@@ -710,7 +711,7 @@ export function AgentSidebar({
710
711
  if (!map.has(label)) {
711
712
  map.set(label, {
712
713
  label,
713
- priority: getDateGroupPriority(label),
714
+ priority: getDateGroupPriority(chat.sortDate),
714
715
  items: [],
715
716
  });
716
717
  }
@@ -746,7 +747,7 @@ export function AgentSidebar({
746
747
  await setChatPinned(id, nextPinned);
747
748
  await refreshChats();
748
749
  } catch (err) {
749
- console.error("切换会话置顶失败:", err);
750
+ console.error("Failed to toggle chat pin:", err);
750
751
  } finally {
751
752
  setPinningChatId(null);
752
753
  }
@@ -787,22 +788,24 @@ export function AgentSidebar({
787
788
  : "text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground",
788
789
  ].join(" ")}
789
790
  title={
790
- chat.isAutomation ? `[由自动化触发] ${chat.title}` : chat.title
791
+ chat.isAutomation
792
+ ? t("[Triggered by automation] {title}", { title: chat.title })
793
+ : chat.title
791
794
  }
792
795
  >
793
796
  {needsUserInput ? (
794
797
  <span
795
798
  className="flex shrink-0 items-center justify-center text-amber-500 dark:text-amber-400"
796
- title="有问题需要输入"
797
- aria-label="等待用户输入"
799
+ title={t("Has a question that needs your input")}
800
+ aria-label={t("Waiting for user input")}
798
801
  >
799
802
  <LuCircleHelp className="h-3.5 w-3.5 animate-pulse" />
800
803
  </span>
801
804
  ) : isStreaming ? (
802
805
  <span
803
806
  className="flex shrink-0 items-center justify-center text-agent-foreground/70"
804
- title="正在对话中"
805
- aria-label="正在生成"
807
+ title={t("Conversation in progress")}
808
+ aria-label={t("Generating")}
806
809
  >
807
810
  <LuLoaderCircle className="h-3.5 w-3.5 animate-spin" />
808
811
  </span>
@@ -810,7 +813,7 @@ export function AgentSidebar({
810
813
  {chat.isAutomation && (
811
814
  <LuCloudCog
812
815
  className="h-3 w-3 shrink-0 text-agent-muted-foreground"
813
- aria-label="由自动化触发"
816
+ aria-label={t("Triggered by automation")}
814
817
  />
815
818
  )}
816
819
  <span className="min-w-0 truncate leading-none">{chat.title}</span>
@@ -832,8 +835,8 @@ export function AgentSidebar({
832
835
  }}
833
836
  disabled={isPinning}
834
837
  className="flex h-6 w-6 items-center justify-center rounded-full text-agent-foreground/70 transition-all duration-200 hover:bg-agent-foreground/10 hover:text-agent-foreground disabled:cursor-not-allowed"
835
- title={chat.isPinned ? "取消置顶" : "置顶会话"}
836
- aria-label={chat.isPinned ? "取消置顶" : "置顶会话"}
838
+ title={chat.isPinned ? t("Unpin") : t("Pin chat")}
839
+ aria-label={chat.isPinned ? t("Unpin") : t("Pin chat")}
837
840
  data-testid="sidebar-chat-pin"
838
841
  >
839
842
  {chat.isPinned ? (
@@ -850,8 +853,8 @@ export function AgentSidebar({
850
853
  }}
851
854
  disabled={isDeleting}
852
855
  className="flex h-6 w-6 items-center justify-center rounded-full text-agent-foreground/70 transition-all duration-200 hover:bg-agent-foreground/10 hover:text-agent-destructive disabled:cursor-not-allowed disabled:opacity-100"
853
- title="删除会话"
854
- aria-label="删除会话"
856
+ title={t("Delete chat")}
857
+ aria-label={t("Delete chat")}
855
858
  data-testid="sidebar-chat-delete"
856
859
  >
857
860
  <LuTrash2
@@ -917,8 +920,8 @@ export function AgentSidebar({
917
920
  type="button"
918
921
  onClick={onCollapse}
919
922
  className="flex h-7 w-7 items-center justify-center rounded-full text-agent-muted-foreground transition-colors duration-200 hover:bg-agent-foreground/5 hover:text-agent-foreground"
920
- title="收起侧边栏"
921
- aria-label="收起侧边栏"
923
+ title={t("Collapse sidebar")}
924
+ aria-label={t("Collapse sidebar")}
922
925
  >
923
926
  <LuPanelLeftClose className="h-3.5 w-3.5" />
924
927
  </button>
@@ -938,11 +941,11 @@ export function AgentSidebar({
938
941
  ? "bg-agent-foreground/10 font-medium text-agent-foreground"
939
942
  : "text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground",
940
943
  ].join(" ")}
941
- title="新建对话"
944
+ title={t("Start a new chat")}
942
945
  data-testid="sidebar-new-chat"
943
946
  >
944
947
  <LuSquarePen className="h-3.5 w-3.5" />
945
- <span>新对话</span>
948
+ <span>{t("New chat")}</span>
946
949
  </button>
947
950
  {pluginsAvailable && (
948
951
  <button
@@ -954,11 +957,11 @@ export function AgentSidebar({
954
957
  ? "bg-agent-foreground/10 font-medium text-agent-foreground"
955
958
  : "text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground",
956
959
  ].join(" ")}
957
- title="插件"
960
+ title={t("Plugins")}
958
961
  data-testid="sidebar-plugins"
959
962
  >
960
963
  <LuBlocks className="h-3.5 w-3.5" />
961
- <span>插件</span>
964
+ <span>{t("Plugins")}</span>
962
965
  </button>
963
966
  )}
964
967
  </div>
@@ -970,7 +973,7 @@ export function AgentSidebar({
970
973
  {isChatLoading && chats.length === 0 && projects.length === 0 ? (
971
974
  <div className="flex items-center justify-center py-4 text-xs text-agent-muted-foreground">
972
975
  <LuLoaderCircle className="mr-1.5 h-3 w-3 animate-spin" />
973
- 加载中...
976
+ {t("Loading...")}
974
977
  </div>
975
978
  ) : (
976
979
  <>
@@ -988,7 +991,7 @@ export function AgentSidebar({
988
991
  }}
989
992
  className="flex flex-1 min-w-0 items-center gap-1 text-left font-medium text-agent-muted-foreground hover:text-agent-foreground"
990
993
  >
991
- <span>置顶</span>
994
+ <span>{t("Pinned")}</span>
992
995
  {isPinnedExpanded ? (
993
996
  <LuChevronDown className="h-3 w-3 shrink-0 opacity-70" />
994
997
  ) : (
@@ -1018,7 +1021,7 @@ export function AgentSidebar({
1018
1021
  }}
1019
1022
  className="flex flex-1 min-w-0 items-center gap-1 text-left font-medium text-agent-muted-foreground hover:text-agent-foreground"
1020
1023
  >
1021
- <span>项目</span>
1024
+ <span>{t("Projects")}</span>
1022
1025
  {isProjectsExpanded ? (
1023
1026
  <LuChevronDown className="h-3 w-3 shrink-0 opacity-70" />
1024
1027
  ) : (
@@ -1033,8 +1036,8 @@ export function AgentSidebar({
1033
1036
  setCreateProjectOpen(true);
1034
1037
  }}
1035
1038
  className="flex h-5 w-5 items-center justify-center rounded-full text-agent-muted-foreground opacity-0 transition-opacity duration-200 hover:bg-agent-foreground/10 hover:text-agent-foreground group-hover/section:opacity-100 focus:opacity-100"
1036
- title="新建项目"
1037
- aria-label="新建项目"
1039
+ title={t("New project")}
1040
+ aria-label={t("New project")}
1038
1041
  >
1039
1042
  <LuFolderPlus className="h-3.5 w-3.5" />
1040
1043
  </button>
@@ -1044,7 +1047,7 @@ export function AgentSidebar({
1044
1047
  <div className="space-y-0.5">
1045
1048
  {projectGroups.length === 0 ? (
1046
1049
  <div className="px-2.5 py-1 text-[11px] text-agent-muted-foreground/60">
1047
- 暂无项目
1050
+ {t("No projects")}
1048
1051
  </div>
1049
1052
  ) : (
1050
1053
  projectGroups.map(({ project, items }) => (
@@ -1103,13 +1106,13 @@ export function AgentSidebar({
1103
1106
  type="button"
1104
1107
  onClick={() => handleOpenNewChat(project.id)}
1105
1108
  className="flex h-5 w-5 items-center justify-center rounded-full text-agent-muted-foreground hover:bg-agent-foreground/10 hover:text-agent-foreground"
1106
- title="在此项目下新建对话"
1109
+ title={t("New chat in this project")}
1107
1110
  >
1108
1111
  <LuSquarePen className="h-3 w-3" />
1109
1112
  </button>
1110
1113
  <button
1111
1114
  type="button"
1112
- aria-label="项目菜单"
1115
+ aria-label={t("Project menu")}
1113
1116
  aria-expanded={projectMenu?.id === project.id}
1114
1117
  onClick={(event) => {
1115
1118
  const button = event.currentTarget;
@@ -1120,7 +1123,7 @@ export function AgentSidebar({
1120
1123
  );
1121
1124
  }}
1122
1125
  className="flex h-5 w-5 items-center justify-center rounded-full text-agent-muted-foreground hover:bg-agent-foreground/10 hover:text-agent-foreground"
1123
- title="项目菜单"
1126
+ title={t("Project menu")}
1124
1127
  >
1125
1128
  <LuEllipsis className="h-3 w-3" />
1126
1129
  </button>
@@ -1132,7 +1135,7 @@ export function AgentSidebar({
1132
1135
  <div className="space-y-0.5 pl-2">
1133
1136
  {items.length === 0 ? (
1134
1137
  <div className="px-2.5 py-0.5 text-[11px] text-agent-muted-foreground/60">
1135
- 暂无会话 — hover 项目名点 + 新建
1138
+ {t("No chats yet. Hover the project name and click + to start one.")}
1136
1139
  </div>
1137
1140
  ) : (
1138
1141
  items.map((chat) => renderChatRow(chat))
@@ -1160,7 +1163,7 @@ export function AgentSidebar({
1160
1163
  }}
1161
1164
  className="flex flex-1 min-w-0 items-center gap-1 text-left font-medium text-agent-muted-foreground hover:text-agent-foreground"
1162
1165
  >
1163
- <span>最近</span>
1166
+ <span>{t("Recent")}</span>
1164
1167
  {isRecentsExpanded ? (
1165
1168
  <LuChevronDown className="h-3 w-3 shrink-0 opacity-70" />
1166
1169
  ) : (
@@ -1175,8 +1178,8 @@ export function AgentSidebar({
1175
1178
  handleOpenNewChat();
1176
1179
  }}
1177
1180
  className="flex h-5 w-5 items-center justify-center rounded-full text-agent-muted-foreground hover:bg-agent-foreground/10 hover:text-agent-foreground"
1178
- title="新对话"
1179
- aria-label="新对话"
1181
+ title={t("New chat")}
1182
+ aria-label={t("New chat")}
1180
1183
  >
1181
1184
  <LuSquarePen className="h-3 w-3" />
1182
1185
  </button>
@@ -1188,11 +1191,11 @@ export function AgentSidebar({
1188
1191
  chats.length === 0 && projects.length === 0 ? (
1189
1192
  <div className="flex flex-col items-center gap-1.5 py-4 text-xs text-agent-muted-foreground">
1190
1193
  <LuMessageSquare className="h-4 w-4 text-agent-muted-foreground/60" />
1191
- 暂无会话
1194
+ {t("No chats yet")}
1192
1195
  </div>
1193
1196
  ) : (
1194
1197
  <div className="px-2.5 py-1 text-[11px] text-agent-muted-foreground/60">
1195
- 暂无会话
1198
+ {t("No chats yet")}
1196
1199
  </div>
1197
1200
  )
1198
1201
  ) : (
@@ -1217,12 +1220,12 @@ export function AgentSidebar({
1217
1220
  {isLoadingMoreChats ? (
1218
1221
  <span className="inline-flex items-center gap-1">
1219
1222
  <LuLoaderCircle className="h-3 w-3 animate-spin" />
1220
- 加载更多...
1223
+ {t("Loading more...")}
1221
1224
  </span>
1222
1225
  ) : hasMoreChats ? (
1223
- "继续下滑加载更多"
1226
+ t("Scroll down to load more")
1224
1227
  ) : (
1225
- `共 ${normalizedChats.length} 个会话`
1228
+ t("Total chats: {count}", { count: normalizedChats.length })
1226
1229
  )}
1227
1230
  </div>
1228
1231
  )}
@@ -1250,11 +1253,11 @@ export function AgentSidebar({
1250
1253
  ? "bg-agent-foreground/10 font-medium text-agent-foreground"
1251
1254
  : "text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground",
1252
1255
  ].join(" ")}
1253
- title="设置"
1256
+ title={t("Settings")}
1254
1257
  data-testid="sidebar-llm-settings"
1255
1258
  >
1256
1259
  <LuSettings className="h-3.5 w-3.5" />
1257
- <span>设置</span>
1260
+ <span>{t("Settings")}</span>
1258
1261
  <SidebarVersionLabel release={release} />
1259
1262
  </button>
1260
1263
  )}
@@ -1277,7 +1280,7 @@ export function AgentSidebar({
1277
1280
  project={project}
1278
1281
  chatCount={chatCount}
1279
1282
  anchor={projectMenu.anchor}
1280
- revealLabel={isMac ? "在访达中显示" : "在文件管理器中显示"}
1283
+ revealLabel={isMac ? t("Show in Finder") : t("Show in file manager")}
1281
1284
  onClose={closeProjectMenu}
1282
1285
  onRename={() => {
1283
1286
  setProjectMenu(null);
@@ -1306,10 +1309,12 @@ export function AgentSidebar({
1306
1309
 
1307
1310
  <ConfirmDialog
1308
1311
  open={pendingDeleteChat !== null}
1309
- title="删除会话"
1312
+ title={t("Delete chat")}
1310
1313
  description={
1311
1314
  pendingDeleteChat
1312
- ? `确定删除「${pendingDeleteChat.title}」?此操作无法撤销。`
1315
+ ? t("Delete \"{title}\"? This cannot be undone.", {
1316
+ title: pendingDeleteChat.title,
1317
+ })
1313
1318
  : ""
1314
1319
  }
1315
1320
  pending={
@@ -1324,13 +1329,16 @@ export function AgentSidebar({
1324
1329
  />
1325
1330
  <ConfirmDialog
1326
1331
  open={pendingDeleteProject !== null}
1327
- title="删除项目"
1332
+ title={t("Delete project")}
1328
1333
  description={
1329
1334
  pendingDeleteProject
1330
- ? `确定删除项目「${pendingDeleteProject.name}」?会话会保留为无项目对话。`
1335
+ ? t(
1336
+ "Delete project \"{name}\"? Its chats will stay as chats without a project.",
1337
+ { name: pendingDeleteProject.name },
1338
+ )
1331
1339
  : ""
1332
1340
  }
1333
- confirmLabel="删除项目"
1341
+ confirmLabel={t("Delete project")}
1334
1342
  pending={
1335
1343
  pendingDeleteProject !== null &&
1336
1344
  deletingProjectId === pendingDeleteProject.id
@@ -19,6 +19,7 @@ import {
19
19
  saveJsonFile,
20
20
  } from '@/lib/portable';
21
21
  import { hostToolChrome } from '@/lib/host-tools';
22
+ import { t } from '@/i18n';
22
23
 
23
24
  interface ChatHeaderProps {
24
25
  chat: LocalChat | null;
@@ -147,8 +148,8 @@ export function ChatHeader({
147
148
  useEffect(() => {
148
149
  if (!branchMenuOpen) return;
149
150
  const onPointerDown = (e: MouseEvent) => {
150
- const t = e.target as Node;
151
- if (buttonRef.current?.contains(t) || menuRef.current?.contains(t)) return;
151
+ const target = e.target as Node;
152
+ if (buttonRef.current?.contains(target) || menuRef.current?.contains(target)) return;
152
153
  setBranchMenuOpen(false);
153
154
  };
154
155
  document.addEventListener('mousedown', onPointerDown);
@@ -166,7 +167,7 @@ export function ChatHeader({
166
167
  >
167
168
  {!branches || branchCount === 0 ? (
168
169
  <div className="px-2.5 py-1.5 text-xs text-agent-muted-foreground">
169
- 暂无分支 — 重新生成回复后,旧版本会保留在这里。
170
+ {t('No branches yet. After you regenerate a reply, the old version stays here.')}
170
171
  </div>
171
172
  ) : (
172
173
  <>
@@ -201,7 +202,7 @@ export function ChatHeader({
201
202
  data-action="branch-tree"
202
203
  >
203
204
  <LuListTree className="h-3 w-3 shrink-0" />
204
- 查看完整分支树
205
+ {t('View full branch tree')}
205
206
  </button>
206
207
  </div>
207
208
  </>
@@ -218,7 +219,7 @@ export function ChatHeader({
218
219
  className="break-words text-xs font-medium text-agent-foreground"
219
220
  title={chat?.title}
220
221
  >
221
- {chat?.title ?? '未选择对话'}
222
+ {chat?.title ?? t('No chat selected')}
222
223
  </span>
223
224
  </div>
224
225
  {chat && isPortableEnabled() && (
@@ -229,7 +230,7 @@ export function ChatHeader({
229
230
  setExporting(true);
230
231
  setExportError(null);
231
232
  void fetchChatDocument(chat.id)
232
- .then((doc) => saveJsonFile(safeDownloadName(chat.title, '对话'), doc))
233
+ .then((doc) => saveJsonFile(safeDownloadName(chat.title, t('Chat')), doc))
233
234
  .then((saved) => {
234
235
  if (!saved) return;
235
236
  })
@@ -237,12 +238,12 @@ export function ChatHeader({
237
238
  .finally(() => setExporting(false));
238
239
  }}
239
240
  className="flex h-7 shrink-0 items-center gap-1 rounded-full px-2 text-xs text-agent-muted-foreground transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground disabled:opacity-60"
240
- title={exportError ?? '导出此对话'}
241
- aria-label="导出此对话"
241
+ title={exportError ?? t('Export this chat')}
242
+ aria-label={t('Export this chat')}
242
243
  data-testid="header-export-chat"
243
244
  >
244
245
  <LuDownload className="h-3.5 w-3.5" />
245
- {exportError ? '导出失败' : exporting ? '导出中' : '导出'}
246
+ {exportError ? t('Export failed') : exporting ? t('Exporting') : t('Export')}
246
247
  </button>
247
248
  )}
248
249
  {chat && onBranchSwitched && (
@@ -252,8 +253,8 @@ export function ChatHeader({
252
253
  type="button"
253
254
  onClick={() => void openBranchMenu()}
254
255
  className="flex h-7 items-center gap-1 rounded-full px-2 text-xs text-agent-muted-foreground transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground"
255
- title="会话分支(重新生成产生的分叉)"
256
- aria-label="会话分支"
256
+ title={t('Chat branches (forks created by regenerating)')}
257
+ aria-label={t('Chat branches')}
257
258
  data-action="branches"
258
259
  >
259
260
  <LuGitBranch className="h-3.5 w-3.5" />
@@ -282,7 +283,11 @@ export function ChatHeader({
282
283
  : 'w-7 justify-center text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground'
283
284
  }`}
284
285
  title={taskButtonTitle(taskBadge, taskShortcut)}
285
- aria-label={taskBadge ? `后台任务:${taskBadge.title}` : '后台任务'}
286
+ aria-label={
287
+ taskBadge
288
+ ? t('Background tasks: {title}', { title: taskBadge.title })
289
+ : t('Background tasks')
290
+ }
286
291
  data-action="tasks"
287
292
  data-task-state={taskBadge?.state}
288
293
  data-task-shortcut={taskShortcut?.kind}
@@ -308,7 +313,11 @@ export function ChatHeader({
308
313
  ? 'bg-agent-foreground/10 text-agent-foreground'
309
314
  : 'text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground'
310
315
  }`}
311
- title={`${open ? '关闭' : '打开'}${slot.title}`}
316
+ title={
317
+ open
318
+ ? t('Close {name}', { name: slot.title })
319
+ : t('Open {name}', { name: slot.title })
320
+ }
312
321
  aria-label={slot.title}
313
322
  aria-pressed={open}
314
323
  data-action={`chat-slot-${slot.slotId}`}
@@ -328,8 +337,12 @@ export function ChatHeader({
328
337
  ? 'bg-agent-foreground/10 text-agent-foreground'
329
338
  : 'text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground'
330
339
  }`}
331
- title={`${rightPanel === 'terminal' ? '关闭' : '打开'}终端面板 (${isMac ? '⌘T' : 'Ctrl+T'})`}
332
- aria-label="终端"
340
+ title={
341
+ rightPanel === 'terminal'
342
+ ? t('Close terminal panel ({shortcut})', { shortcut: isMac ? '⌘T' : 'Ctrl+T' })
343
+ : t('Open terminal panel ({shortcut})', { shortcut: isMac ? '⌘T' : 'Ctrl+T' })
344
+ }
345
+ aria-label={t('Terminal')}
333
346
  aria-pressed={rightPanel === 'terminal'}
334
347
  data-action="terminal"
335
348
  data-testid="header-terminal"
@@ -386,14 +399,16 @@ function describeTaskBadge(summary: ChatTaskSummary): TaskBadge | null {
386
399
  if (summary.total === 0) return null;
387
400
 
388
401
  const detail = [
389
- summary.running > 0 ? `${summary.running} 个运行中` : null,
390
- summary.blocked > 0 ? `${summary.blocked} 个等依赖` : null,
391
- summary.needsReview > 0 ? `${summary.needsReview} 个待合并` : null,
392
- summary.failed > 0 ? `${summary.failed} 个失败` : null,
402
+ summary.running > 0 ? t('{count} in progress', { count: summary.running }) : null,
403
+ summary.blocked > 0 ? t('{count} waiting on dependencies', { count: summary.blocked }) : null,
404
+ summary.needsReview > 0 ? t('{count} awaiting merge', { count: summary.needsReview }) : null,
405
+ summary.failed > 0 ? t('{count} failed', { count: summary.failed }) : null,
393
406
  ]
394
407
  .filter((part) => part !== null)
395
- .join(',');
396
- const title = `共 ${summary.total} 个后台任务${detail ? `(${detail})` : ''}`;
408
+ .join(t(', '));
409
+ const title = detail
410
+ ? t('Background tasks: {total} ({detail})', { total: summary.total, detail })
411
+ : t('Background tasks: {total}', { total: summary.total });
397
412
 
398
413
  // 等依赖的任务也算在推进中:依赖跑完它会自动点火,用户不用做任何事。
399
414
  const active = summary.running + summary.blocked;
@@ -459,11 +474,12 @@ function taskButtonTitle(
459
474
  badge: TaskBadge | null,
460
475
  shortcut: TaskShortcut | null,
461
476
  ): string {
462
- const base = badge?.title ?? '后台任务(跨轮运行,可查看结果与合并 worktree)';
477
+ const base =
478
+ badge?.title ?? t('Background tasks (run across turns; view results and merge worktrees)');
463
479
  if (!shortcut) return base;
464
480
  return shortcut.kind === 'process'
465
- ? `${base} · 点击查看推理过程`
466
- : `${base} · 点击处理 worktree`;
481
+ ? t('{base} · Click to view the reasoning', { base })
482
+ : t('{base} · Click to handle the worktree', { base });
467
483
  }
468
484
 
469
485
  /** Pin the branch menu to the trigger, clamped inside the chat panel so
@@ -505,7 +521,7 @@ function BranchRow({
505
521
  data-branch-row
506
522
  data-active={active || undefined}
507
523
  >
508
- <span className="min-w-0 flex-1 truncate">{label || '(空分支)'}</span>
524
+ <span className="min-w-0 flex-1 truncate">{label || t('(empty branch)')}</span>
509
525
  {active && (
510
526
  <LuCheck className="h-3 w-3 shrink-0 text-emerald-600 dark:text-emerald-400" />
511
527
  )}
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { useEffect, useId } from 'react';
5
5
  import { createPortal } from 'react-dom';
6
+ import { t } from '@/i18n';
6
7
 
7
8
  export interface ConfirmDialogProps {
8
9
  open: boolean;
@@ -19,7 +20,7 @@ export function ConfirmDialog({
19
20
  open,
20
21
  title,
21
22
  description,
22
- confirmLabel = '删除',
23
+ confirmLabel = t('Delete'),
23
24
  pending = false,
24
25
  onCancel,
25
26
  onConfirm,
@@ -71,7 +72,7 @@ export function ConfirmDialog({
71
72
  className="h-9 rounded-full px-4 text-sm text-agent-muted-foreground transition-colors hover:bg-agent-muted hover:text-agent-foreground disabled:opacity-40"
72
73
  data-testid={`${testId}-cancel`}
73
74
  >
74
- 取消
75
+ {t('Cancel')}
75
76
  </button>
76
77
  <button
77
78
  type="button"
@@ -80,7 +81,7 @@ export function ConfirmDialog({
80
81
  className="h-9 rounded-full bg-agent-destructive px-4 text-sm font-medium text-white transition-opacity hover:opacity-90 disabled:opacity-40"
81
82
  data-testid={`${testId}-confirm`}
82
83
  >
83
- {pending ? '删除中…' : confirmLabel}
84
+ {pending ? t('Deleting…') : confirmLabel}
84
85
  </button>
85
86
  </div>
86
87
  </div>