@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.
- 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 +138 -42
- 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 +193 -71
- 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 +24 -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 +19 -6
- package/src/lib/host-file-drop.ts +68 -0
- package/src/lib/host-paste.ts +38 -9
- 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/lib/tauri-bridge.ts +5 -0
- 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 {
|
|
@@ -19,6 +20,14 @@ export interface AttachmentFile {
|
|
|
19
20
|
file?: File;
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
/** 宿主拖放 / 粘贴只给出绝对路径时,用最后一段作为展示名。 */
|
|
24
|
+
export function attachmentFromPath(path: string): AttachmentFile {
|
|
25
|
+
const trimmed = path.trim();
|
|
26
|
+
const parts = trimmed.split(/[/\\]/).filter((part) => part.length > 0);
|
|
27
|
+
const name = parts[parts.length - 1] ?? trimmed;
|
|
28
|
+
return { name, path: trimmed };
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp']);
|
|
23
32
|
|
|
24
33
|
/** 扩展名是否为图像(renderer 侧预过滤,后端 image-attachment 会二次校验)。 */
|
|
@@ -101,7 +110,7 @@ export async function saveChatAttachments(
|
|
|
101
110
|
kept.push(file);
|
|
102
111
|
return;
|
|
103
112
|
}
|
|
104
|
-
failures.push({ name: file.name, error: entry?.error || '
|
|
113
|
+
failures.push({ name: file.name, error: entry?.error || t('Upload failed'), file });
|
|
105
114
|
});
|
|
106
115
|
return { files: kept, failures };
|
|
107
116
|
} catch (err) {
|
|
@@ -115,13 +124,21 @@ export async function saveChatAttachments(
|
|
|
115
124
|
}
|
|
116
125
|
}
|
|
117
126
|
|
|
118
|
-
/**
|
|
127
|
+
/** 把失败项拼成一条给用户看的提示(供各提交入口复用)。 */
|
|
119
128
|
export function formatAttachmentFailures(failures: AttachmentSaveFailure[]): string {
|
|
120
129
|
if (failures.length === 0) return '';
|
|
121
|
-
const detail = failures
|
|
130
|
+
const detail = failures
|
|
131
|
+
.map((f) => t('{name} ({error})', { name: f.name, error: f.error }))
|
|
132
|
+
.join(t('; '));
|
|
122
133
|
return failures.length === 1
|
|
123
|
-
?
|
|
124
|
-
|
|
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
|
+
});
|
|
125
142
|
}
|
|
126
143
|
|
|
127
144
|
/**
|
|
@@ -134,8 +151,8 @@ export function appendAttachmentRefs(rawText: string, files: AttachmentFile[]):
|
|
|
134
151
|
if (files.length === 0) return rawText;
|
|
135
152
|
const refs = files.map((f) => `- \`${f.path}\``).join('\n');
|
|
136
153
|
return rawText
|
|
137
|
-
? `${rawText}\n\n---\n
|
|
138
|
-
:
|
|
154
|
+
? `${rawText}\n\n---\n${t('Related files:')}\n${refs}`
|
|
155
|
+
: `${t('Related files:')}\n${refs}`;
|
|
139
156
|
}
|
|
140
157
|
|
|
141
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
|
/**
|
|
@@ -197,10 +196,24 @@ export interface HostBridge {
|
|
|
197
196
|
/**
|
|
198
197
|
* App menu IPC — `Cmd+V` / 编辑 → 粘贴 fires `menu:paste`.
|
|
199
198
|
* WKWebView does not expose that pasteboard to the page, so the renderer
|
|
200
|
-
* reads it through `
|
|
199
|
+
* reads it through `readClipboard`.
|
|
201
200
|
*/
|
|
202
201
|
onMenuPaste?: (callback: () => void) => void;
|
|
203
202
|
offMenuPaste?: () => void;
|
|
203
|
+
/**
|
|
204
|
+
* OS pasteboard. `files` are copied paths or a screenshot PNG.
|
|
205
|
+
* Text is empty when the pasteboard holds files, so the path is not
|
|
206
|
+
* inserted into the composer as well.
|
|
207
|
+
*/
|
|
208
|
+
readClipboard?: () => Promise<{
|
|
209
|
+
text: string;
|
|
210
|
+
files: Array<{
|
|
211
|
+
name: string;
|
|
212
|
+
path?: string;
|
|
213
|
+
dataBase64?: string;
|
|
214
|
+
mime?: string;
|
|
215
|
+
}>;
|
|
216
|
+
}>;
|
|
204
217
|
/** Plain text from the OS pasteboard. Empty when the clipboard has no text. */
|
|
205
218
|
readClipboardText?: () => Promise<string>;
|
|
206
219
|
/**
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tauri consumes OS file drags before they become HTML5 drop events.
|
|
3
|
+
* The webview reports paths through `onDragDropEvent` instead.
|
|
4
|
+
*
|
|
5
|
+
* macOS labels AppKit points as a physical position. Windows reports device
|
|
6
|
+
* pixels. Checking both against the composer rectangle accepts the drop on
|
|
7
|
+
* either scale.
|
|
8
|
+
*/
|
|
9
|
+
import { getCurrentWebview, type DragDropEvent } from '@tauri-apps/api/webview';
|
|
10
|
+
|
|
11
|
+
export interface HostFileDrop {
|
|
12
|
+
type: 'enter' | 'over' | 'drop' | 'leave';
|
|
13
|
+
paths: string[];
|
|
14
|
+
x?: number;
|
|
15
|
+
y?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function hostFileDropAvailable(): boolean {
|
|
19
|
+
return typeof window !== 'undefined' && window.__TAURI_INTERNALS__ != null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function dropPointHitsRect(
|
|
23
|
+
position: { x: number; y: number },
|
|
24
|
+
rect: { left: number; right: number; top: number; bottom: number },
|
|
25
|
+
devicePixelRatio: number,
|
|
26
|
+
): boolean {
|
|
27
|
+
const scale = devicePixelRatio > 0 ? devicePixelRatio : 1;
|
|
28
|
+
return (
|
|
29
|
+
contains(rect, position.x, position.y) ||
|
|
30
|
+
contains(rect, position.x / scale, position.y / scale)
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Subscribe while a composer is mounted. The returned function removes it. */
|
|
35
|
+
export function listenHostFileDrop(onEvent: (event: HostFileDrop) => void): () => void {
|
|
36
|
+
if (!hostFileDropAvailable()) return () => {};
|
|
37
|
+
let unlisten: (() => void) | undefined;
|
|
38
|
+
let cancelled = false;
|
|
39
|
+
const pending = getCurrentWebview().onDragDropEvent((event) => {
|
|
40
|
+
onEvent(hostFileDropFrom(event.payload));
|
|
41
|
+
});
|
|
42
|
+
void pending.then((stop) => {
|
|
43
|
+
if (cancelled) stop();
|
|
44
|
+
else unlisten = stop;
|
|
45
|
+
});
|
|
46
|
+
return () => {
|
|
47
|
+
cancelled = true;
|
|
48
|
+
unlisten?.();
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function contains(
|
|
53
|
+
rect: { left: number; right: number; top: number; bottom: number },
|
|
54
|
+
x: number,
|
|
55
|
+
y: number,
|
|
56
|
+
): boolean {
|
|
57
|
+
return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function hostFileDropFrom(payload: DragDropEvent): HostFileDrop {
|
|
61
|
+
if (payload.type === 'leave') return { type: 'leave', paths: [] };
|
|
62
|
+
return {
|
|
63
|
+
type: payload.type,
|
|
64
|
+
paths: payload.type === 'over' ? [] : payload.paths,
|
|
65
|
+
x: payload.position.x,
|
|
66
|
+
y: payload.position.y,
|
|
67
|
+
};
|
|
68
|
+
}
|
package/src/lib/host-paste.ts
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* WKWebView paste does not carry clipboard text into the page. The
|
|
3
|
-
* shortcut emits `menu:paste`; this module reads the OS pasteboard
|
|
4
|
-
* it into the focused field.
|
|
2
|
+
* WKWebView paste does not carry clipboard text or files into the page. The
|
|
3
|
+
* Edit menu shortcut emits `menu:paste`; this module reads the OS pasteboard
|
|
4
|
+
* and writes it into the focused field.
|
|
5
5
|
*/
|
|
6
|
+
|
|
7
|
+
export interface HostClipboardFile {
|
|
8
|
+
name: string;
|
|
9
|
+
path?: string;
|
|
10
|
+
dataBase64?: string;
|
|
11
|
+
mime?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface HostClipboard {
|
|
15
|
+
text: string;
|
|
16
|
+
files: HostClipboardFile[];
|
|
17
|
+
}
|
|
6
18
|
import { getHostBridge } from './electron-bridge';
|
|
7
19
|
|
|
8
20
|
let pending = false;
|
|
@@ -25,12 +37,18 @@ export function installHostPaste(): void {
|
|
|
25
37
|
|
|
26
38
|
/** Read the OS pasteboard and insert it into `target` or the focused field. */
|
|
27
39
|
export function requestHostPaste(target?: HTMLElement | null): void {
|
|
28
|
-
const
|
|
29
|
-
|
|
40
|
+
const bridge = getHostBridge();
|
|
41
|
+
const readClipboard = bridge?.readClipboard;
|
|
42
|
+
const readText = bridge?.readClipboardText;
|
|
43
|
+
if ((!readClipboard && !readText) || pending) return;
|
|
30
44
|
pending = true;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
45
|
+
const read = readClipboard
|
|
46
|
+
? readClipboard()
|
|
47
|
+
: readText!().then((text) => ({ text, files: [] as HostClipboardFile[] }));
|
|
48
|
+
void read
|
|
49
|
+
.then((clip) => {
|
|
50
|
+
if (clip.files.length > 0) insertHostFiles(clip.files, target ?? null);
|
|
51
|
+
if (clip.text) insertHostText(clip.text, target ?? null);
|
|
34
52
|
})
|
|
35
53
|
.catch((error: unknown) => {
|
|
36
54
|
console.warn('[host-paste] clipboard read failed', error);
|
|
@@ -41,7 +59,10 @@ export function requestHostPaste(target?: HTMLElement | null): void {
|
|
|
41
59
|
}
|
|
42
60
|
|
|
43
61
|
export function hostClipboardAvailable(): boolean {
|
|
44
|
-
|
|
62
|
+
const bridge = getHostBridge();
|
|
63
|
+
return (
|
|
64
|
+
typeof bridge?.readClipboard === 'function' || typeof bridge?.readClipboardText === 'function'
|
|
65
|
+
);
|
|
45
66
|
}
|
|
46
67
|
|
|
47
68
|
function isEditable(target: EventTarget | null): target is HTMLElement {
|
|
@@ -58,6 +79,14 @@ function focusedEditable(): HTMLElement | null {
|
|
|
58
79
|
return null;
|
|
59
80
|
}
|
|
60
81
|
|
|
82
|
+
function insertHostFiles(files: HostClipboardFile[], preferred: HTMLElement | null): void {
|
|
83
|
+
const el = preferred?.isConnected ? preferred : focusedEditable();
|
|
84
|
+
if (!el) return;
|
|
85
|
+
if (el.isContentEditable || el.getAttribute('contenteditable') === 'true') {
|
|
86
|
+
el.dispatchEvent(new CustomEvent('hostpastefiles', { detail: files, cancelable: true }));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
61
90
|
function insertHostText(text: string, preferred: HTMLElement | null): void {
|
|
62
91
|
const el = preferred?.isConnected ? preferred : focusedEditable();
|
|
63
92
|
if (!el) return;
|
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
|
|