@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.
- package/README.md +3 -4
- package/package.json +4 -4
- package/src/AppShell.tsx +3 -3
- package/src/brand.ts +1 -1
- package/src/components/AgentSidebar.tsx +64 -56
- package/src/components/ChatHeader.tsx +41 -25
- package/src/components/ConfirmDialog.tsx +4 -3
- package/src/components/CreateProjectModal.tsx +31 -21
- package/src/components/LocalLlmSettingsModal.tsx +4 -3
- package/src/components/SidebarRelease.tsx +6 -5
- package/src/components/TerminalPanel.tsx +6 -5
- package/src/components/TerminalView.tsx +15 -10
- package/src/components/chat/ApprovalModal.tsx +40 -26
- package/src/components/chat/ApprovalPromptProvider.tsx +5 -5
- package/src/components/chat/AskUserPromptProvider.tsx +4 -4
- 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 +27 -22
- 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 +33 -23
- package/src/components/chat/TaskProcessPanel.tsx +16 -15
- 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/chat/useChatTasks.ts +3 -3
- package/src/components/settings/AgentCapabilityFields.tsx +26 -23
- package/src/components/settings/AgentsSettingsPanel.tsx +33 -28
- 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 +58 -43
- package/src/components/settings/LlmSettingsPanel.tsx +107 -86
- package/src/components/settings/McpSettingsPanel.tsx +72 -46
- 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 +26 -23
- package/src/components/settings/SecuritySettingsPanel.tsx +58 -34
- package/src/components/settings/SettingsNavMenu.tsx +3 -2
- package/src/components/settings/SettingsSaveButton.tsx +2 -1
- package/src/components/settings/SkillsSettingsPanel.tsx +43 -38
- package/src/components/settings/TelemetrySettingsPanel.tsx +20 -17
- package/src/components/settings/UsagePanel.tsx +22 -23
- package/src/components/settings/WebSearchSettingsPanel.tsx +28 -19
- 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/useChatsAndAgents.ts +9 -9
- package/src/hooks/useProjects.ts +3 -3
- 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 +13 -12
- package/src/lib/app-release.ts +16 -11
- package/src/lib/attachments.ts +18 -9
- package/src/lib/{browser-dev-electron-mock.ts → browser-dev-host-mock.ts} +42 -42
- package/src/lib/chat-title.ts +3 -2
- package/src/lib/chat-transport.ts +16 -12
- package/src/lib/date-groups.ts +23 -15
- package/src/lib/host-bridge.ts +371 -4
- package/src/lib/host-paste.ts +1 -1
- package/src/lib/http-bridge.ts +12 -9
- package/src/lib/insights.ts +2 -2
- package/src/lib/local-api.ts +5 -5
- package/src/lib/portable.ts +11 -10
- package/src/lib/show-thinking-content.ts +15 -3
- package/src/lib/slash-sources.ts +3 -3
- package/src/lib/tauri-bridge.ts +1 -1
- package/src/main.tsx +27 -4
- package/src/pages/AgentPage.tsx +39 -34
- package/src/pages/ChatPanelPreviewPage.tsx +46 -44
- package/src/pages/SettingsPage.tsx +51 -45
- package/src/vite-env.d.ts +3 -0
- package/vite.base.js +6 -2
- package/src/lib/electron-bridge.ts +0 -390
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { useEffect, useId } from 'react';
|
|
5
5
|
import { createPortal } from 'react-dom';
|
|
6
|
+
import { t } from '@/i18n';
|
|
6
7
|
|
|
7
8
|
export interface ConfirmDialogProps {
|
|
8
9
|
open: boolean;
|
|
@@ -19,7 +20,7 @@ export function ConfirmDialog({
|
|
|
19
20
|
open,
|
|
20
21
|
title,
|
|
21
22
|
description,
|
|
22
|
-
confirmLabel = '
|
|
23
|
+
confirmLabel = t('Delete'),
|
|
23
24
|
pending = false,
|
|
24
25
|
onCancel,
|
|
25
26
|
onConfirm,
|
|
@@ -71,7 +72,7 @@ export function ConfirmDialog({
|
|
|
71
72
|
className="h-9 rounded-full px-4 text-sm text-agent-muted-foreground transition-colors hover:bg-agent-muted hover:text-agent-foreground disabled:opacity-40"
|
|
72
73
|
data-testid={`${testId}-cancel`}
|
|
73
74
|
>
|
|
74
|
-
|
|
75
|
+
{t('Cancel')}
|
|
75
76
|
</button>
|
|
76
77
|
<button
|
|
77
78
|
type="button"
|
|
@@ -80,7 +81,7 @@ export function ConfirmDialog({
|
|
|
80
81
|
className="h-9 rounded-full bg-agent-destructive px-4 text-sm font-medium text-white transition-opacity hover:opacity-90 disabled:opacity-40"
|
|
81
82
|
data-testid={`${testId}-confirm`}
|
|
82
83
|
>
|
|
83
|
-
{pending ? '
|
|
84
|
+
{pending ? t('Deleting…') : confirmLabel}
|
|
84
85
|
</button>
|
|
85
86
|
</div>
|
|
86
87
|
</div>
|
|
@@ -7,7 +7,8 @@ import { createPortal } from 'react-dom';
|
|
|
7
7
|
import { LuFolder, LuFolderPlus, LuX } from 'react-icons/lu';
|
|
8
8
|
import { BRAND_NAME } from '@/brand';
|
|
9
9
|
import { ConfirmDialog } from '@/components/ConfirmDialog';
|
|
10
|
-
import {
|
|
10
|
+
import { getHostBridge } from '@/lib/host-bridge';
|
|
11
|
+
import { t } from '@/i18n';
|
|
11
12
|
|
|
12
13
|
export interface ProjectFormValues {
|
|
13
14
|
name: string;
|
|
@@ -65,7 +66,7 @@ export function CreateProjectModal({
|
|
|
65
66
|
if (typeof document === 'undefined') return null;
|
|
66
67
|
|
|
67
68
|
const trimmed = name.trim();
|
|
68
|
-
const homePreview = `~/Documents/${BRAND_NAME}/${trimmed || '
|
|
69
|
+
const homePreview = `~/Documents/${BRAND_NAME}/${trimmed || t('project-name')}`;
|
|
69
70
|
const titleId = isEdit ? 'edit-project-title' : 'create-project-title';
|
|
70
71
|
|
|
71
72
|
const addSourceFolder = (folder: string) => {
|
|
@@ -82,13 +83,13 @@ export function CreateProjectModal({
|
|
|
82
83
|
addSourceFolder(typed);
|
|
83
84
|
return;
|
|
84
85
|
}
|
|
85
|
-
const picker =
|
|
86
|
+
const picker = getHostBridge()?.local?.selectDirectory;
|
|
86
87
|
if (!picker) {
|
|
87
|
-
setError('
|
|
88
|
+
setError(t('Enter a folder path'));
|
|
88
89
|
return;
|
|
89
90
|
}
|
|
90
91
|
try {
|
|
91
|
-
const result = await picker({ title: '
|
|
92
|
+
const result = await picker({ title: t('Add source folder') });
|
|
92
93
|
if (!result || result.canceled || result.filePaths.length === 0) return;
|
|
93
94
|
addSourceFolder(result.filePaths[0]);
|
|
94
95
|
} catch (err) {
|
|
@@ -137,13 +138,13 @@ export function CreateProjectModal({
|
|
|
137
138
|
<div className="flex w-[440px] max-w-[92vw] flex-col overflow-hidden rounded-2xl border border-agent-border bg-agent-canvas shadow-2xl">
|
|
138
139
|
<div className="flex items-center justify-between px-5 pb-1 pt-4">
|
|
139
140
|
<h2 id={titleId} className="text-base font-semibold text-agent-foreground">
|
|
140
|
-
{isEdit ? '
|
|
141
|
+
{isEdit ? t('Edit project') : t('New project')}
|
|
141
142
|
</h2>
|
|
142
143
|
<button
|
|
143
144
|
type="button"
|
|
144
145
|
onClick={onClose}
|
|
145
146
|
className="rounded-full p-1 text-agent-muted-foreground transition-colors hover:bg-agent-muted hover:text-agent-foreground"
|
|
146
|
-
aria-label=
|
|
147
|
+
aria-label={t('Close')}
|
|
147
148
|
>
|
|
148
149
|
<LuX className="h-4 w-4" />
|
|
149
150
|
</button>
|
|
@@ -159,7 +160,7 @@ export function CreateProjectModal({
|
|
|
159
160
|
onKeyDown={(event) => {
|
|
160
161
|
if (event.key === 'Enter') void handleSubmit();
|
|
161
162
|
}}
|
|
162
|
-
placeholder=
|
|
163
|
+
placeholder={t('Project name')}
|
|
163
164
|
autoFocus
|
|
164
165
|
className="h-full min-w-0 flex-1 bg-transparent text-sm text-agent-foreground outline-none placeholder:text-agent-muted-foreground"
|
|
165
166
|
data-testid={isEdit ? 'edit-project-name' : 'create-project-name'}
|
|
@@ -167,14 +168,14 @@ export function CreateProjectModal({
|
|
|
167
168
|
</label>
|
|
168
169
|
{!isEdit && (
|
|
169
170
|
<p className="px-1 text-[11px] text-agent-muted-foreground">
|
|
170
|
-
|
|
171
|
+
{t('Default location: {path}', { path: homePreview })}
|
|
171
172
|
</p>
|
|
172
173
|
)}
|
|
173
174
|
|
|
174
175
|
<div>
|
|
175
|
-
<div className="mb-1 text-sm text-agent-foreground"
|
|
176
|
+
<div className="mb-1 text-sm text-agent-foreground">{t('Source folders')}</div>
|
|
176
177
|
<p className="mb-2 text-[11px] text-agent-muted-foreground">
|
|
177
|
-
|
|
178
|
+
{t('These folders and their subfolders are readable and writable.')}
|
|
178
179
|
</p>
|
|
179
180
|
<div className="overflow-hidden rounded-2xl border border-agent-border">
|
|
180
181
|
{sourceFolders.map((folder) => (
|
|
@@ -192,7 +193,7 @@ export function CreateProjectModal({
|
|
|
192
193
|
setSourceFolders((prev) => prev.filter((item) => item !== folder))
|
|
193
194
|
}
|
|
194
195
|
className="rounded-full p-0.5 text-agent-muted-foreground hover:text-agent-foreground"
|
|
195
|
-
aria-label={
|
|
196
|
+
aria-label={t('Remove {folder}', { folder })}
|
|
196
197
|
>
|
|
197
198
|
<LuX className="h-3.5 w-3.5" />
|
|
198
199
|
</button>
|
|
@@ -210,7 +211,7 @@ export function CreateProjectModal({
|
|
|
210
211
|
void handleAddFolder();
|
|
211
212
|
}
|
|
212
213
|
}}
|
|
213
|
-
placeholder=
|
|
214
|
+
placeholder={t('Type a path, or click Add to browse')}
|
|
214
215
|
className="h-7 min-w-0 flex-1 bg-transparent text-xs text-agent-foreground outline-none placeholder:text-agent-muted-foreground"
|
|
215
216
|
data-testid={isEdit ? 'edit-project-folder-path' : 'create-project-folder-path'}
|
|
216
217
|
/>
|
|
@@ -220,7 +221,7 @@ export function CreateProjectModal({
|
|
|
220
221
|
className="shrink-0 rounded-full px-2 py-1 text-xs font-medium text-agent-foreground hover:bg-agent-muted"
|
|
221
222
|
data-testid={isEdit ? 'edit-project-add-folder' : 'create-project-add-folder'}
|
|
222
223
|
>
|
|
223
|
-
|
|
224
|
+
{t('Add')}
|
|
224
225
|
</button>
|
|
225
226
|
</div>
|
|
226
227
|
</div>
|
|
@@ -239,11 +240,11 @@ export function CreateProjectModal({
|
|
|
239
240
|
type="button"
|
|
240
241
|
onClick={() => setDeleteConfirmOpen(true)}
|
|
241
242
|
disabled={deleting || submitting}
|
|
242
|
-
title=
|
|
243
|
+
title={t('Delete project (its chats stay as chats without a project)')}
|
|
243
244
|
className="mr-auto h-9 rounded-full px-3 text-sm text-agent-destructive transition-colors hover:bg-agent-destructive/10 disabled:opacity-40"
|
|
244
245
|
data-testid="edit-project-delete"
|
|
245
246
|
>
|
|
246
|
-
|
|
247
|
+
{t('Delete local project')}
|
|
247
248
|
</button>
|
|
248
249
|
)}
|
|
249
250
|
<button
|
|
@@ -253,7 +254,7 @@ export function CreateProjectModal({
|
|
|
253
254
|
onDelete ? '' : 'ml-auto'
|
|
254
255
|
}`}
|
|
255
256
|
>
|
|
256
|
-
|
|
257
|
+
{t('Cancel')}
|
|
257
258
|
</button>
|
|
258
259
|
<button
|
|
259
260
|
type="button"
|
|
@@ -262,16 +263,25 @@ export function CreateProjectModal({
|
|
|
262
263
|
className="h-9 rounded-full bg-agent-foreground px-4 text-sm font-medium text-agent-canvas transition-opacity disabled:opacity-40"
|
|
263
264
|
data-testid={isEdit ? 'edit-project-submit' : 'create-project-submit'}
|
|
264
265
|
>
|
|
265
|
-
{submitting
|
|
266
|
+
{submitting
|
|
267
|
+
? isEdit
|
|
268
|
+
? t('Saving…')
|
|
269
|
+
: t('Creating…')
|
|
270
|
+
: isEdit
|
|
271
|
+
? t('Save')
|
|
272
|
+
: t('Create project')}
|
|
266
273
|
</button>
|
|
267
274
|
</div>
|
|
268
275
|
</div>
|
|
269
276
|
{onDelete && (
|
|
270
277
|
<ConfirmDialog
|
|
271
278
|
open={deleteConfirmOpen}
|
|
272
|
-
title=
|
|
273
|
-
description={
|
|
274
|
-
|
|
279
|
+
title={t('Delete project')}
|
|
280
|
+
description={t(
|
|
281
|
+
'Delete project "{name}"? Its chats will stay as chats without a project.',
|
|
282
|
+
{ name: trimmed || t('Untitled') },
|
|
283
|
+
)}
|
|
284
|
+
confirmLabel={t('Delete project')}
|
|
275
285
|
pending={deleting}
|
|
276
286
|
onCancel={() => {
|
|
277
287
|
if (deleting) return;
|
|
@@ -3,6 +3,7 @@ import { createPortal } from 'react-dom';
|
|
|
3
3
|
import { LuX } from 'react-icons/lu';
|
|
4
4
|
import type { LlmSettings } from '@/lib/local-api';
|
|
5
5
|
import { LlmSettingsPanel } from '@/components/settings/LlmSettingsPanel';
|
|
6
|
+
import { t } from '@/i18n';
|
|
6
7
|
|
|
7
8
|
interface LocalLlmSettingsModalProps {
|
|
8
9
|
open: boolean;
|
|
@@ -43,12 +44,12 @@ export function LocalLlmSettingsModal({ open, onClose, onSaved }: LocalLlmSettin
|
|
|
43
44
|
className="flex h-[560px] max-h-[90vh] w-[540px] max-w-[92vw] flex-col overflow-hidden rounded-agent-lg border border-agent-border bg-agent-canvas shadow-2xl animate-fade-in"
|
|
44
45
|
>
|
|
45
46
|
<div className="flex h-9 flex-shrink-0 items-center justify-between border-b border-agent-border px-3">
|
|
46
|
-
<h2 className="text-xs font-semibold text-agent-foreground"
|
|
47
|
+
<h2 className="text-xs font-semibold text-agent-foreground">{t('Local model settings')}</h2>
|
|
47
48
|
<button
|
|
48
49
|
type="button"
|
|
49
50
|
onClick={onClose}
|
|
50
51
|
className="rounded-full p-1 text-agent-muted-foreground transition-colors hover:bg-agent-muted hover:text-agent-foreground"
|
|
51
|
-
aria-label=
|
|
52
|
+
aria-label={t('Close')}
|
|
52
53
|
>
|
|
53
54
|
<LuX className="h-4 w-4" />
|
|
54
55
|
</button>
|
|
@@ -62,7 +63,7 @@ export function LocalLlmSettingsModal({ open, onClose, onSaved }: LocalLlmSettin
|
|
|
62
63
|
onClick={onClose}
|
|
63
64
|
className="h-7 rounded-full px-3 text-xs font-medium text-agent-muted-foreground transition-colors hover:bg-agent-muted hover:text-agent-foreground"
|
|
64
65
|
>
|
|
65
|
-
|
|
66
|
+
{t('Close')}
|
|
66
67
|
</button>
|
|
67
68
|
</div>
|
|
68
69
|
</div>
|
|
@@ -4,16 +4,17 @@ import {
|
|
|
4
4
|
type AppReleaseView,
|
|
5
5
|
} from '@/lib/app-release';
|
|
6
6
|
import {
|
|
7
|
-
|
|
7
|
+
getHostBridge,
|
|
8
8
|
type AppReleaseSnapshot,
|
|
9
|
-
} from '@/lib/
|
|
9
|
+
} from '@/lib/host-bridge';
|
|
10
|
+
import { t } from '@/i18n';
|
|
10
11
|
|
|
11
12
|
export function useAppRelease(): AppReleaseView & { run: () => void } {
|
|
12
13
|
const [snap, setSnap] = useState<AppReleaseSnapshot | null>(null);
|
|
13
14
|
const [confirmedCurrent, setConfirmedCurrent] = useState(false);
|
|
14
15
|
|
|
15
16
|
useEffect(() => {
|
|
16
|
-
const app =
|
|
17
|
+
const app = getHostBridge()?.app;
|
|
17
18
|
if (!app) return;
|
|
18
19
|
let alive = true;
|
|
19
20
|
void app.snapshot().then((next) => {
|
|
@@ -31,7 +32,7 @@ export function useAppRelease(): AppReleaseView & { run: () => void } {
|
|
|
31
32
|
}, []);
|
|
32
33
|
|
|
33
34
|
const run = useCallback(() => {
|
|
34
|
-
const app =
|
|
35
|
+
const app = getHostBridge()?.app;
|
|
35
36
|
if (!app || !snap || !snap.enabled) return;
|
|
36
37
|
if (snap.phase !== 'idle' && snap.phase !== 'error' && snap.phase !== 'ready') return;
|
|
37
38
|
const task = snap.phase === 'ready' ? app.install() : app.check();
|
|
@@ -59,7 +60,7 @@ export function SidebarVersionLabel({ release }: { release: AppReleaseView }) {
|
|
|
59
60
|
<span
|
|
60
61
|
className="ml-auto text-[10px] tabular-nums text-agent-muted-foreground/70"
|
|
61
62
|
data-testid="sidebar-app-version"
|
|
62
|
-
title={
|
|
63
|
+
title={t('Current version {version}', { version: release.version })}
|
|
63
64
|
>
|
|
64
65
|
v{release.version}
|
|
65
66
|
</span>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Suspense, lazy, useEffect, useState } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { getHostBridge, hasHostBridge } from '@/lib/host-bridge';
|
|
3
|
+
import { t } from '@/i18n';
|
|
3
4
|
|
|
4
5
|
// 把 xterm 那一坨 (~300KB) 拆到独立 chunk —— 用户没打开终端就不下载。
|
|
5
6
|
// 跟原 Next.js 版本里 `dynamic(() => import('./TerminalView'), {ssr:false})`
|
|
@@ -16,7 +17,7 @@ const TerminalView = lazy(() =>
|
|
|
16
17
|
* xterm 视图,不杀 shell;重新打开时通过 `terminal:ensure` 的 replay
|
|
17
18
|
* buffer 补回面板关闭期间的输出。
|
|
18
19
|
*
|
|
19
|
-
* Electron 检测保留给浏览器预览 / dev 模式(没有 `window.
|
|
20
|
+
* Electron 检测保留给浏览器预览 / dev 模式(没有 `window.steerableHost` 就
|
|
20
21
|
* 连不到本地 PTY)。延迟到首个 effect 再渲染,避免检测闪烁。
|
|
21
22
|
*/
|
|
22
23
|
export function TerminalPanel({
|
|
@@ -32,7 +33,7 @@ export function TerminalPanel({
|
|
|
32
33
|
const [hasElectron, setHasElectron] = useState<boolean | null>(null);
|
|
33
34
|
|
|
34
35
|
useEffect(() => {
|
|
35
|
-
setHasElectron(
|
|
36
|
+
setHasElectron(hasHostBridge() && !!getHostBridge()?.terminal);
|
|
36
37
|
}, []);
|
|
37
38
|
|
|
38
39
|
if (hasElectron === null) {
|
|
@@ -42,9 +43,9 @@ export function TerminalPanel({
|
|
|
42
43
|
return (
|
|
43
44
|
<div className="flex h-full w-full items-center justify-center bg-black p-4 text-center font-mono text-xs text-white">
|
|
44
45
|
<div>
|
|
45
|
-
<p className="mb-2">⚠️
|
|
46
|
+
<p className="mb-2">⚠️ {t('This panel needs the desktop client or a BS server connection.')}</p>
|
|
46
47
|
<p className="text-white/60">
|
|
47
|
-
|
|
48
|
+
{t('This page has no host bridge, so it cannot connect to a local PTY.')}
|
|
48
49
|
</p>
|
|
49
50
|
</div>
|
|
50
51
|
</div>
|
|
@@ -5,15 +5,16 @@ import { WebLinksAddon } from '@xterm/addon-web-links';
|
|
|
5
5
|
import { LuListTodo } from 'react-icons/lu';
|
|
6
6
|
import '@xterm/xterm/css/xterm.css';
|
|
7
7
|
import { DockHeaderButton, DockPanelHeader } from '@/components/DockPanelHeader';
|
|
8
|
-
import {
|
|
8
|
+
import { getHostBridge } from '@/lib/host-bridge';
|
|
9
9
|
import { BRAND_NAME } from '@/brand';
|
|
10
|
+
import { t } from '@/i18n';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Renders an xterm.js terminal wired up to the main-process PTY via the
|
|
13
14
|
* `terminal.*` bridge. Ported from `deeppath/apps/web/src/app/agent/terminal/
|
|
14
15
|
* TerminalView.tsx`, with these adaptations:
|
|
15
16
|
*
|
|
16
|
-
* - `window.
|
|
17
|
+
* - `window.steerableHost` direct access → `getHostBridge()` helper so the
|
|
17
18
|
* "no bridge" path is type-safe.
|
|
18
19
|
* - Removed `'use client'` directive (not a Next.js project).
|
|
19
20
|
* - Container is `h-full` instead of `h-screen` so the component is
|
|
@@ -44,11 +45,11 @@ export function TerminalView({
|
|
|
44
45
|
|
|
45
46
|
useEffect(() => {
|
|
46
47
|
if (!containerRef.current) return;
|
|
47
|
-
const bridge =
|
|
48
|
+
const bridge = getHostBridge();
|
|
48
49
|
const terminal = bridge?.terminal;
|
|
49
50
|
if (!terminal) {
|
|
50
51
|
setStatus('error');
|
|
51
|
-
setStatusMsg('
|
|
52
|
+
setStatusMsg(t('The host terminal is unavailable'));
|
|
52
53
|
return;
|
|
53
54
|
}
|
|
54
55
|
|
|
@@ -199,23 +200,27 @@ export function TerminalView({
|
|
|
199
200
|
}, []);
|
|
200
201
|
|
|
201
202
|
const statusLabel =
|
|
202
|
-
status === 'starting'
|
|
203
|
+
status === 'starting'
|
|
204
|
+
? t('Starting…')
|
|
205
|
+
: status === 'ready'
|
|
206
|
+
? t('Online')
|
|
207
|
+
: t('Error: {message}', { message: statusMsg });
|
|
203
208
|
|
|
204
209
|
return (
|
|
205
210
|
<div className="flex h-full w-full flex-col bg-[#0b0b0c]">
|
|
206
211
|
<DockPanelHeader
|
|
207
|
-
title={
|
|
212
|
+
title={t('{brand} · Terminal · {status}', { brand: BRAND_NAME, status: statusLabel })}
|
|
208
213
|
onClose={onClose}
|
|
209
|
-
closeLabel=
|
|
214
|
+
closeLabel={t('Close terminal panel')}
|
|
210
215
|
actions={
|
|
211
216
|
onShowTaskProcess && (
|
|
212
217
|
<DockHeaderButton
|
|
213
218
|
icon={<LuListTodo className="h-3 w-3" />}
|
|
214
|
-
label=
|
|
219
|
+
label={t('Background tasks')}
|
|
215
220
|
title={
|
|
216
221
|
taskProcessTitle
|
|
217
|
-
?
|
|
218
|
-
: '
|
|
222
|
+
? t('Switch to background reasoning: {title}', { title: taskProcessTitle })
|
|
223
|
+
: t('Switch to background reasoning')
|
|
219
224
|
}
|
|
220
225
|
onClick={onShowTaskProcess}
|
|
221
226
|
/>
|
|
@@ -2,7 +2,8 @@ import { LuShieldAlert, LuTriangleAlert } from 'react-icons/lu';
|
|
|
2
2
|
import {
|
|
3
3
|
type ApprovalDecisionKind,
|
|
4
4
|
type ApprovalPromptRequest,
|
|
5
|
-
} from '@/lib/
|
|
5
|
+
} from '@/lib/host-bridge';
|
|
6
|
+
import { t } from '@/i18n';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* ApprovalPromptMenu — W4-1 审批代数的输入区 UI。
|
|
@@ -19,10 +20,10 @@ import {
|
|
|
19
20
|
*/
|
|
20
21
|
|
|
21
22
|
const MODE_LABEL: Record<string, string> = {
|
|
22
|
-
read: '
|
|
23
|
-
safe_write: '
|
|
24
|
-
destructive: '
|
|
25
|
-
other: '
|
|
23
|
+
read: 'Read only',
|
|
24
|
+
safe_write: 'Write',
|
|
25
|
+
destructive: 'Dangerous',
|
|
26
|
+
other: 'Other',
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
function summarizeArguments(args: Record<string, unknown>): string {
|
|
@@ -45,15 +46,15 @@ interface DecisionButton {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
const ALLOW_BUTTONS: DecisionButton[] = [
|
|
48
|
-
{ kind: 'allow_once', label: '
|
|
49
|
-
{ kind: 'allow_for_session', label: '
|
|
50
|
-
{ kind: 'allow_always', label: '
|
|
49
|
+
{ kind: 'allow_once', label: 'Allow once', tone: 'allow' },
|
|
50
|
+
{ kind: 'allow_for_session', label: 'Allow for this chat', tone: 'allow' },
|
|
51
|
+
{ kind: 'allow_always', label: 'Always allow', tone: 'allow' },
|
|
51
52
|
];
|
|
52
53
|
|
|
53
54
|
const DENY_BUTTONS: DecisionButton[] = [
|
|
54
|
-
{ kind: 'deny_once', label: '
|
|
55
|
-
{ kind: 'deny_for_session', label: '
|
|
56
|
-
{ kind: 'deny_always', label: '
|
|
55
|
+
{ kind: 'deny_once', label: 'Deny once', tone: 'deny' },
|
|
56
|
+
{ kind: 'deny_for_session', label: 'Deny for this chat', tone: 'deny' },
|
|
57
|
+
{ kind: 'deny_always', label: 'Always deny', tone: 'deny' },
|
|
57
58
|
];
|
|
58
59
|
|
|
59
60
|
function DecisionRow({
|
|
@@ -77,7 +78,7 @@ function DecisionRow({
|
|
|
77
78
|
: 'rounded-agent-md border border-agent-border px-3 py-1.5 text-xs font-medium text-agent-foreground transition hover:bg-agent-muted'
|
|
78
79
|
}
|
|
79
80
|
>
|
|
80
|
-
{b.label}
|
|
81
|
+
{t(b.label)}
|
|
81
82
|
</button>
|
|
82
83
|
))}
|
|
83
84
|
</div>
|
|
@@ -120,27 +121,29 @@ export function ApprovalPromptMenu({
|
|
|
120
121
|
<div className="text-xs font-medium text-agent-foreground">
|
|
121
122
|
{isEgress ? (
|
|
122
123
|
<>
|
|
123
|
-
Agent
|
|
124
|
+
{t('Agent requests internet access')}
|
|
124
125
|
<span className="mx-1 font-mono">
|
|
125
126
|
{egressHost}
|
|
126
127
|
{egressPort !== null ? `:${egressPort}` : ''}
|
|
127
128
|
</span>
|
|
128
129
|
</>
|
|
129
130
|
) : isSandboxEscalation ? (
|
|
130
|
-
<>Agent
|
|
131
|
+
<>{t('Agent requests to run a command outside the workspace')}</>
|
|
131
132
|
) : (
|
|
132
133
|
<>
|
|
133
|
-
Agent
|
|
134
|
+
{t('Agent requests to run')}
|
|
134
135
|
<span className="mx-1 font-mono">{current.toolName}</span>
|
|
135
136
|
<span className="text-agent-muted-foreground">
|
|
136
|
-
|
|
137
|
+
{t('({mode} action)', {
|
|
138
|
+
mode: MODE_LABEL[current.mode] ? t(MODE_LABEL[current.mode]) : current.mode,
|
|
139
|
+
})}
|
|
137
140
|
</span>
|
|
138
141
|
</>
|
|
139
142
|
)}
|
|
140
143
|
</div>
|
|
141
144
|
{pendingCount > 0 && (
|
|
142
145
|
<div className="mt-0.5 text-[11px] text-agent-muted-foreground">
|
|
143
|
-
|
|
146
|
+
{t('{count} more awaiting approval', { count: pendingCount })}
|
|
144
147
|
</div>
|
|
145
148
|
)}
|
|
146
149
|
</div>
|
|
@@ -152,23 +155,30 @@ export function ApprovalPromptMenu({
|
|
|
152
155
|
</pre>
|
|
153
156
|
{isEgress ? (
|
|
154
157
|
<p className="mt-2 text-[11px] leading-relaxed text-agent-muted-foreground">
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
+
{t(
|
|
159
|
+
'This domain is not on the egress allowlist. Check the full spelling before allowing it (lookalike domains often use similar spellings, such as cdn.jsdelivr.net.evil.com). Allowing it applies only to this chat. To allow it permanently, add the domain to the egress allowlist in Settings.',
|
|
160
|
+
)}
|
|
158
161
|
</p>
|
|
159
162
|
) : isSandboxEscalation ? (
|
|
160
163
|
<p className="mt-2 text-[11px] leading-relaxed text-agent-muted-foreground">
|
|
161
|
-
|
|
164
|
+
{t(
|
|
165
|
+
'The workspace sandbox blocked this command. Allowing it retries only this command, without limiting where it can write.',
|
|
166
|
+
)}
|
|
162
167
|
</p>
|
|
163
168
|
) : (
|
|
164
169
|
<>
|
|
165
170
|
{current.category !== current.toolName && (
|
|
166
171
|
<p className="mt-2 text-[11px] text-agent-muted-foreground">
|
|
167
|
-
|
|
172
|
+
{t(
|
|
173
|
+
'Saved category: {category} (a chat or always decision applies to every call in this category)',
|
|
174
|
+
{ category: current.category },
|
|
175
|
+
)}
|
|
168
176
|
</p>
|
|
169
177
|
)}
|
|
170
178
|
<p className="mt-2 text-[11px] leading-relaxed text-agent-muted-foreground">
|
|
171
|
-
|
|
179
|
+
{t(
|
|
180
|
+
'Runs in the workspace sandbox. Writes outside it are blocked. To write outside, switch the sandbox to "Full access" and try again.',
|
|
181
|
+
)}
|
|
172
182
|
</p>
|
|
173
183
|
</>
|
|
174
184
|
)}
|
|
@@ -198,13 +208,17 @@ export function ApprovalPromptMenu({
|
|
|
198
208
|
className="shrink-0 rounded-agent-md bg-agent-destructive px-3 py-1.5 text-xs font-medium text-white transition hover:opacity-90"
|
|
199
209
|
data-testid="approval-abort"
|
|
200
210
|
>
|
|
201
|
-
|
|
211
|
+
{t('Abort this turn')}
|
|
202
212
|
</button>
|
|
203
213
|
</div>
|
|
204
214
|
<p className="text-[11px] leading-relaxed text-agent-muted-foreground">
|
|
205
215
|
{isEgress
|
|
206
|
-
?
|
|
207
|
-
|
|
216
|
+
? t(
|
|
217
|
+
'"This chat" decisions are remembered in this chat until the agent process exits. With no action for about 3 minutes, the request is denied.',
|
|
218
|
+
)
|
|
219
|
+
: t(
|
|
220
|
+
'"This chat" decisions are remembered in this chat. "Always" decisions are written to ~/.steerable/approvals.json on this computer and apply across chats. With no action for about 2 minutes, the request is denied.',
|
|
221
|
+
)}
|
|
208
222
|
</p>
|
|
209
223
|
</div>
|
|
210
224
|
</div>
|
|
@@ -9,10 +9,10 @@ import {
|
|
|
9
9
|
type ReactNode,
|
|
10
10
|
} from 'react';
|
|
11
11
|
import {
|
|
12
|
-
|
|
12
|
+
getHostBridge,
|
|
13
13
|
type ApprovalDecisionKind,
|
|
14
14
|
type ApprovalPromptRequest,
|
|
15
|
-
} from '@/lib/
|
|
15
|
+
} from '@/lib/host-bridge';
|
|
16
16
|
import { bindPromptToActiveChat, promptVisibleInChat } from './prompt-chat-scope';
|
|
17
17
|
|
|
18
18
|
interface ApprovalPromptContextValue {
|
|
@@ -37,7 +37,7 @@ export function ApprovalPromptProvider({ children }: { children: ReactNode }) {
|
|
|
37
37
|
const decidedRequestIds = useRef(new Set<string>());
|
|
38
38
|
|
|
39
39
|
useEffect(() => {
|
|
40
|
-
const bridge =
|
|
40
|
+
const bridge = getHostBridge();
|
|
41
41
|
if (!bridge?.approval) return;
|
|
42
42
|
let active = true;
|
|
43
43
|
const addRequests = (
|
|
@@ -67,7 +67,7 @@ export function ApprovalPromptProvider({ children }: { children: ReactNode }) {
|
|
|
67
67
|
if (active) addRequests(requests, 'prepend');
|
|
68
68
|
})
|
|
69
69
|
.catch((error) => {
|
|
70
|
-
console.error('
|
|
70
|
+
console.error('Failed to restore pending approval requests:', error);
|
|
71
71
|
});
|
|
72
72
|
return () => {
|
|
73
73
|
active = false;
|
|
@@ -76,7 +76,7 @@ export function ApprovalPromptProvider({ children }: { children: ReactNode }) {
|
|
|
76
76
|
}, []);
|
|
77
77
|
|
|
78
78
|
const decide = useCallback((requestId: string, kind: ApprovalDecisionKind) => {
|
|
79
|
-
const bridge =
|
|
79
|
+
const bridge = getHostBridge();
|
|
80
80
|
decidedRequestIds.current.add(requestId);
|
|
81
81
|
setQueue((previous) => previous.filter((request) => request.requestId !== requestId));
|
|
82
82
|
void bridge?.approval?.decide({ requestId, kind });
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
useState,
|
|
9
9
|
type ReactNode,
|
|
10
10
|
} from 'react';
|
|
11
|
-
import {
|
|
11
|
+
import { getHostBridge, type AskUserPromptRequest } from '@/lib/host-bridge';
|
|
12
12
|
import { bindPromptToActiveChat, promptVisibleInChat } from './prompt-chat-scope';
|
|
13
13
|
|
|
14
14
|
type AnswerValue = string | string[];
|
|
@@ -35,7 +35,7 @@ export function AskUserPromptProvider({ children }: { children: ReactNode }) {
|
|
|
35
35
|
const answeredRequestIds = useRef(new Set<string>());
|
|
36
36
|
|
|
37
37
|
useEffect(() => {
|
|
38
|
-
const bridge =
|
|
38
|
+
const bridge = getHostBridge();
|
|
39
39
|
if (!bridge?.askUser) return;
|
|
40
40
|
let active = true;
|
|
41
41
|
const addRequests = (
|
|
@@ -65,7 +65,7 @@ export function AskUserPromptProvider({ children }: { children: ReactNode }) {
|
|
|
65
65
|
if (active) addRequests(requests, 'prepend');
|
|
66
66
|
})
|
|
67
67
|
.catch((error) => {
|
|
68
|
-
console.error('
|
|
68
|
+
console.error('Failed to restore pending questions:', error);
|
|
69
69
|
});
|
|
70
70
|
return () => {
|
|
71
71
|
active = false;
|
|
@@ -75,7 +75,7 @@ export function AskUserPromptProvider({ children }: { children: ReactNode }) {
|
|
|
75
75
|
|
|
76
76
|
const answer = useCallback(
|
|
77
77
|
(requestId: string, answers: Record<string, AnswerValue>) => {
|
|
78
|
-
const bridge =
|
|
78
|
+
const bridge = getHostBridge();
|
|
79
79
|
answeredRequestIds.current.add(requestId);
|
|
80
80
|
setQueue((previous) => previous.filter((request) => request.requestId !== requestId));
|
|
81
81
|
void bridge?.askUser?.answer({ requestId, answers });
|