@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
@@ -8,7 +8,7 @@
8
8
  * adapter:
9
9
  * - It wires the framework hook to the local-backend transport
10
10
  * (`@/lib/local-api`).
11
- * - It gates everything behind `isElectron()` so a renderer running in a
11
+ * - It gates everything behind `hasHostBridge()` so a renderer running in a
12
12
  * plain browser doesn't fire requests.
13
13
  * - It keeps the legacy `UseChatsAndAgentsResult` shape so callers
14
14
  * (`AgentLayout`, `useChatsAndAgents()` consumers) don't change.
@@ -35,7 +35,7 @@ import {
35
35
  type LocalChat,
36
36
  type LocalChatAgent,
37
37
  } from '@/lib/local-api';
38
- import { isElectron } from '@/lib/electron-bridge';
38
+ import { hasHostBridge } from '@/lib/host-bridge';
39
39
  import { pickDefaultAgentId } from '@/brand';
40
40
 
41
41
  const CHAT_PAGE_SIZE = 50;
@@ -66,12 +66,12 @@ export interface UseChatsAndAgentsResult {
66
66
  }
67
67
 
68
68
  export function useChatsAndAgents(): UseChatsAndAgentsResult {
69
- const isElectronRef = useRef(isElectron());
69
+ const hasHostBridgeRef = useRef(hasHostBridge());
70
70
 
71
71
  const transport = useMemo<ChatListTransport<LocalChat, LocalChatAgent>>(
72
72
  () => ({
73
73
  listChats: async ({ page, pageSize }) => {
74
- if (!isElectronRef.current) return { chats: [], hasMore: false };
74
+ if (!hasHostBridgeRef.current) return { chats: [], hasMore: false };
75
75
  const data = await listChats({ page, limit: pageSize });
76
76
  return {
77
77
  chats: data.chats,
@@ -79,13 +79,13 @@ export function useChatsAndAgents(): UseChatsAndAgentsResult {
79
79
  };
80
80
  },
81
81
  listAgents: async () => {
82
- if (!isElectronRef.current) return [];
82
+ if (!hasHostBridgeRef.current) return [];
83
83
  const data = await listChatAgents(false);
84
84
  return data.agents;
85
85
  },
86
86
  createChat: async ({ agentId, projectId }) => {
87
- if (!isElectronRef.current) {
88
- throw new Error('not in electron');
87
+ if (!hasHostBridgeRef.current) {
88
+ throw new Error('not connected to the host');
89
89
  }
90
90
  const res = await apiCreateChat({
91
91
  ...(agentId ? { agentId } : {}),
@@ -94,7 +94,7 @@ export function useChatsAndAgents(): UseChatsAndAgentsResult {
94
94
  return res.chatId;
95
95
  },
96
96
  deleteChat: async (chatId) => {
97
- if (!isElectronRef.current) return false;
97
+ if (!hasHostBridgeRef.current) return false;
98
98
  await apiDeleteChat(chatId);
99
99
  return true;
100
100
  },
@@ -110,7 +110,7 @@ export function useChatsAndAgents(): UseChatsAndAgentsResult {
110
110
  pageSize: CHAT_PAGE_SIZE,
111
111
  // The renderer mounts before electron decides whether the bridge is live;
112
112
  // skip the auto-fetch and trigger it manually after the SSR no-op check.
113
- skipInitialLoad: !isElectronRef.current,
113
+ skipInitialLoad: !hasHostBridgeRef.current,
114
114
  });
115
115
 
116
116
  // Default-select this flavor's builtin expert once the catalog loads.
@@ -3,7 +3,7 @@
3
3
  * 任一侧新建、改名或改目录后调用 refresh,其它订阅者一起更新。
4
4
  */
5
5
  import { useCallback, useEffect, useSyncExternalStore } from 'react';
6
- import { isElectron } from '@/lib/electron-bridge';
6
+ import { hasHostBridge } from '@/lib/host-bridge';
7
7
  import { hostToolChrome } from '@/lib/host-tools';
8
8
  import { listProjects, type LocalProject } from '@/lib/local-api';
9
9
 
@@ -36,7 +36,7 @@ export function getProjectsSnapshot(): ProjectsSnapshot {
36
36
  /** 重新拉取项目列表并通知所有订阅者。非 Electron 或关掉项目入口时清空。 */
37
37
  export async function refreshProjects(): Promise<void> {
38
38
  const seq = ++requestSeq;
39
- if (!isElectron() || !hostToolChrome('projects')) {
39
+ if (!hasHostBridge() || !hostToolChrome('projects')) {
40
40
  if (snapshot.projects.length > 0 || snapshot.error) {
41
41
  snapshot = EMPTY_SNAPSHOT;
42
42
  emit();
@@ -51,7 +51,7 @@ export async function refreshProjects(): Promise<void> {
51
51
  } catch (err) {
52
52
  if (seq !== requestSeq) return;
53
53
  const message = err instanceof Error ? err.message : String(err);
54
- console.error('获取项目列表失败:', err);
54
+ console.error('Failed to load projects:', err);
55
55
  snapshot = { projects: snapshot.projects, error: message };
56
56
  emit();
57
57
  }
@@ -0,0 +1,15 @@
1
+ import { t } from './runtime';
2
+
3
+ /**
4
+ * 内置智能体的界面名按 id 翻译。库里的 name 仍是种子原文,切换语言不改历史行。
5
+ * 用户自建智能体用库里的名字。
6
+ */
7
+ const BUILTIN_AGENT_LABELS: Record<string, string> = {
8
+ 'local-assistant': 'Computer operator',
9
+ 'all-round-assistant': 'Assistant',
10
+ };
11
+
12
+ export function agentLabel(agent: { id: string; name: string }): string {
13
+ const source = BUILTIN_AGENT_LABELS[agent.id];
14
+ return source ? t(source) : agent.name;
15
+ }
@@ -0,0 +1,68 @@
1
+ import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from 'react';
2
+
3
+ import {
4
+ configureI18n,
5
+ getLocale,
6
+ registerLocale,
7
+ setLocale,
8
+ subscribeI18n,
9
+ supportedLocales,
10
+ t,
11
+ } from './runtime';
12
+
13
+ export { configureI18n, getLocale, registerLocale, setLocale, supportedLocales, t };
14
+ export { LOCALE_STORAGE_KEY, SOURCE_LOCALE, normalizeLocale, resolveLocale } from './translate';
15
+
16
+ interface I18nContextValue {
17
+ locale: string;
18
+ locales: readonly string[];
19
+ t: typeof t;
20
+ setLocale: (locale: string) => void;
21
+ }
22
+
23
+ const I18nContext = createContext<I18nContextValue | null>(null);
24
+
25
+ /**
26
+ * 包住应用树。语言变化时用 key 重挂子树,这样直接调用 `t()` 的组件也会换文案。
27
+ */
28
+ export function I18nProvider({ children }: { children: ReactNode }) {
29
+ const [locale, setSnapshot] = useState(getLocale);
30
+ useEffect(() => subscribeI18n(() => setSnapshot(getLocale())), []);
31
+ const value = useMemo<I18nContextValue>(
32
+ () => ({
33
+ locale,
34
+ locales: supportedLocales(),
35
+ t,
36
+ setLocale: (next) => {
37
+ if (!setLocale(next)) return;
38
+ window.steerableHost?.setLocale?.(next);
39
+ },
40
+ }),
41
+ [locale],
42
+ );
43
+ return (
44
+ <I18nContext.Provider value={value}>
45
+ <div key={locale} className="contents">
46
+ {children}
47
+ </div>
48
+ </I18nContext.Provider>
49
+ );
50
+ }
51
+
52
+ export function useI18n(): I18nContextValue {
53
+ const value = useContext(I18nContext);
54
+ if (value) return value;
55
+ return {
56
+ locale: getLocale(),
57
+ locales: supportedLocales(),
58
+ t,
59
+ setLocale,
60
+ };
61
+ }
62
+
63
+ /** 语言名用该语言自己的写法,不跟着界面语言翻。 */
64
+ export function localeEndonym(locale: string): string {
65
+ if (locale === 'zh') return '中文'; // i18n:allow 语言名用该语言自己的写法
66
+ if (locale === 'en') return 'English';
67
+ return locale;
68
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * 渲染进程的当前语言。产品入口在 bootstrap 前 registerLocale,
3
+ * bootstrap 再 configureI18n。未配置时只有英语。
4
+ */
5
+
6
+ import {
7
+ LOCALE_STORAGE_KEY,
8
+ SOURCE_LOCALE,
9
+ resolveLocale,
10
+ translate,
11
+ } from './translate';
12
+
13
+ export interface I18nStorage {
14
+ getItem(key: string): string | null;
15
+ setItem(key: string, value: string): void;
16
+ }
17
+
18
+ interface I18nState {
19
+ locales: string[];
20
+ defaultLocale: string;
21
+ locale: string;
22
+ catalogs: Record<string, Record<string, string>>;
23
+ }
24
+
25
+ const state: I18nState = {
26
+ locales: [SOURCE_LOCALE],
27
+ defaultLocale: SOURCE_LOCALE,
28
+ locale: SOURCE_LOCALE,
29
+ catalogs: {},
30
+ };
31
+
32
+ const listeners = new Set<() => void>();
33
+
34
+ function emit(): void {
35
+ for (const listener of listeners) listener();
36
+ }
37
+
38
+ export function subscribeI18n(listener: () => void): () => void {
39
+ listeners.add(listener);
40
+ return () => {
41
+ listeners.delete(listener);
42
+ };
43
+ }
44
+
45
+ export function registerLocale(locale: string, messages: Record<string, string>): void {
46
+ const normalized = locale.trim();
47
+ if (!normalized || normalized === SOURCE_LOCALE) return;
48
+ state.catalogs[normalized] = { ...state.catalogs[normalized], ...messages };
49
+ emit();
50
+ }
51
+
52
+ function readStorage(): I18nStorage | null {
53
+ if (typeof localStorage === 'undefined') return null;
54
+ return localStorage;
55
+ }
56
+
57
+ function systemLocale(): string | null {
58
+ if (typeof navigator === 'undefined') return null;
59
+ return navigator.language || null;
60
+ }
61
+
62
+ export function configureI18n(options: {
63
+ locales: readonly string[];
64
+ defaultLocale: string;
65
+ systemLocale?: string | null;
66
+ storage?: I18nStorage | null;
67
+ }): void {
68
+ const locales = [...new Set(options.locales.filter((locale) => locale.length > 0))];
69
+ state.locales = locales.length > 0 ? locales : [SOURCE_LOCALE];
70
+ state.defaultLocale = options.defaultLocale || SOURCE_LOCALE;
71
+ const storage = options.storage === undefined ? readStorage() : options.storage;
72
+ state.locale = resolveLocale({
73
+ supported: state.locales,
74
+ defaultLocale: state.defaultLocale,
75
+ saved: storage?.getItem(LOCALE_STORAGE_KEY) ?? null,
76
+ system: options.systemLocale === undefined ? systemLocale() : options.systemLocale,
77
+ });
78
+ applyDocumentLang(state.locale);
79
+ emit();
80
+ }
81
+
82
+ export function getLocale(): string {
83
+ return state.locale;
84
+ }
85
+
86
+ export function supportedLocales(): readonly string[] {
87
+ return state.locales;
88
+ }
89
+
90
+ export function setLocale(locale: string): boolean {
91
+ if (!state.locales.includes(locale)) return false;
92
+ if (state.locale === locale) return true;
93
+ state.locale = locale;
94
+ const storage = readStorage();
95
+ storage?.setItem(LOCALE_STORAGE_KEY, locale);
96
+ applyDocumentLang(locale);
97
+ emit();
98
+ return true;
99
+ }
100
+
101
+ export function t(message: string, params?: Record<string, string | number>): string {
102
+ return translate(message, state.locale, state.catalogs, params);
103
+ }
104
+
105
+ function applyDocumentLang(locale: string): void {
106
+ if (typeof document === 'undefined') return;
107
+ document.documentElement.lang = locale === 'zh' ? 'zh-CN' : locale;
108
+ }
109
+
110
+ /** 测试隔离。生产代码不调用。 */
111
+ export function resetI18nForTests(): void {
112
+ state.locales = [SOURCE_LOCALE];
113
+ state.defaultLocale = SOURCE_LOCALE;
114
+ state.locale = SOURCE_LOCALE;
115
+ state.catalogs = {};
116
+ listeners.clear();
117
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * 界面文案查找。源语言是英语:调用处写英文句子,应用层语言包把英文映射到译文。
3
+ * 缺译文时原样返回英文,漏翻在界面上看得见。
4
+ */
5
+
6
+ export const SOURCE_LOCALE = 'en';
7
+ export const LOCALE_STORAGE_KEY = 'agent-shell.locale';
8
+
9
+ /** `zh-CN` / `en-US` 收成语言包用的主标签。不认识的标签保留主段。 */
10
+ export function normalizeLocale(raw: string | null | undefined): string | null {
11
+ if (!raw) return null;
12
+ const lower = raw.trim().toLowerCase().replace(/_/g, '-');
13
+ if (!lower) return null;
14
+ if (lower.startsWith('zh')) return 'zh';
15
+ if (lower.startsWith('en')) return 'en';
16
+ const primary = lower.split('-')[0];
17
+ return primary || null;
18
+ }
19
+
20
+ export function resolveLocale(options: {
21
+ supported: readonly string[];
22
+ defaultLocale: string;
23
+ saved: string | null;
24
+ system: string | null;
25
+ }): string {
26
+ const supported = options.supported.filter((locale) => locale.length > 0);
27
+ const list = supported.length > 0 ? supported : [SOURCE_LOCALE];
28
+ const pick = (value: string | null): string | null => {
29
+ if (!value) return null;
30
+ if (list.includes(value)) return value;
31
+ const normalized = normalizeLocale(value);
32
+ if (normalized && list.includes(normalized)) return normalized;
33
+ return null;
34
+ };
35
+ return (
36
+ pick(options.saved) ??
37
+ pick(options.system) ??
38
+ pick(options.defaultLocale) ??
39
+ list[0] ??
40
+ SOURCE_LOCALE
41
+ );
42
+ }
43
+
44
+ export function formatMessage(
45
+ template: string,
46
+ params?: Record<string, string | number>,
47
+ ): string {
48
+ if (!params) return template;
49
+ return template.replace(/\{(\w+)\}/g, (whole, name: string) => {
50
+ const value = params[name];
51
+ return value === undefined ? whole : String(value);
52
+ });
53
+ }
54
+
55
+ /** 语言包的键是英文源句。源语言不查表。 */
56
+ export function translate(
57
+ message: string,
58
+ locale: string,
59
+ catalogs: Readonly<Record<string, Readonly<Record<string, string>>>>,
60
+ params?: Record<string, string | number>,
61
+ ): string {
62
+ const table = locale === SOURCE_LOCALE ? undefined : catalogs[locale];
63
+ const template = table?.[message] ?? message;
64
+ return formatMessage(template, params);
65
+ }
@@ -8,7 +8,7 @@ import { AskUserPromptProvider } from '@/components/chat/AskUserPromptProvider';
8
8
  import { ApprovalPromptProvider } from '@/components/chat/ApprovalPromptProvider';
9
9
  import { InsightsConsentBanner } from '@/components/settings/InsightsSettingsPanel';
10
10
  import { trackBehavior } from '@/lib/insights';
11
- import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
11
+ import { getHostBridge, hasHostBridge } from '@/lib/host-bridge';
12
12
  import { deleteChatIfEmpty, pruneEmptyChats } from '@/lib/local-api';
13
13
  import { PORTABLE_CHATS_CHANGED_EVENT } from '@/lib/portable';
14
14
  import { getPackChatSlots, type PackChatSlotContribution } from '@/packs/registry';
@@ -17,6 +17,7 @@ import {
17
17
  type UseChatsAndAgentsResult,
18
18
  } from '@/hooks/useChatsAndAgents';
19
19
  import { hostToolChrome, sanitizeRightPanelKind, settingsChrome } from '@/lib/host-tools';
20
+ import { t } from '@/i18n';
20
21
 
21
22
  /**
22
23
  * AgentLayout wraps `/agent`, `/agent/:chatId`, and `/` (default) with a
@@ -169,7 +170,7 @@ function AgentLayoutContent() {
169
170
  refreshChatsRef.current = data.refreshChats;
170
171
  }, [data.refreshChats]);
171
172
  useEffect(() => {
172
- const bridge = getElectronBridge();
173
+ const bridge = getHostBridge();
173
174
  if (!bridge?.onChatTitleUpdated) return;
174
175
  const unsubscribe = bridge.onChatTitleUpdated((payload) => {
175
176
  patchChatTitleRef.current(payload.chatId, payload.title);
@@ -180,7 +181,7 @@ function AgentLayoutContent() {
180
181
  // 现场补建(见 router.handleStream),随后广播 `chat-created`。这里刷一次
181
182
  // 侧栏把这条会话拉出来——否则会出现"URL 能聊、列表里却查无此会话"。
182
183
  useEffect(() => {
183
- const bridge = getElectronBridge();
184
+ const bridge = getHostBridge();
184
185
  if (!bridge?.onChatCreated) return;
185
186
  const unsubscribe = bridge.onChatCreated(() => {
186
187
  void refreshChatsRef.current();
@@ -198,7 +199,7 @@ function AgentLayoutContent() {
198
199
  // 空会话不进侧栏:启动时清掉从未发过消息的残骸;离开一段空对话时丢掉它。
199
200
  // except / 离开检测都避开当前打开的 chatId,避免和首页首条发送抢跑。
200
201
  useEffect(() => {
201
- if (!isElectron()) return;
202
+ if (!hasHostBridge()) return;
202
203
  let cancelled = false;
203
204
  void pruneEmptyChats(chatId)
204
205
  .then((res) => {
@@ -216,7 +217,7 @@ function AgentLayoutContent() {
216
217
  useEffect(() => {
217
218
  const prev = prevChatIdRef.current;
218
219
  prevChatIdRef.current = chatId;
219
- if (!isElectron() || !prev || prev === chatId) return;
220
+ if (!hasHostBridge() || !prev || prev === chatId) return;
220
221
  void deleteChatIfEmpty(prev)
221
222
  .then((res) => {
222
223
  if (res.deleted) return refreshChatsRef.current();
@@ -415,7 +416,7 @@ function AgentLayoutContent() {
415
416
  // so this flips the layout state owned by AgentLayout.
416
417
  useEffect(() => {
417
418
  if (!hostToolChrome('terminal')) return;
418
- const bridge = getElectronBridge();
419
+ const bridge = getHostBridge();
419
420
  if (!bridge?.onMenuOpenTerminal) return;
420
421
  bridge.onMenuOpenTerminal(() => {
421
422
  toggleRightPanel('terminal');
@@ -560,8 +561,8 @@ function AgentLayoutContent() {
560
561
  type="button"
561
562
  onClick={toggleSidebarCollapsed}
562
563
  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"
563
- title="展开侧边栏"
564
- aria-label="展开侧边栏"
564
+ title={t('Expand sidebar')}
565
+ aria-label={t('Expand sidebar')}
565
566
  >
566
567
  <LuPanelLeftOpen className="h-3.5 w-3.5" />
567
568
  </button>
@@ -581,11 +582,11 @@ function AgentLayoutContent() {
581
582
  <div
582
583
  role="separator"
583
584
  aria-orientation="vertical"
584
- aria-label="调整侧边栏宽度"
585
+ aria-label={t('Resize sidebar')}
585
586
  onMouseDown={() => setIsResizing(true)}
586
587
  onDoubleClick={resetWidth}
587
588
  className="w-1 flex-shrink-0 cursor-col-resize bg-transparent transition-colors hover:bg-agent-border/60"
588
- title="拖拽调整侧边栏宽度(双击恢复默认)"
589
+ title={t('Drag to resize the sidebar (double-click to reset)')}
589
590
  />
590
591
  </>
591
592
  )}
@@ -612,11 +613,11 @@ function AgentLayoutContent() {
612
613
  <div
613
614
  role="separator"
614
615
  aria-orientation="vertical"
615
- aria-label="调整右侧面板宽度"
616
+ aria-label={t('Resize right panel')}
616
617
  onMouseDown={() => setIsTerminalResizing(true)}
617
618
  onDoubleClick={resetTerminalWidth}
618
619
  className="w-1 flex-shrink-0 cursor-col-resize bg-transparent transition-colors hover:bg-agent-border/60"
619
- title="拖拽调整右侧面板宽度(双击恢复默认)"
620
+ title={t('Drag to resize the right panel (double-click to reset)')}
620
621
  />
621
622
  <div
622
623
  className={`h-full flex-shrink-0 overflow-hidden rounded-agent-lg shadow-sm ${
@@ -1,4 +1,5 @@
1
- import type { AppReleasePhase, AppReleaseSnapshot } from './electron-bridge';
1
+ import { t } from '@/i18n';
2
+ import type { AppReleasePhase, AppReleaseSnapshot } from './host-bridge';
2
3
 
3
4
  export interface AppReleaseView {
4
5
  version: string | null;
@@ -20,21 +21,25 @@ export function sidebarUpdateLabel(
20
21
  if (!snap.enabled || snap.phase === 'disabled') return null;
21
22
  switch (snap.phase) {
22
23
  case 'checking':
23
- return '检查中';
24
+ return t('Checking');
24
25
  case 'downloading':
25
- return typeof snap.percent === 'number' ? `下载 ${Math.round(snap.percent)}%` : '下载中';
26
+ return typeof snap.percent === 'number'
27
+ ? t('Downloading {percent}%', { percent: Math.round(snap.percent) })
28
+ : t('Downloading…');
26
29
  case 'ready':
27
- return snap.availableVersion ? `升级 ${snap.availableVersion}` : '升级';
30
+ return snap.availableVersion
31
+ ? t('Update to {version}', { version: snap.availableVersion })
32
+ : t('Update');
28
33
  case 'installing':
29
- return '正在安装';
34
+ return t('Installing');
30
35
  case 'error':
31
- return '重试';
36
+ return t('Retry');
32
37
  case 'idle':
33
- if (snap.availableVersion) return `发现 ${snap.availableVersion}`;
38
+ if (snap.availableVersion) return t('{version} available', { version: snap.availableVersion });
34
39
  if (snap.message) return snap.message;
35
- return confirmedCurrent ? '已是最新' : '检查更新';
40
+ return confirmedCurrent ? t('Up to date') : t('Check for updates');
36
41
  default:
37
- return '检查更新';
42
+ return t('Check for updates');
38
43
  }
39
44
  }
40
45
 
@@ -43,9 +48,9 @@ export function sidebarUpdateTitle(snap: AppReleaseSnapshot): string | undefined
43
48
  return snap.message;
44
49
  }
45
50
  if (snap.phase === 'ready' && snap.availableVersion) {
46
- return `重启并安装 ${snap.availableVersion}`;
51
+ return t('Restart and install {version}', { version: snap.availableVersion });
47
52
  }
48
- if (snap.phase === 'downloading') return '正在下载更新';
53
+ if (snap.phase === 'downloading') return t('Downloading update');
49
54
  return undefined;
50
55
  }
51
56
 
@@ -9,7 +9,8 @@
9
9
  * - BS/browser:File 没有路径,这里把字节读成 base64 走 HTTP 端点落盘。
10
10
  * 返回的落盘路径会重写消息正文里的文件引用与图像附件元数据。
11
11
  */
12
- import { getElectronBridge } from './electron-bridge';
12
+ import { t } from '@/i18n';
13
+ import { getHostBridge } from './host-bridge';
13
14
 
14
15
  export interface AttachmentFile {
15
16
  name: string;
@@ -82,7 +83,7 @@ export async function saveChatAttachments(
82
83
  files: AttachmentFile[],
83
84
  ): Promise<SaveChatAttachmentsResult> {
84
85
  if (!chatId || files.length === 0) return { files, failures: [] };
85
- const bridge = getElectronBridge();
86
+ const bridge = getHostBridge();
86
87
  if (!bridge?.attachments?.save) return { files, failures: [] };
87
88
  try {
88
89
  // 有真实路径走 path 拷贝(Electron);没有则读字节走 data 上传(BS/browser)。
@@ -109,7 +110,7 @@ export async function saveChatAttachments(
109
110
  kept.push(file);
110
111
  return;
111
112
  }
112
- failures.push({ name: file.name, error: entry?.error || '上传失败', file });
113
+ failures.push({ name: file.name, error: entry?.error || t('Upload failed'), file });
113
114
  });
114
115
  return { files: kept, failures };
115
116
  } catch (err) {
@@ -123,13 +124,21 @@ export async function saveChatAttachments(
123
124
  }
124
125
  }
125
126
 
126
- /** 把失败项拼成一条给用户看的中文提示(供各提交入口复用)。 */
127
+ /** 把失败项拼成一条给用户看的提示(供各提交入口复用)。 */
127
128
  export function formatAttachmentFailures(failures: AttachmentSaveFailure[]): string {
128
129
  if (failures.length === 0) return '';
129
- const detail = failures.map((f) => `${f.name}(${f.error})`).join(';');
130
+ const detail = failures
131
+ .map((f) => t('{name} ({error})', { name: f.name, error: f.error }))
132
+ .join(t('; '));
130
133
  return failures.length === 1
131
- ? `文件「${failures[0].name}」未能上传,本条消息未包含它:${failures[0].error}`
132
- : `${failures.length} 个文件未能上传,本条消息未包含它们:${detail}`;
134
+ ? t('File "{name}" failed to upload and is not included in this message: {error}', {
135
+ name: failures[0].name,
136
+ error: failures[0].error,
137
+ })
138
+ : t('{count} files failed to upload and are not included in this message: {detail}', {
139
+ count: failures.length,
140
+ detail,
141
+ });
133
142
  }
134
143
 
135
144
  /**
@@ -142,8 +151,8 @@ export function appendAttachmentRefs(rawText: string, files: AttachmentFile[]):
142
151
  if (files.length === 0) return rawText;
143
152
  const refs = files.map((f) => `- \`${f.path}\``).join('\n');
144
153
  return rawText
145
- ? `${rawText}\n\n---\n关联文件:\n${refs}`
146
- : `关联文件:\n${refs}`;
154
+ ? `${rawText}\n\n---\n${t('Related files:')}\n${refs}`
155
+ : `${t('Related files:')}\n${refs}`;
147
156
  }
148
157
 
149
158
  /**