@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,8 +1,8 @@
1
1
  import type {
2
- ElectronBridge,
2
+ HostBridge,
3
3
  LocalBackendRequestInput,
4
4
  LocalBackendStreamEvent,
5
- } from './electron-bridge';
5
+ } from './host-bridge';
6
6
  import type {
7
7
  ChatAgentWriteInput,
8
8
  LocalChat,
@@ -97,33 +97,33 @@ function createInitialState(): BrowserMockState {
97
97
  {
98
98
  id: 'coach',
99
99
  slug: 'coach',
100
- name: '行动教练',
100
+ name: 'Action coach',
101
101
  icon: null,
102
102
  color: '#2563eb',
103
- description: '把目标拆成可执行计划,适合调试标准聊天路径。',
104
- rolePrompt: '你是一个行动教练。',
103
+ description: 'Breaks a goal into an executable plan. Useful for debugging the standard chat path.',
104
+ rolePrompt: 'You are an action coach.',
105
105
  isBuiltin: true,
106
106
  sortOrder: 1,
107
107
  },
108
108
  {
109
109
  id: 'tool-demo',
110
110
  slug: 'tool-demo',
111
- name: '工具演示专家',
111
+ name: 'Tool demo',
112
112
  icon: null,
113
113
  color: '#7c3aed',
114
- description: '模拟本地工具调用和执行摘要,适合调试工具卡片。',
115
- rolePrompt: '你是工具演示智能体。',
114
+ description: 'Simulates local tool calls and execution summaries. Useful for debugging tool cards.',
115
+ rolePrompt: 'You are the tool-demo agent.',
116
116
  isBuiltin: true,
117
117
  sortOrder: 2,
118
118
  },
119
119
  {
120
120
  id: 'writer',
121
121
  slug: 'writer',
122
- name: '文案顾问',
122
+ name: 'Writing advisor',
123
123
  icon: null,
124
124
  color: '#16a34a',
125
- description: '偏长文本回复样例,适合调试 markdown 与滚动。',
126
- rolePrompt: '你是一个文案顾问。',
125
+ description: 'Longer text replies, for debugging markdown and scrolling.',
126
+ rolePrompt: 'You are a writing advisor.',
127
127
  isBuiltin: true,
128
128
  sortOrder: 3,
129
129
  },
@@ -132,14 +132,14 @@ function createInitialState(): BrowserMockState {
132
132
  const chats = [
133
133
  createChatRecord({
134
134
  id: 'chat-browser-demo',
135
- title: '浏览器 UI 调试样例',
135
+ title: 'Browser UI debug sample',
136
136
  agentId: 'coach',
137
137
  minutesAgo: 8,
138
138
  isPinned: true,
139
139
  }),
140
140
  createChatRecord({
141
141
  id: 'chat-tool-demo',
142
- title: '工具调用卡片预览',
142
+ title: 'Tool-call card preview',
143
143
  agentId: 'tool-demo',
144
144
  minutesAgo: 64,
145
145
  }),
@@ -155,14 +155,14 @@ function createInitialState(): BrowserMockState {
155
155
  chatId: 'chat-browser-demo',
156
156
  role: 'assistant',
157
157
  content:
158
- '可以在这里调试普通浏览器里的聊天 UI。这个回复来自 dev mock bridge,不依赖 Electron IPC。',
158
+ 'You can debug the chat UI in a normal browser here. This reply comes from the dev mock bridge and does not call a live model.',
159
159
  minutesAgo: 7,
160
160
  }),
161
161
  createMessage({
162
162
  id: 'msg-demo-user-1',
163
163
  chatId: 'chat-browser-demo',
164
164
  role: 'user',
165
- content: '我想在浏览器里调试聊天界面。',
165
+ content: 'I want to debug the chat UI in the browser.',
166
166
  minutesAgo: 8,
167
167
  }),
168
168
  ],
@@ -171,7 +171,7 @@ function createInitialState(): BrowserMockState {
171
171
  id: 'msg-tool-assistant-1',
172
172
  chatId: 'chat-tool-demo',
173
173
  role: 'assistant',
174
- content: '我已经模拟执行了一次本地命令,并把结果展示在工具卡片里。',
174
+ content: 'I simulated a local command and put the result in a tool card.',
175
175
  minutesAgo: 63,
176
176
  messageMetadata: JSON.stringify({
177
177
  executedActions: [
@@ -187,7 +187,7 @@ function createInitialState(): BrowserMockState {
187
187
  id: 'msg-tool-user-1',
188
188
  chatId: 'chat-tool-demo',
189
189
  role: 'user',
190
- content: '帮我看一下最近的工具调用记录。',
190
+ content: 'Show me the recent tool-call records.',
191
191
  minutesAgo: 64,
192
192
  }),
193
193
  ],
@@ -201,9 +201,9 @@ function createInitialState(): BrowserMockState {
201
201
  maxTotalTokens: 60_000,
202
202
  },
203
203
  skills: [
204
- { name: 'tool-demo', description: '模拟查询与回放工具。', builtin: true },
205
- { name: 'local-exec', description: '模拟本地命令执行。', builtin: true },
206
- { name: 'anti-deferred', description: '模拟防拖延执行策略。', builtin: true },
204
+ { name: 'tool-demo', description: 'Simulates query and replay tools.', builtin: true },
205
+ { name: 'local-exec', description: 'Simulates local command execution.', builtin: true },
206
+ { name: 'anti-deferred', description: 'Simulates the anti-deferral execution policy.', builtin: true },
207
207
  ],
208
208
  activeStreams: new Map(),
209
209
  source: 'fallback',
@@ -235,8 +235,8 @@ function createStateFromFixture(fixture: BrowserDevFixture): BrowserMockState |
235
235
  Array.isArray(fixture.skills) && fixture.skills.length > 0
236
236
  ? fixture.skills
237
237
  : [
238
- { name: 'tool-demo', description: '查询与回放工具。', builtin: true },
239
- { name: 'local-exec', description: '本地命令执行。', builtin: true },
238
+ { name: 'tool-demo', description: 'Query and replay tools.', builtin: true },
239
+ { name: 'local-exec', description: 'Local command execution.', builtin: true },
240
240
  ],
241
241
  activeStreams: new Map(),
242
242
  source: 'fixture',
@@ -318,7 +318,7 @@ async function request<T>(input: LocalBackendRequestInput): Promise<T> {
318
318
  const agent: LocalChatAgent = {
319
319
  id,
320
320
  slug: null,
321
- name: (body.name || '新助手').trim() || '新助手',
321
+ name: (body.name || 'New assistant').trim() || 'New assistant',
322
322
  icon: null,
323
323
  color: body.color ?? '#4f46e5',
324
324
  description: body.description ?? null,
@@ -392,10 +392,10 @@ async function request<T>(input: LocalBackendRequestInput): Promise<T> {
392
392
  if (method === 'GET' && cleanPath === '/api/v2/chat-agents/tools') {
393
393
  return {
394
394
  tools: [
395
- { name: 'local_exec_shell', description: '执行本地 shell 命令。', category: 'local' },
396
- { name: 'local_read_file', description: '读取本地文本文件。', category: 'local' },
397
- { name: 'local_write_file', description: '写入本地文件。', category: 'local' },
398
- { name: 'web_search', description: '联网搜索。', category: 'local' },
395
+ { name: 'local_exec_shell', description: 'Run a local shell command.', category: 'local' },
396
+ { name: 'local_read_file', description: 'Read a local text file.', category: 'local' },
397
+ { name: 'local_write_file', description: 'Write a local file.', category: 'local' },
398
+ { name: 'web_search', description: 'Search the web.', category: 'local' },
399
399
  ],
400
400
  } as T;
401
401
  }
@@ -404,7 +404,7 @@ async function request<T>(input: LocalBackendRequestInput): Promise<T> {
404
404
  const body = input.body as { path?: string } | undefined;
405
405
  const name = body?.path?.split(/[\\/]/).filter(Boolean).pop() || 'browser-skill';
406
406
  if (!state.skills.some((skill) => skill.name === name)) {
407
- state.skills.push({ name, description: '浏览器预览中导入的 mock skill。' });
407
+ state.skills.push({ name, description: 'Mock skill imported in the browser preview.' });
408
408
  }
409
409
  return { success: true, name } as T;
410
410
  }
@@ -444,7 +444,7 @@ async function request<T>(input: LocalBackendRequestInput): Promise<T> {
444
444
  const chatId = `chat-browser-${Date.now()}`;
445
445
  const chat = createChatRecord({
446
446
  id: chatId,
447
- title: '新会话',
447
+ title: 'New chat',
448
448
  agentId,
449
449
  minutesAgo: 0,
450
450
  });
@@ -515,7 +515,7 @@ async function startStream(
515
515
  }
516
516
 
517
517
  const body = input.body as { message?: string } | undefined;
518
- const message = body?.message?.trim() || '请演示一下浏览器 mock 回复。';
518
+ const message = body?.message?.trim() || 'Please demo a browser mock reply.';
519
519
  const streamId = `browser-stream-${Date.now()}`;
520
520
  state.activeStreams.set(streamId, []);
521
521
 
@@ -530,10 +530,10 @@ async function startStream(
530
530
  state.messagesByChatId[chatId] = [...(state.messagesByChatId[chatId] ?? []), userMessage];
531
531
 
532
532
  const reply =
533
- `这是浏览器 dev mock 的流式回复。你刚才输入的是:「${message}」。` +
534
- ' 这里可以调试消息气泡、滚动、停止按钮、工具执行卡片和后续状态。';
533
+ `This is a streaming reply from the browser dev mock. You wrote: "${message}".` +
534
+ ' Use it to debug message bubbles, scrolling, the stop button, tool cards, and later status.';
535
535
  const chunks = reply.match(/.{1,8}/g) ?? [reply];
536
- const reasoning = '先调用一次 mock 工具,再把回复按调用流程交错显示。';
536
+ const reasoning = 'Call one mock tool, then interleave the reply with that call.';
537
537
  const mockAction = {
538
538
  id: 'browser-mock-1',
539
539
  tool: 'browser_mock',
@@ -580,7 +580,7 @@ async function startStream(
580
580
  { type: 'text', content: reply },
581
581
  ],
582
582
  durationMs: 1_200,
583
- suggestedReplies: ['继续完善这份结果', '换一种呈现方式', '告诉我下一步怎么做'],
583
+ suggestedReplies: ['Keep improving this result', 'Try another presentation', 'Tell me the next step'],
584
584
  }),
585
585
  });
586
586
  assistantMessage.createdAt = new Date().toISOString();
@@ -593,7 +593,7 @@ async function startStream(
593
593
  pushSse(onEvent, '[DONE]');
594
594
  onEvent({ type: 'end', status: 200 });
595
595
  state.activeStreams.delete(streamId);
596
- const suggestions = ['继续完善这份结果', '换一种呈现方式', '告诉我下一步怎么做'];
596
+ const suggestions = ['Keep improving this result', 'Try another presentation', 'Tell me the next step'];
597
597
  for (const listener of suggestedReplyListeners) {
598
598
  listener({ chatId, messageId: assistantMessage.id, suggestions });
599
599
  }
@@ -608,13 +608,13 @@ function cancelStream(streamId: string) {
608
608
  state.activeStreams.delete(streamId);
609
609
  }
610
610
 
611
- export function installBrowserDevElectronMock() {
612
- if (typeof window === 'undefined' || window.electron) return;
611
+ export function installBrowserDevHostMock() {
612
+ if (typeof window === 'undefined' || window.steerableHost) return;
613
613
  // DEV 浏览器预览自动安装;官网静态 demo 构建(app-demo 入口)经 demo flag
614
614
  // 显式安装。普通 prod 构建两条路都不通,本模块根本不会进产物。
615
615
  if (!import.meta.env.DEV && !isDemoMode()) return;
616
616
 
617
- window.electron = {
617
+ window.steerableHost = {
618
618
  runtime: 'local',
619
619
  platform: 'win32',
620
620
  local: {
@@ -622,10 +622,10 @@ export function installBrowserDevElectronMock() {
622
622
  canceled: false,
623
623
  filePaths: ['C:\\browser-preview\\mock-skill'],
624
624
  }),
625
- // 浏览器 dev 模式没有 Electron capturePage/clipboard,如实返回失败。
625
+ // 浏览器 dev 模式没有桌面截图和剪贴板,如实返回失败。
626
626
  captureScreenshot: async () => ({
627
627
  success: false as const,
628
- error: '浏览器开发模式不支持窗口截图(需要 Electron 运行时)',
628
+ error: 'Browser dev mode cannot capture a window screenshot.',
629
629
  }),
630
630
  },
631
631
  localBackend: {
@@ -644,10 +644,10 @@ export function installBrowserDevElectronMock() {
644
644
  suggestedReplyListeners.delete(callback);
645
645
  };
646
646
  },
647
- } satisfies ElectronBridge;
647
+ } satisfies HostBridge;
648
648
 
649
649
  console.info(
650
- '[browser-dev-electron-mock] Installed mock window.electron bridge.',
650
+ '[browser-dev-host-mock] Installed mock window.steerableHost bridge.',
651
651
  state.source === 'fixture'
652
652
  ? { source: state.fixtureMeta?.source, exportedAt: state.fixtureMeta?.exportedAt }
653
653
  : { source: 'built-in fallback' },
@@ -5,8 +5,9 @@
5
5
  * automation.py:_AUTOMATION_TITLE_PREFIX), so the same parser works for both.
6
6
  */
7
7
 
8
- export const AUTOMATION_TITLE_PREFIX = '[自动化] ';
9
- const BARE_BRACKET_PREFIX = '[自动化]';
8
+ // 与后端 automation.py 的标题前缀是同一条持久化约定,不能按界面语言改写。
9
+ export const AUTOMATION_TITLE_PREFIX = '[自动化] '; // i18n:allow
10
+ const BARE_BRACKET_PREFIX = '[自动化]'; // i18n:allow
10
11
 
11
12
  export interface ParsedChatTitle {
12
13
  displayTitle: string;
@@ -7,7 +7,7 @@
7
7
  * user types in ChatPanel
8
8
  * -> useChatStream.sendUserMessage()
9
9
  * -> transport.stream(input, onEvent)
10
- * -> window.electron.localBackend.startStream({ POST /api/v2/chats/:id/run })
10
+ * -> window.steerableHost.localBackend.startStream({ POST /api/v2/chats/:id/run })
11
11
  * -> main process forwards each raw SSE chunk via webContents.send
12
12
  * -> preload re-emits as `{ type: 'data'; chunk: string }`
13
13
  * -> we parse the SSE frames with the framework's `SSEParser`, normalise
@@ -52,7 +52,8 @@ import {
52
52
  parseSSEData,
53
53
  } from '@steerable/agent-ui/state';
54
54
  import type { SSEEvent } from '@steerable/agent-protocol';
55
- import { getElectronBridge } from './electron-bridge';
55
+ import { getHostBridge } from './host-bridge';
56
+ import { t } from '@/i18n';
56
57
  import {
57
58
  appendDelta,
58
59
  freezeReasoningDurations,
@@ -213,7 +214,7 @@ class LocalBackendSseAdapter {
213
214
  // starts a fresh SSE pipe against /api/v2/chats/:id/run.
214
215
  // ---------------------------------------------------------------------------
215
216
 
216
- export interface ElectronChatTransport extends ChatStreamTransport {
217
+ export interface HostChatTransport extends ChatStreamTransport {
217
218
  /**
218
219
  * 立即取消当前进行中的流(如果有)。
219
220
  *
@@ -225,7 +226,7 @@ export interface ElectronChatTransport extends ChatStreamTransport {
225
226
  cancelActive: () => void;
226
227
  }
227
228
 
228
- export function createElectronChatTransport(chatId: string): ElectronChatTransport {
229
+ export function createHostChatTransport(chatId: string): HostChatTransport {
229
230
  // 当前 in-flight 流的 id。startStream resolve 后写入,流结束(end/error)
230
231
  // 或被取消后清空。同一 transport 一次只会有一条流(useChatStream 在
231
232
  // isStreaming 时拒绝重入)。
@@ -233,7 +234,7 @@ export function createElectronChatTransport(chatId: string): ElectronChatTranspo
233
234
 
234
235
  return {
235
236
  cancelActive: () => {
236
- const bridge = getElectronBridge();
237
+ const bridge = getHostBridge();
237
238
  if (active.streamId) {
238
239
  bridge?.localBackend.cancelStream(active.streamId);
239
240
  active.streamId = null;
@@ -254,15 +255,15 @@ export function createElectronChatTransport(chatId: string): ElectronChatTranspo
254
255
  // 轮中转向:仅在 CoreLoop 路径(STEERABLE_USE_CORELOOP=1)且回合仍
255
256
  // 运行时生效;否则主进程软失败,useChatStream 保留草稿。
256
257
  steer: async (content: string) => {
257
- const bridge = getElectronBridge();
258
+ const bridge = getHostBridge();
258
259
  if (!bridge?.localBackend.steerChat) return false;
259
260
  return await bridge.localBackend.steerChat(chatId, content);
260
261
  },
261
262
  stream: async (input: ChatStreamSendInput, onEvent) => {
262
- const bridge = getElectronBridge();
263
+ const bridge = getHostBridge();
263
264
  if (!bridge) {
264
265
  throw new Error(
265
- 'Electron bridge unavailable — chat transport requires the desktop shell.',
266
+ 'Host bridge unavailable — chat transport requires the desktop shell.',
266
267
  );
267
268
  }
268
269
 
@@ -347,10 +348,10 @@ export async function regenerateChatMessage(
347
348
  chatId: string,
348
349
  messageId: string,
349
350
  ): Promise<void> {
350
- const bridge = getElectronBridge();
351
+ const bridge = getHostBridge();
351
352
  if (!bridge) {
352
353
  throw new Error(
353
- 'Electron bridge unavailable — regenerate requires the desktop shell.',
354
+ 'Host bridge unavailable — regenerate requires the desktop shell.',
354
355
  );
355
356
  }
356
357
  await new Promise<void>((resolve, reject) => {
@@ -372,7 +373,7 @@ export async function regenerateChatMessage(
372
373
  if (frame.event !== 'error' || !frame.data) return;
373
374
  const parsed = parseSSEData(frame.data);
374
375
  const message = (parsed as { message?: unknown } | null)?.message;
375
- refusal = typeof message === 'string' && message ? message : '重新生成失败';
376
+ refusal = typeof message === 'string' && message ? message : t('Regeneration failed');
376
377
  },
377
378
  });
378
379
  bridge.localBackend
@@ -388,7 +389,10 @@ export async function regenerateChatMessage(
388
389
  parser.end();
389
390
  settle(
390
391
  payload.status >= 400
391
- ? new Error(refusal ?? `重新生成失败(HTTP ${payload.status})`)
392
+ ? new Error(
393
+ refusal ??
394
+ t('Regeneration failed (HTTP {status})', { status: payload.status }),
395
+ )
392
396
  : undefined,
393
397
  );
394
398
  } else if (payload.type === 'error') settle(new Error(payload.error));
@@ -1,34 +1,42 @@
1
1
  /**
2
2
  * Date-group helpers used by AgentSidebar to label chat groups
3
- * ("今天" / "昨天" / "N天前" / "M月D日" / "YYYY年M月D日"). Lifted verbatim
4
- * from `deeppath/apps/web/src/app/agent/AgentSidebar.tsx` so the visual
3
+ * ("Today" / "Yesterday" / "N days ago" / "M/D" / "M/D/YYYY"). Lifted from
4
+ * `deeppath/apps/web/src/app/agent/AgentSidebar.tsx` so the visual
5
5
  * grouping stays in lockstep.
6
6
  *
7
7
  * priority is used for ordering groups: smaller = newer = listed first.
8
+ * 标签走 t(),排序只看日期,不解析标签文本。
8
9
  */
10
+ import { t } from '@/i18n';
9
11
 
10
- export function getDateGroupLabel(date: Date): string {
12
+ function dayBuckets(date: Date): { diffDays: number; target: Date; today: Date } {
11
13
  const today = new Date();
12
14
  today.setHours(0, 0, 0, 0);
13
15
  const target = new Date(date);
14
16
  target.setHours(0, 0, 0, 0);
15
17
  const diffDays =
16
18
  (today.getTime() - target.getTime()) / (1000 * 60 * 60 * 24);
17
- if (diffDays === 0) return '今天';
18
- if (diffDays === 1) return '昨天';
19
- if (diffDays < 4) return `${Math.floor(diffDays)}天前`;
20
- const m = target.getMonth() + 1;
21
- const d = target.getDate();
19
+ return { diffDays, target, today };
20
+ }
21
+
22
+ export function getDateGroupLabel(date: Date): string {
23
+ const { diffDays, target, today } = dayBuckets(date);
24
+ if (diffDays === 0) return t('Today');
25
+ if (diffDays === 1) return t('Yesterday');
26
+ if (diffDays < 4) return t('{count} days ago', { count: Math.floor(diffDays) });
27
+ const month = target.getMonth() + 1;
28
+ const day = target.getDate();
22
29
  if (target.getFullYear() !== today.getFullYear()) {
23
- return `${target.getFullYear()}年${m}月${d}日`;
30
+ return t('{month}/{day}/{year}', { year: target.getFullYear(), month, day });
24
31
  }
25
- return `${m}月${d}日`;
32
+ return t('{month}/{day}', { month, day });
26
33
  }
27
34
 
28
- export function getDateGroupPriority(label: string): number {
29
- if (label === '今天') return 1;
30
- if (label === '昨天') return 2;
31
- if (label.endsWith('天前')) return 2 + parseInt(label, 10);
32
- if (label.includes('年')) return 1000;
35
+ export function getDateGroupPriority(date: Date): number {
36
+ const { diffDays, target, today } = dayBuckets(date);
37
+ if (diffDays === 0) return 1;
38
+ if (diffDays === 1) return 2;
39
+ if (diffDays < 4) return 2 + Math.floor(diffDays);
40
+ if (target.getFullYear() !== today.getFullYear()) return 1000;
33
41
  return 100;
34
42
  }