@steerable/agent-shell-web 0.6.53 → 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.
- package/package.json +4 -4
- package/src/brand.ts +1 -1
- package/src/components/AgentSidebar.tsx +60 -52
- package/src/components/ChatHeader.tsx +41 -25
- package/src/components/ConfirmDialog.tsx +4 -3
- package/src/components/CreateProjectModal.tsx +29 -19
- package/src/components/LocalLlmSettingsModal.tsx +4 -3
- package/src/components/SidebarRelease.tsx +2 -1
- package/src/components/TerminalPanel.tsx +3 -2
- package/src/components/TerminalView.tsx +12 -7
- package/src/components/chat/ApprovalModal.tsx +39 -25
- package/src/components/chat/ApprovalPromptProvider.tsx +1 -1
- package/src/components/chat/AskUserPromptProvider.tsx +1 -1
- package/src/components/chat/AskUserQuestionMenu.tsx +19 -14
- package/src/components/chat/AssistantMessage.tsx +18 -13
- package/src/components/chat/ChatInput.tsx +56 -40
- package/src/components/chat/ChatProjectBadge.tsx +24 -19
- package/src/components/chat/EmptyChatHero.tsx +2 -1
- package/src/components/chat/ExecPolicyPicker.tsx +12 -11
- package/src/components/chat/ExecutedActionsCard.tsx +45 -33
- package/src/components/chat/FilePathCode.tsx +8 -3
- package/src/components/chat/InterruptedTurnCard.tsx +4 -3
- package/src/components/chat/LocalChatPanel.tsx +2 -1
- package/src/components/chat/Markdown.tsx +2 -1
- package/src/components/chat/MessageList.tsx +3 -2
- package/src/components/chat/ModelPicker.tsx +25 -16
- package/src/components/chat/OrchestrationChildrenCard.tsx +2 -1
- package/src/components/chat/OrchestrationFlowCard.tsx +23 -22
- package/src/components/chat/PlanTodoList.tsx +2 -1
- package/src/components/chat/ProjectTrustBanner.tsx +19 -5
- package/src/components/chat/SessionOrchestrationFlow.tsx +27 -26
- package/src/components/chat/SessionTodoList.tsx +8 -7
- package/src/components/chat/SessionTreeModal.tsx +10 -9
- package/src/components/chat/StreamingStatus.tsx +4 -3
- package/src/components/chat/SuggestedReplies.tsx +3 -2
- package/src/components/chat/TaskOutcomeCards.tsx +8 -5
- package/src/components/chat/TaskPanelModal.tsx +31 -21
- package/src/components/chat/TaskProcessPanel.tsx +14 -13
- package/src/components/chat/TurnFilesCard.tsx +14 -13
- package/src/components/chat/UserMessage.tsx +12 -11
- package/src/components/chat/executed-actions-model.ts +47 -35
- package/src/components/chat/orchestration-flow-model.ts +16 -5
- package/src/components/chat/process-status.ts +8 -7
- package/src/components/chat/todo-list-model.ts +5 -4
- package/src/components/chat/turn-files.ts +8 -8
- package/src/components/settings/AgentCapabilityFields.tsx +24 -21
- package/src/components/settings/AgentsSettingsPanel.tsx +30 -25
- package/src/components/settings/AppUpdateSettingsPanel.tsx +3 -2
- package/src/components/settings/AppearanceSettingsPanel.tsx +81 -37
- package/src/components/settings/DiagnoseSettingsPanel.tsx +11 -8
- package/src/components/settings/InsightsSettingsPanel.tsx +43 -28
- package/src/components/settings/LlmSettingsPanel.tsx +104 -83
- package/src/components/settings/McpSettingsPanel.tsx +60 -34
- package/src/components/settings/ModelIdCombobox.tsx +2 -1
- package/src/components/settings/OrchestrationSettingsPanel.tsx +8 -5
- package/src/components/settings/PortableSettingsPanel.tsx +83 -55
- package/src/components/settings/PythonRunnerSettingsPanel.tsx +22 -19
- package/src/components/settings/SecuritySettingsPanel.tsx +55 -31
- package/src/components/settings/SettingsNavMenu.tsx +3 -2
- package/src/components/settings/SettingsSaveButton.tsx +2 -1
- package/src/components/settings/SkillsSettingsPanel.tsx +36 -31
- package/src/components/settings/TelemetrySettingsPanel.tsx +15 -12
- package/src/components/settings/UsagePanel.tsx +19 -20
- package/src/components/settings/WebSearchSettingsPanel.tsx +23 -14
- package/src/components/settings/llm-vendors.ts +12 -7
- package/src/components/settings/model-capabilities.ts +16 -15
- package/src/components/settings/preset-choice-model.ts +6 -3
- package/src/fixtures/browser-dev-data.json +24 -24
- package/src/hooks/useProjects.ts +1 -1
- package/src/i18n/agent-label.ts +15 -0
- package/src/i18n/index.tsx +68 -0
- package/src/i18n/runtime.ts +117 -0
- package/src/i18n/translate.ts +65 -0
- package/src/layouts/AgentLayout.tsx +7 -6
- package/src/lib/app-release.ts +15 -10
- package/src/lib/attachments.ts +16 -7
- package/src/lib/browser-dev-electron-mock.ts +34 -34
- package/src/lib/chat-title.ts +3 -2
- package/src/lib/chat-transport.ts +6 -2
- package/src/lib/date-groups.ts +23 -15
- package/src/lib/electron-bridge.ts +4 -5
- package/src/lib/http-bridge.ts +4 -1
- package/src/lib/portable.ts +8 -7
- package/src/lib/show-thinking-content.ts +15 -3
- package/src/main.tsx +25 -2
- package/src/pages/AgentPage.tsx +17 -12
- package/src/pages/ChatPanelPreviewPage.tsx +46 -44
- package/src/pages/SettingsPage.tsx +48 -42
- package/src/vite-env.d.ts +3 -0
- package/vite.base.js +6 -2
package/src/lib/app-release.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t } from '@/i18n';
|
|
1
2
|
import type { AppReleasePhase, AppReleaseSnapshot } from './electron-bridge';
|
|
2
3
|
|
|
3
4
|
export interface AppReleaseView {
|
|
@@ -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'
|
|
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
|
|
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
|
|
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
|
|
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
|
|
package/src/lib/attachments.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* - BS/browser:File 没有路径,这里把字节读成 base64 走 HTTP 端点落盘。
|
|
10
10
|
* 返回的落盘路径会重写消息正文里的文件引用与图像附件元数据。
|
|
11
11
|
*/
|
|
12
|
+
import { t } from '@/i18n';
|
|
12
13
|
import { getElectronBridge } from './electron-bridge';
|
|
13
14
|
|
|
14
15
|
export interface AttachmentFile {
|
|
@@ -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 || '
|
|
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
|
|
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
|
-
?
|
|
132
|
-
|
|
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
|
|
146
|
-
:
|
|
154
|
+
? `${rawText}\n\n---\n${t('Related files:')}\n${refs}`
|
|
155
|
+
: `${t('Related files:')}\n${refs}`;
|
|
147
156
|
}
|
|
148
157
|
|
|
149
158
|
/**
|
|
@@ -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: '
|
|
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: '
|
|
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
|
-
'
|
|
158
|
+
'You can debug the chat UI in a normal browser here. This reply comes from the dev mock bridge and does not use Electron IPC.',
|
|
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: '
|
|
205
|
-
{ name: 'local-exec', description: '
|
|
206
|
-
{ name: 'anti-deferred', description: '
|
|
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: '
|
|
239
|
-
{ name: 'local-exec', description: '
|
|
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 || '
|
|
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: '
|
|
396
|
-
{ name: 'local_read_file', description: '
|
|
397
|
-
{ name: 'local_write_file', description: '
|
|
398
|
-
{ name: 'web_search', description: '
|
|
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: '
|
|
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() || '
|
|
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
|
-
|
|
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 = '
|
|
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
|
}
|
|
@@ -625,7 +625,7 @@ export function installBrowserDevElectronMock() {
|
|
|
625
625
|
// 浏览器 dev 模式没有 Electron capturePage/clipboard,如实返回失败。
|
|
626
626
|
captureScreenshot: async () => ({
|
|
627
627
|
success: false as const,
|
|
628
|
-
error: '
|
|
628
|
+
error: 'Browser dev mode cannot capture a window screenshot (it needs the Electron runtime).',
|
|
629
629
|
}),
|
|
630
630
|
},
|
|
631
631
|
localBackend: {
|
package/src/lib/chat-title.ts
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* automation.py:_AUTOMATION_TITLE_PREFIX), so the same parser works for both.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
const
|
|
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;
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
} from '@steerable/agent-ui/state';
|
|
54
54
|
import type { SSEEvent } from '@steerable/agent-protocol';
|
|
55
55
|
import { getElectronBridge } from './electron-bridge';
|
|
56
|
+
import { t } from '@/i18n';
|
|
56
57
|
import {
|
|
57
58
|
appendDelta,
|
|
58
59
|
freezeReasoningDurations,
|
|
@@ -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(
|
|
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));
|
package/src/lib/date-groups.ts
CHANGED
|
@@ -1,34 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Date-group helpers used by AgentSidebar to label chat groups
|
|
3
|
-
* ("
|
|
4
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
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
|
|
30
|
+
return t('{month}/{day}/{year}', { year: target.getFullYear(), month, day });
|
|
24
31
|
}
|
|
25
|
-
return
|
|
32
|
+
return t('{month}/{day}', { month, day });
|
|
26
33
|
}
|
|
27
34
|
|
|
28
|
-
export function getDateGroupPriority(
|
|
29
|
-
|
|
30
|
-
if (
|
|
31
|
-
if (
|
|
32
|
-
if (
|
|
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
|
}
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Host-neutral bridge used by the renderer in desktop and browser modes.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* file across workspaces — it would drag the whole main-process source tree
|
|
7
|
-
* into the renderer's compile graph. Treat this as a contract surface and
|
|
8
|
-
* extend it lazily as apps/web starts using new bridge methods.
|
|
4
|
+
* Implementations are `tauri-bridge.ts` (desktop) and `http-bridge.ts`
|
|
5
|
+
* (browser server); both must satisfy {@link HostBridge}.
|
|
9
6
|
*
|
|
10
7
|
* `window.electron` and `getElectronBridge()` remain compatibility aliases for
|
|
11
8
|
* existing products. New integrations should use `window.steerableHost` and
|
|
@@ -184,6 +181,8 @@ export interface HostBridge {
|
|
|
184
181
|
* to clear the listener (otherwise multiple AgentSidebar mounts
|
|
185
182
|
* during HMR would queue up duplicate callbacks).
|
|
186
183
|
*/
|
|
184
|
+
/** 把当前界面语言告诉主进程,以便重建应用菜单。浏览器预览里没有。 */
|
|
185
|
+
setLocale?: (locale: string) => void;
|
|
187
186
|
onMenuNewChat?: (callback: () => void) => void;
|
|
188
187
|
offMenuNewChat?: () => void;
|
|
189
188
|
/**
|
package/src/lib/http-bridge.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
TerminalSession,
|
|
24
24
|
TerminalSpawnOptions,
|
|
25
25
|
} from './electron-bridge';
|
|
26
|
+
import { t } from '@/i18n';
|
|
26
27
|
|
|
27
28
|
interface BsBootstrap {
|
|
28
29
|
platform: NodeJS.Platform;
|
|
@@ -225,7 +226,9 @@ export function createHttpBridge(): ElectronBridge {
|
|
|
225
226
|
},
|
|
226
227
|
captureScreenshot: async () => ({
|
|
227
228
|
success: false as const,
|
|
228
|
-
error:
|
|
229
|
+
error: t(
|
|
230
|
+
'Browser mode cannot capture window screenshots. Use your system screenshot tool.',
|
|
231
|
+
),
|
|
229
232
|
}),
|
|
230
233
|
},
|
|
231
234
|
|
package/src/lib/portable.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* 界面档位和命令权限存在 localStorage,由这里并进配置包;其余段走本地后端。
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { t } from '@/i18n';
|
|
6
7
|
import { getElectronBridge } from '@/lib/electron-bridge';
|
|
7
8
|
import { listChats } from '@/lib/local-api';
|
|
8
9
|
import { persistExecPolicy, readStoredExecPolicy, type ExecPolicy } from '@/lib/exec-policy';
|
|
@@ -69,9 +70,9 @@ export interface PortableChatImportResult {
|
|
|
69
70
|
const CLIENT_SECTION_IDS = ['appearance', 'execPolicy'] as const;
|
|
70
71
|
|
|
71
72
|
export function portableErrorMessage(err: unknown): string {
|
|
72
|
-
if (!(err instanceof Error)) return '
|
|
73
|
+
if (!(err instanceof Error)) return t('Operation failed');
|
|
73
74
|
const message = err.message.trim();
|
|
74
|
-
if (!message) return '
|
|
75
|
+
if (!message) return t('Operation failed');
|
|
75
76
|
try {
|
|
76
77
|
const parsed = JSON.parse(message) as { detail?: unknown; error?: unknown };
|
|
77
78
|
if (typeof parsed.detail === 'string' && parsed.detail.trim()) return parsed.detail;
|
|
@@ -86,13 +87,13 @@ export function parsePortableText(text: string): { ok: true; value: unknown } |
|
|
|
86
87
|
try {
|
|
87
88
|
return { ok: true, value: JSON.parse(text) as unknown };
|
|
88
89
|
} catch {
|
|
89
|
-
return { ok: false, error: '
|
|
90
|
+
return { ok: false, error: t('The file is not JSON') };
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
export function safeDownloadName(stem: string, suffix: string): string {
|
|
94
95
|
const cleaned = stem.replace(/[\\/:*?"<>|]/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 40);
|
|
95
|
-
return `${cleaned || '
|
|
96
|
+
return `${cleaned || t('Export')}-${suffix}.json`;
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
export function readLocalSections(): { appearance: { thinkingDisplay: ThinkingDisplayMode }; execPolicy: { policy: ExecPolicy } } {
|
|
@@ -139,7 +140,7 @@ export function isClientSection(id: string): boolean {
|
|
|
139
140
|
|
|
140
141
|
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
141
142
|
const bridge = getElectronBridge();
|
|
142
|
-
if (!bridge) throw new Error('
|
|
143
|
+
if (!bridge) throw new Error(t('No local backend in this environment'));
|
|
143
144
|
return bridge.localBackend.request<T>(body === undefined ? { method, path } : { method, path, body });
|
|
144
145
|
}
|
|
145
146
|
|
|
@@ -159,7 +160,7 @@ export async function listPortableChats(): Promise<Array<{ id: string; title: st
|
|
|
159
160
|
for (;;) {
|
|
160
161
|
const res = await listChats({ page, limit: 100 });
|
|
161
162
|
for (const chat of res.chats) {
|
|
162
|
-
rows.push({ id: chat.id, title: chat.title.trim() || '
|
|
163
|
+
rows.push({ id: chat.id, title: chat.title.trim() || t('Untitled chat') });
|
|
163
164
|
}
|
|
164
165
|
if (!res.pagination.hasMore || page >= 50) break;
|
|
165
166
|
page += 1;
|
|
@@ -190,7 +191,7 @@ export async function saveJsonFile(filename: string, data: unknown): Promise<str
|
|
|
190
191
|
const content = `${JSON.stringify(data, null, 2)}\n`;
|
|
191
192
|
const save = getElectronBridge()?.local?.saveTextFile;
|
|
192
193
|
if (save) {
|
|
193
|
-
const saved = await save({ title: '
|
|
194
|
+
const saved = await save({ title: t('Export'), defaultPath: filename, content });
|
|
194
195
|
if (saved.canceled || !saved.filePath) return null;
|
|
195
196
|
return saved.filePath;
|
|
196
197
|
}
|
|
@@ -19,9 +19,21 @@ export const THINKING_DISPLAY_OPTIONS: ReadonlyArray<{
|
|
|
19
19
|
label: string;
|
|
20
20
|
hint: string;
|
|
21
21
|
}> = [
|
|
22
|
-
{
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
{
|
|
23
|
+
mode: 'hidden',
|
|
24
|
+
label: 'Hide',
|
|
25
|
+
hint: 'Hide the thinking text and keep only the work status line.',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
mode: 'peek',
|
|
29
|
+
label: 'Show 5 lines',
|
|
30
|
+
hint: 'Show at most 5 lines of the current turn\'s thinking. Do not stretch the row when there is less, and collapse it when the turn ends. Use "Show all" for the full text.',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
mode: 'full',
|
|
34
|
+
label: 'Show all',
|
|
35
|
+
hint: 'Expand the current turn\'s thinking, then collapse it when the turn ends. The work row still collapses when it finishes.',
|
|
36
|
+
},
|
|
25
37
|
];
|
|
26
38
|
|
|
27
39
|
export function isThinkingDisplayMode(value: unknown): value is ThinkingDisplayMode {
|
package/src/main.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { StrictMode, lazy, Suspense, type ReactNode } from 'react';
|
|
|
2
2
|
import { createRoot, type Root } from 'react-dom/client';
|
|
3
3
|
import { createHashRouter, RouterProvider } from 'react-router-dom';
|
|
4
4
|
import { AppShell } from './AppShell';
|
|
5
|
+
import { I18nProvider, configureI18n, getLocale } from './i18n';
|
|
5
6
|
import { AgentLayout } from './layouts/AgentLayout';
|
|
6
7
|
import { AgentPage } from './pages/AgentPage';
|
|
7
8
|
import { SettingsPage } from './pages/SettingsPage';
|
|
@@ -82,7 +83,9 @@ function renderApplication(root: Root): void {
|
|
|
82
83
|
|
|
83
84
|
root.render(
|
|
84
85
|
<StrictMode>
|
|
85
|
-
<
|
|
86
|
+
<I18nProvider>
|
|
87
|
+
<RouterProvider router={router} />
|
|
88
|
+
</I18nProvider>
|
|
86
89
|
</StrictMode>,
|
|
87
90
|
);
|
|
88
91
|
}
|
|
@@ -97,6 +100,12 @@ export async function bootstrap(): Promise<void> {
|
|
|
97
100
|
|
|
98
101
|
// 标题与 favicon 在 bootstrap 时设置——此刻产品入口已完成包注册
|
|
99
102
|
// (品牌 logo 由包经 setBrandLogoUrl 注入,见 brand.ts)。
|
|
103
|
+
configureI18n({
|
|
104
|
+
locales: parseLocales(import.meta.env.VITE_LOCALES),
|
|
105
|
+
defaultLocale: import.meta.env.VITE_DEFAULT_LOCALE || 'en',
|
|
106
|
+
});
|
|
107
|
+
document.documentElement.lang = getLocale() === 'zh' ? 'zh-CN' : getLocale();
|
|
108
|
+
window.electron?.setLocale?.(getLocale());
|
|
100
109
|
document.title = BRAND_NAME;
|
|
101
110
|
document.querySelector<HTMLLinkElement>('link[rel="icon"]')?.setAttribute('href', getBrandLogoUrl());
|
|
102
111
|
installHostPaste();
|
|
@@ -117,7 +126,9 @@ export async function bootstrap(): Promise<void> {
|
|
|
117
126
|
const Gate = gate.Component;
|
|
118
127
|
root.render(
|
|
119
128
|
<StrictMode>
|
|
120
|
-
<
|
|
129
|
+
<I18nProvider>
|
|
130
|
+
<Gate onAuthenticated={onAuthenticated} />
|
|
131
|
+
</I18nProvider>
|
|
121
132
|
</StrictMode>,
|
|
122
133
|
);
|
|
123
134
|
return;
|
|
@@ -125,3 +136,15 @@ export async function bootstrap(): Promise<void> {
|
|
|
125
136
|
|
|
126
137
|
renderApplication(root);
|
|
127
138
|
}
|
|
139
|
+
|
|
140
|
+
function parseLocales(raw: string | undefined): string[] {
|
|
141
|
+
if (!raw) return ['en'];
|
|
142
|
+
try {
|
|
143
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
144
|
+
if (!Array.isArray(parsed)) return ['en'];
|
|
145
|
+
const locales = parsed.filter((item): item is string => typeof item === 'string' && item.length > 0);
|
|
146
|
+
return locales.length > 0 ? locales : ['en'];
|
|
147
|
+
} catch {
|
|
148
|
+
return ['en'];
|
|
149
|
+
}
|
|
150
|
+
}
|