@steerable/agent-shell-web 0.6.31 → 0.6.33
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 +5 -8
- package/src/components/AgentSidebar.tsx +298 -77
- package/src/components/CreateProjectModal.tsx +156 -67
- package/src/components/chat/ApprovalModal.tsx +2 -2
- package/src/components/chat/ChatInput.tsx +13 -3
- package/src/components/chat/ChatProjectBadge.tsx +1 -1
- package/src/components/chat/ExecPolicyPicker.tsx +4 -3
- package/src/components/chat/FilePathCode.tsx +6 -2
- package/src/components/chat/TurnFilesCard.tsx +14 -3
- package/src/layouts/AgentLayout.tsx +16 -9
- package/src/lib/exec-policy.ts +5 -2
- package/src/lib/host-tools.ts +206 -0
- package/src/lib/http-bridge.ts +20 -7
- package/src/lib/local-api.ts +2 -2
- package/src/pages/AgentPage.tsx +42 -27
- package/src/pages/SettingsPage.tsx +21 -2
- package/src/vite-env.d.ts +16 -0
- package/vite.base.js +5 -0
|
@@ -1,56 +1,98 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* 家目录由后端建在 Documents
|
|
2
|
+
* 新建 / 编辑项目弹窗(Codex 式):项目是带名字的容器,可附加多个源文件夹。
|
|
3
|
+
* 家目录由后端建在 Documents/<应用名>/<项目名>/;源文件夹只放宽读取。
|
|
4
4
|
*/
|
|
5
5
|
import { useEffect, useState } from 'react';
|
|
6
6
|
import { createPortal } from 'react-dom';
|
|
7
7
|
import { LuFolder, LuFolderPlus, LuX } from 'react-icons/lu';
|
|
8
8
|
import { BRAND_NAME } from '@/brand';
|
|
9
|
-
import { getElectronBridge
|
|
9
|
+
import { getElectronBridge } from '@/lib/electron-bridge';
|
|
10
|
+
|
|
11
|
+
export interface ProjectFormValues {
|
|
12
|
+
name: string;
|
|
13
|
+
sourceFolders: string[];
|
|
14
|
+
}
|
|
10
15
|
|
|
11
16
|
export interface CreateProjectModalProps {
|
|
12
17
|
open: boolean;
|
|
13
18
|
onClose: () => void;
|
|
14
|
-
onCreate: (input:
|
|
19
|
+
onCreate: (input: ProjectFormValues) => Promise<void>;
|
|
20
|
+
mode?: 'create' | 'edit';
|
|
21
|
+
initial?: ProjectFormValues;
|
|
22
|
+
onDelete?: () => Promise<void> | void;
|
|
15
23
|
}
|
|
16
24
|
|
|
17
|
-
export function CreateProjectModal({
|
|
18
|
-
|
|
19
|
-
|
|
25
|
+
export function CreateProjectModal({
|
|
26
|
+
open,
|
|
27
|
+
onClose,
|
|
28
|
+
onCreate,
|
|
29
|
+
mode = 'create',
|
|
30
|
+
initial,
|
|
31
|
+
onDelete,
|
|
32
|
+
}: CreateProjectModalProps) {
|
|
33
|
+
const isEdit = mode === 'edit';
|
|
34
|
+
const [name, setName] = useState(initial?.name ?? '');
|
|
35
|
+
const [sourceFolders, setSourceFolders] = useState(initial?.sourceFolders ?? []);
|
|
36
|
+
const [folderDraft, setFolderDraft] = useState('');
|
|
20
37
|
const [submitting, setSubmitting] = useState(false);
|
|
38
|
+
const [deleting, setDeleting] = useState(false);
|
|
39
|
+
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
21
40
|
const [error, setError] = useState<string | null>(null);
|
|
41
|
+
const initialFoldersKey = (initial?.sourceFolders ?? []).join('\0');
|
|
22
42
|
|
|
23
43
|
useEffect(() => {
|
|
24
44
|
if (!open) return;
|
|
25
|
-
setName('');
|
|
26
|
-
setSourceFolders([]);
|
|
45
|
+
setName(initial?.name ?? '');
|
|
46
|
+
setSourceFolders(initial?.sourceFolders ?? []);
|
|
47
|
+
setFolderDraft('');
|
|
27
48
|
setSubmitting(false);
|
|
49
|
+
setDeleting(false);
|
|
50
|
+
setConfirmDelete(false);
|
|
28
51
|
setError(null);
|
|
29
52
|
const onKey = (event: KeyboardEvent) => {
|
|
30
53
|
if (event.key === 'Escape') onClose();
|
|
31
54
|
};
|
|
32
55
|
window.addEventListener('keydown', onKey);
|
|
33
56
|
return () => window.removeEventListener('keydown', onKey);
|
|
34
|
-
}, [open, onClose]);
|
|
57
|
+
}, [open, onClose, initial?.name, initialFoldersKey]);
|
|
35
58
|
|
|
36
59
|
if (!open) return null;
|
|
37
60
|
if (typeof document === 'undefined') return null;
|
|
38
61
|
|
|
39
62
|
const trimmed = name.trim();
|
|
40
63
|
const homePreview = `~/Documents/${BRAND_NAME}/${trimmed || '项目名'}`;
|
|
64
|
+
const titleId = isEdit ? 'edit-project-title' : 'create-project-title';
|
|
41
65
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
title: '添加源文件夹',
|
|
46
|
-
});
|
|
47
|
-
if (!result || result.canceled || result.filePaths.length === 0) return;
|
|
48
|
-
const next = result.filePaths[0];
|
|
66
|
+
const addSourceFolder = (folder: string) => {
|
|
67
|
+
const next = folder.trim();
|
|
68
|
+
if (!next) return;
|
|
49
69
|
setSourceFolders((prev) => (prev.includes(next) ? prev : [...prev, next]));
|
|
70
|
+
setFolderDraft('');
|
|
71
|
+
setError(null);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const handleAddFolder = async () => {
|
|
75
|
+
const typed = folderDraft.trim();
|
|
76
|
+
if (typed) {
|
|
77
|
+
addSourceFolder(typed);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const picker = getElectronBridge()?.local?.selectDirectory;
|
|
81
|
+
if (!picker) {
|
|
82
|
+
setError('请输入文件夹路径');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const result = await picker({ title: '添加源文件夹' });
|
|
87
|
+
if (!result || result.canceled || result.filePaths.length === 0) return;
|
|
88
|
+
addSourceFolder(result.filePaths[0]);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
91
|
+
}
|
|
50
92
|
};
|
|
51
93
|
|
|
52
94
|
const handleSubmit = async () => {
|
|
53
|
-
if (!trimmed || submitting) return;
|
|
95
|
+
if (!trimmed || submitting || deleting) return;
|
|
54
96
|
setSubmitting(true);
|
|
55
97
|
setError(null);
|
|
56
98
|
try {
|
|
@@ -62,6 +104,23 @@ export function CreateProjectModal({ open, onClose, onCreate }: CreateProjectMod
|
|
|
62
104
|
}
|
|
63
105
|
};
|
|
64
106
|
|
|
107
|
+
const handleDelete = async () => {
|
|
108
|
+
if (!onDelete || deleting || submitting) return;
|
|
109
|
+
if (!confirmDelete) {
|
|
110
|
+
setConfirmDelete(true);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
setDeleting(true);
|
|
114
|
+
setError(null);
|
|
115
|
+
try {
|
|
116
|
+
await onDelete();
|
|
117
|
+
onClose();
|
|
118
|
+
} catch (err) {
|
|
119
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
120
|
+
setDeleting(false);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
65
124
|
return createPortal(
|
|
66
125
|
<div
|
|
67
126
|
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/50 backdrop-blur-sm"
|
|
@@ -70,13 +129,13 @@ export function CreateProjectModal({ open, onClose, onCreate }: CreateProjectMod
|
|
|
70
129
|
}}
|
|
71
130
|
role="dialog"
|
|
72
131
|
aria-modal="true"
|
|
73
|
-
aria-labelledby=
|
|
74
|
-
data-testid=
|
|
132
|
+
aria-labelledby={titleId}
|
|
133
|
+
data-testid={isEdit ? 'edit-project-dialog' : 'create-project-dialog'}
|
|
75
134
|
>
|
|
76
135
|
<div className="flex w-[440px] max-w-[92vw] flex-col overflow-hidden rounded-2xl border border-agent-border bg-agent-canvas shadow-2xl">
|
|
77
136
|
<div className="flex items-center justify-between px-5 pb-1 pt-4">
|
|
78
|
-
<h2 id=
|
|
79
|
-
新建项目
|
|
137
|
+
<h2 id={titleId} className="text-base font-semibold text-agent-foreground">
|
|
138
|
+
{isEdit ? '编辑项目' : '新建项目'}
|
|
80
139
|
</h2>
|
|
81
140
|
<button
|
|
82
141
|
type="button"
|
|
@@ -101,52 +160,64 @@ export function CreateProjectModal({ open, onClose, onCreate }: CreateProjectMod
|
|
|
101
160
|
placeholder="项目名称"
|
|
102
161
|
autoFocus
|
|
103
162
|
className="h-full min-w-0 flex-1 bg-transparent text-sm text-agent-foreground outline-none placeholder:text-agent-muted-foreground"
|
|
104
|
-
data-testid=
|
|
163
|
+
data-testid={isEdit ? 'edit-project-name' : 'create-project-name'}
|
|
105
164
|
/>
|
|
106
165
|
</label>
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
166
|
+
{!isEdit && (
|
|
167
|
+
<p className="px-1 text-[11px] text-agent-muted-foreground">
|
|
168
|
+
默认位置:{homePreview}
|
|
169
|
+
</p>
|
|
170
|
+
)}
|
|
110
171
|
|
|
111
172
|
<div>
|
|
112
173
|
<div className="mb-2 text-sm text-agent-foreground">源文件夹</div>
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
174
|
+
<div className="overflow-hidden rounded-2xl border border-agent-border">
|
|
175
|
+
{sourceFolders.map((folder) => (
|
|
176
|
+
<div
|
|
177
|
+
key={folder}
|
|
178
|
+
className="flex items-center gap-2 border-b border-agent-border/60 px-3 py-2 text-xs text-agent-foreground"
|
|
179
|
+
>
|
|
180
|
+
<LuFolder className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
|
|
181
|
+
<span className="min-w-0 flex-1 truncate font-mono" title={folder}>
|
|
182
|
+
{folder}
|
|
183
|
+
</span>
|
|
184
|
+
<button
|
|
185
|
+
type="button"
|
|
186
|
+
onClick={() =>
|
|
187
|
+
setSourceFolders((prev) => prev.filter((item) => item !== folder))
|
|
188
|
+
}
|
|
189
|
+
className="rounded-full p-0.5 text-agent-muted-foreground hover:text-agent-foreground"
|
|
190
|
+
aria-label={`移除 ${folder}`}
|
|
119
191
|
>
|
|
120
|
-
<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
</button>
|
|
192
|
+
<LuX className="h-3.5 w-3.5" />
|
|
193
|
+
</button>
|
|
194
|
+
</div>
|
|
195
|
+
))}
|
|
196
|
+
<div className="flex items-center gap-2 px-3 py-2">
|
|
197
|
+
<LuFolderPlus className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
|
|
198
|
+
<input
|
|
199
|
+
type="text"
|
|
200
|
+
value={folderDraft}
|
|
201
|
+
onChange={(event) => setFolderDraft(event.target.value)}
|
|
202
|
+
onKeyDown={(event) => {
|
|
203
|
+
if (event.key === 'Enter') {
|
|
204
|
+
event.preventDefault();
|
|
205
|
+
void handleAddFolder();
|
|
206
|
+
}
|
|
207
|
+
}}
|
|
208
|
+
placeholder="输入路径或点添加浏览"
|
|
209
|
+
className="h-7 min-w-0 flex-1 bg-transparent text-xs text-agent-foreground outline-none placeholder:text-agent-muted-foreground"
|
|
210
|
+
data-testid={isEdit ? 'edit-project-folder-path' : 'create-project-folder-path'}
|
|
211
|
+
/>
|
|
212
|
+
<button
|
|
213
|
+
type="button"
|
|
214
|
+
onClick={() => void handleAddFolder()}
|
|
215
|
+
className="shrink-0 rounded-full px-2 py-1 text-xs font-medium text-agent-foreground hover:bg-agent-muted"
|
|
216
|
+
data-testid={isEdit ? 'edit-project-add-folder' : 'create-project-add-folder'}
|
|
217
|
+
>
|
|
218
|
+
添加
|
|
219
|
+
</button>
|
|
220
|
+
</div>
|
|
150
221
|
</div>
|
|
151
222
|
</div>
|
|
152
223
|
|
|
@@ -157,22 +228,40 @@ export function CreateProjectModal({ open, onClose, onCreate }: CreateProjectMod
|
|
|
157
228
|
)}
|
|
158
229
|
</div>
|
|
159
230
|
|
|
160
|
-
<div className="flex items-center
|
|
231
|
+
<div className="flex items-center gap-2 px-5 pb-4 pt-1">
|
|
232
|
+
{onDelete && (
|
|
233
|
+
<button
|
|
234
|
+
type="button"
|
|
235
|
+
onClick={() => void handleDelete()}
|
|
236
|
+
disabled={deleting || submitting}
|
|
237
|
+
title={
|
|
238
|
+
confirmDelete
|
|
239
|
+
? '再次点击确认删除(会话会保留为无项目对话)'
|
|
240
|
+
: '删除项目(会话保留为无项目对话)'
|
|
241
|
+
}
|
|
242
|
+
className="mr-auto h-9 rounded-full px-3 text-sm text-agent-destructive transition-colors hover:bg-agent-destructive/10 disabled:opacity-40"
|
|
243
|
+
data-testid="edit-project-delete"
|
|
244
|
+
>
|
|
245
|
+
{confirmDelete ? '再次点击确认删除' : '删除本地项目'}
|
|
246
|
+
</button>
|
|
247
|
+
)}
|
|
161
248
|
<button
|
|
162
249
|
type="button"
|
|
163
250
|
onClick={onClose}
|
|
164
|
-
className=
|
|
251
|
+
className={`h-9 rounded-full px-4 text-sm text-agent-muted-foreground transition-colors hover:bg-agent-muted hover:text-agent-foreground ${
|
|
252
|
+
onDelete ? '' : 'ml-auto'
|
|
253
|
+
}`}
|
|
165
254
|
>
|
|
166
255
|
取消
|
|
167
256
|
</button>
|
|
168
257
|
<button
|
|
169
258
|
type="button"
|
|
170
259
|
onClick={() => void handleSubmit()}
|
|
171
|
-
disabled={!trimmed || submitting}
|
|
260
|
+
disabled={!trimmed || submitting || deleting}
|
|
172
261
|
className="h-9 rounded-full bg-agent-foreground px-4 text-sm font-medium text-agent-canvas transition-opacity disabled:opacity-40"
|
|
173
|
-
data-testid=
|
|
262
|
+
data-testid={isEdit ? 'edit-project-submit' : 'create-project-submit'}
|
|
174
263
|
>
|
|
175
|
-
{submitting ? '创建中…' : '创建项目'}
|
|
264
|
+
{submitting ? (isEdit ? '保存中…' : '创建中…') : isEdit ? '保存' : '创建项目'}
|
|
176
265
|
</button>
|
|
177
266
|
</div>
|
|
178
267
|
</div>
|
|
@@ -161,8 +161,8 @@ export function ApprovalPromptMenu({
|
|
|
161
161
|
</p>
|
|
162
162
|
)}
|
|
163
163
|
<p className="mt-2 text-[11px] leading-relaxed text-agent-muted-foreground">
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
默认在工作区沙箱中执行(只能写入当前项目或本对话工作区)。写入
|
|
165
|
+
Downloads 等工作区外路径会被系统拒绝;需要时在输入框把沙箱切到「完整权限」后再让
|
|
166
166
|
Agent 重试。
|
|
167
167
|
</p>
|
|
168
168
|
</>
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
} from 'react-icons/lu';
|
|
33
33
|
import type { LocalChat, LocalChatAgent } from '@/lib/local-api';
|
|
34
34
|
import type { ExecPolicy } from '@/lib/exec-policy';
|
|
35
|
+
import { getWebChatModes, hostToolChrome, settingsChrome } from '@/lib/host-tools';
|
|
35
36
|
import type { AttachmentFile } from '@/lib/attachments';
|
|
36
37
|
import type { SteerOutcome } from '@steerable/agent-ui';
|
|
37
38
|
import {
|
|
@@ -1112,8 +1113,11 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1112
1113
|
const trimmed = value.trim();
|
|
1113
1114
|
const canSend = (trimmed.length > 0 || actualFiles.length > 0) && !disabled && !isStreaming;
|
|
1114
1115
|
|
|
1116
|
+
const allowFileAttach = hostToolChrome('local-fs');
|
|
1117
|
+
|
|
1115
1118
|
const handleDragOver = (event: React.DragEvent) => {
|
|
1116
1119
|
event.preventDefault();
|
|
1120
|
+
if (!allowFileAttach) return;
|
|
1117
1121
|
if (event.dataTransfer.types.includes('Files') && !disabled) {
|
|
1118
1122
|
setIsDragging(true);
|
|
1119
1123
|
}
|
|
@@ -1143,7 +1147,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1143
1147
|
// 按钮点击 → 触发隐藏的 <input type="file" multiple>,由浏览器/Electron
|
|
1144
1148
|
// 直接弹出系统文件选择框(不依赖 IPC,避免「点击无反应」)。
|
|
1145
1149
|
const handlePickFiles = () => {
|
|
1146
|
-
if (disabled) return;
|
|
1150
|
+
if (disabled || !allowFileAttach) return;
|
|
1147
1151
|
fileInputRef.current?.click();
|
|
1148
1152
|
};
|
|
1149
1153
|
const handleFileInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
@@ -1158,7 +1162,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1158
1162
|
event.preventDefault();
|
|
1159
1163
|
setIsDragging(false);
|
|
1160
1164
|
|
|
1161
|
-
if (disabled) return;
|
|
1165
|
+
if (disabled || !allowFileAttach) return;
|
|
1162
1166
|
|
|
1163
1167
|
const droppedFiles = Array.from(event.dataTransfer.files);
|
|
1164
1168
|
if (droppedFiles.length > 0) {
|
|
@@ -1816,7 +1820,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1816
1820
|
Agent picker lives in the meta row above the box, next to
|
|
1817
1821
|
the project badge. min-w-0 允许窄屏时胶囊截断收缩。 */}
|
|
1818
1822
|
<div ref={toolbarLeftRef} className="flex min-w-0 items-center gap-1">
|
|
1819
|
-
{mode && onModeChange && (
|
|
1823
|
+
{mode && onModeChange && getWebChatModes().length > 1 && (
|
|
1820
1824
|
<ModeToggle
|
|
1821
1825
|
mode={mode}
|
|
1822
1826
|
disabled={disabled || isStreaming}
|
|
@@ -1831,6 +1835,8 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1831
1835
|
/>
|
|
1832
1836
|
)}
|
|
1833
1837
|
{toolbarExtras}
|
|
1838
|
+
{allowFileAttach && (
|
|
1839
|
+
<>
|
|
1834
1840
|
<button
|
|
1835
1841
|
type="button"
|
|
1836
1842
|
onClick={handlePickFiles}
|
|
@@ -1851,6 +1857,8 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1851
1857
|
tabIndex={-1}
|
|
1852
1858
|
onChange={handleFileInputChange}
|
|
1853
1859
|
/>
|
|
1860
|
+
</>
|
|
1861
|
+
)}
|
|
1854
1862
|
{onOpenSettings && (
|
|
1855
1863
|
<button
|
|
1856
1864
|
type="button"
|
|
@@ -2142,6 +2150,7 @@ function AgentSelect({
|
|
|
2142
2150
|
</button>
|
|
2143
2151
|
);
|
|
2144
2152
|
})}
|
|
2153
|
+
{settingsChrome('agents') && (
|
|
2145
2154
|
<button
|
|
2146
2155
|
type="button"
|
|
2147
2156
|
role="menuitem"
|
|
@@ -2155,6 +2164,7 @@ function AgentSelect({
|
|
|
2155
2164
|
<LuBot className="h-3.5 w-3.5 shrink-0" />
|
|
2156
2165
|
<span className="text-xs font-medium">管理智能体</span>
|
|
2157
2166
|
</button>
|
|
2167
|
+
)}
|
|
2158
2168
|
</div>
|
|
2159
2169
|
)}
|
|
2160
2170
|
</div>
|
|
@@ -6,8 +6,8 @@ import type { ExecPolicy } from '@/lib/exec-policy';
|
|
|
6
6
|
* ExecPolicyPicker — 输入框上的命令沙箱切换(类 Codex 底部权限档)。
|
|
7
7
|
*
|
|
8
8
|
* 切档立刻记住,下一轮才生效:当前回合的 `execSandbox` 已经随流发出。
|
|
9
|
-
* 工作区 = Seatbelt/bwrap
|
|
10
|
-
*
|
|
9
|
+
* 工作区 = Seatbelt/bwrap 只允许写项目家目录或本对话工作区;完整权限
|
|
10
|
+
* = 不下发命令沙箱,本机路径(如 Downloads)不再被 Operation not permitted 拦住。
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
const OPTIONS: Array<{
|
|
@@ -18,7 +18,8 @@ const OPTIONS: Array<{
|
|
|
18
18
|
{
|
|
19
19
|
id: 'workspace',
|
|
20
20
|
label: '工作区',
|
|
21
|
-
description:
|
|
21
|
+
description:
|
|
22
|
+
'命令只能写入当前项目或本对话工作区(文稿/<应用名>/conversations/);工作区外路径会被系统拒绝。',
|
|
22
23
|
},
|
|
23
24
|
{
|
|
24
25
|
id: 'full',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useState, type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
|
2
2
|
import { openLocalPath, type ResolvedLocalPath } from '@/lib/local-api';
|
|
3
|
+
import { hostToolChrome } from '@/lib/host-tools';
|
|
3
4
|
import { peekResolvedPath, subscribeResolvedPath } from './path-mentions';
|
|
4
5
|
import { splitTurnFilePath } from './turn-files';
|
|
5
6
|
|
|
@@ -27,12 +28,15 @@ export function FilePathCode({ candidate, chatId, children, ...rest }: FilePathC
|
|
|
27
28
|
);
|
|
28
29
|
const [openError, setOpenError] = useState<string | null>(null);
|
|
29
30
|
|
|
31
|
+
const allowOpenPath = hostToolChrome('local-fs');
|
|
32
|
+
|
|
30
33
|
useEffect(() => {
|
|
31
34
|
setOpenError(null);
|
|
35
|
+
if (!allowOpenPath) return;
|
|
32
36
|
return subscribeResolvedPath(candidate, chatId, (entry) => setResolved(entry));
|
|
33
|
-
}, [candidate, chatId]);
|
|
37
|
+
}, [allowOpenPath, candidate, chatId]);
|
|
34
38
|
|
|
35
|
-
if (!resolved) {
|
|
39
|
+
if (!resolved || !allowOpenPath) {
|
|
36
40
|
return (
|
|
37
41
|
<code {...rest} className={INLINE_CODE_CLASS}>
|
|
38
42
|
{children}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
LuLoaderCircle,
|
|
7
7
|
} from 'react-icons/lu';
|
|
8
8
|
import { openLocalPath } from '@/lib/local-api';
|
|
9
|
+
import { hostToolChrome } from '@/lib/host-tools';
|
|
9
10
|
import {
|
|
10
11
|
formatFileSize,
|
|
11
12
|
splitTurnFilePath,
|
|
@@ -30,10 +31,12 @@ interface TurnFilesCardProps {
|
|
|
30
31
|
export function TurnFilesCard({ files }: TurnFilesCardProps) {
|
|
31
32
|
const [openingPath, setOpeningPath] = useState<string | null>(null);
|
|
32
33
|
const [openError, setOpenError] = useState<{ path: string; message: string } | null>(null);
|
|
34
|
+
const allowOpenPath = hostToolChrome('local-fs');
|
|
33
35
|
|
|
34
36
|
if (files.length === 0) return null;
|
|
35
37
|
|
|
36
38
|
const handleOpen = async (file: TurnFile) => {
|
|
39
|
+
if (!allowOpenPath) return;
|
|
37
40
|
if (openingPath) return;
|
|
38
41
|
setOpeningPath(file.path);
|
|
39
42
|
setOpenError(null);
|
|
@@ -59,7 +62,11 @@ export function TurnFilesCard({ files }: TurnFilesCardProps) {
|
|
|
59
62
|
>
|
|
60
63
|
<div className="flex items-center gap-1.5 border-b border-agent-border/60 px-2.5 py-1 text-[11px] text-agent-muted-foreground">
|
|
61
64
|
<LuFiles className="h-3.5 w-3.5" />
|
|
62
|
-
<span
|
|
65
|
+
<span>
|
|
66
|
+
{allowOpenPath
|
|
67
|
+
? `本轮产生了 ${files.length} 个文件,点击打开`
|
|
68
|
+
: `本轮产生了 ${files.length} 个文件`}
|
|
69
|
+
</span>
|
|
63
70
|
</div>
|
|
64
71
|
<ul className="max-h-64 overflow-y-auto py-1">
|
|
65
72
|
{files.map((file) => {
|
|
@@ -72,9 +79,13 @@ export function TurnFilesCard({ files }: TurnFilesCardProps) {
|
|
|
72
79
|
<button
|
|
73
80
|
type="button"
|
|
74
81
|
onClick={() => void handleOpen(file)}
|
|
75
|
-
disabled={openingPath !== null}
|
|
82
|
+
disabled={!allowOpenPath || openingPath !== null}
|
|
76
83
|
title={file.path}
|
|
77
|
-
className=
|
|
84
|
+
className={`flex w-full items-center gap-2 px-2.5 py-1 text-left text-xs transition-colors ${
|
|
85
|
+
allowOpenPath
|
|
86
|
+
? 'hover:bg-agent-foreground/5 disabled:cursor-wait'
|
|
87
|
+
: 'cursor-default'
|
|
88
|
+
}`}
|
|
78
89
|
data-turn-file=""
|
|
79
90
|
data-kind={file.kind}
|
|
80
91
|
>
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
useChatsAndAgents,
|
|
16
16
|
type UseChatsAndAgentsResult,
|
|
17
17
|
} from '@/hooks/useChatsAndAgents';
|
|
18
|
+
import { hostToolChrome, sanitizeRightPanelKind, settingsChrome } from '@/lib/host-tools';
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* AgentLayout wraps `/agent`, `/agent/:chatId`, and `/` (default) with a
|
|
@@ -272,14 +273,16 @@ function AgentLayoutContent() {
|
|
|
272
273
|
try {
|
|
273
274
|
const saved = window.localStorage.getItem(RIGHT_PANEL_KEY);
|
|
274
275
|
if (saved !== null) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
276
|
+
const raw =
|
|
277
|
+
saved === 'terminal' || packChatSlots.some((s) => s.slotId === saved)
|
|
278
|
+
? saved
|
|
279
|
+
: null;
|
|
280
|
+
return sanitizeRightPanelKind(raw);
|
|
278
281
|
}
|
|
279
282
|
// 新 key 不存在时才迁移老版本仅持久化终端的旧 key。
|
|
280
|
-
return
|
|
281
|
-
? 'terminal'
|
|
282
|
-
|
|
283
|
+
return sanitizeRightPanelKind(
|
|
284
|
+
window.localStorage.getItem(TERMINAL_OPEN_KEY) === '1' ? 'terminal' : null,
|
|
285
|
+
);
|
|
283
286
|
} catch {
|
|
284
287
|
return null;
|
|
285
288
|
}
|
|
@@ -311,7 +314,8 @@ function AgentLayoutContent() {
|
|
|
311
314
|
打开任一面板即离开任务 dock(栏位同一时刻只呈现一个内容)。 */
|
|
312
315
|
const toggleRightPanel = useCallback(
|
|
313
316
|
(kind: string) => {
|
|
314
|
-
const
|
|
317
|
+
const raw: RightPanelState = rightPanelRef.current === kind ? null : kind;
|
|
318
|
+
const next = sanitizeRightPanelKind(raw);
|
|
315
319
|
rightPanelRef.current = next;
|
|
316
320
|
if (next !== null) setInspectedTask(null);
|
|
317
321
|
setRightPanelState(next);
|
|
@@ -363,6 +367,7 @@ function AgentLayoutContent() {
|
|
|
363
367
|
}, []);
|
|
364
368
|
|
|
365
369
|
const showTerminalFromTask = useCallback(() => {
|
|
370
|
+
if (!hostToolChrome('terminal')) return;
|
|
366
371
|
setInspectedTask(null);
|
|
367
372
|
rightPanelRef.current = 'terminal';
|
|
368
373
|
setRightPanelState('terminal');
|
|
@@ -499,7 +504,7 @@ function AgentLayoutContent() {
|
|
|
499
504
|
} satisfies AgentOutletContext}
|
|
500
505
|
/>
|
|
501
506
|
</div>
|
|
502
|
-
<InsightsConsentBanner />
|
|
507
|
+
{settingsChrome('insights') && <InsightsConsentBanner />}
|
|
503
508
|
</div>
|
|
504
509
|
{(rightPanel !== null || inspectedTask !== null) && (
|
|
505
510
|
<>
|
|
@@ -523,7 +528,9 @@ function AgentLayoutContent() {
|
|
|
523
528
|
<TaskProcessPanel
|
|
524
529
|
inspected={inspectedTask}
|
|
525
530
|
onClose={closeTaskProcess}
|
|
526
|
-
onShowTerminal={
|
|
531
|
+
onShowTerminal={
|
|
532
|
+
hostToolChrome('terminal') ? showTerminalFromTask : undefined
|
|
533
|
+
}
|
|
527
534
|
/>
|
|
528
535
|
) : rightPanel === 'terminal' ? (
|
|
529
536
|
<TerminalPanel
|
package/src/lib/exec-policy.ts
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* 会话级命令执行沙箱(类 Codex 的 sandbox_mode)。
|
|
3
3
|
*
|
|
4
4
|
* 作用于下一轮 `chat.stream` 的 `execSandbox`,不是 sidecar 进程沙箱
|
|
5
|
-
* (设置页那一层)。工作区 =
|
|
6
|
-
*
|
|
5
|
+
* (设置页那一层)。工作区 = 只能写入当前项目或本对话工作区;完整权限
|
|
6
|
+
* = 关闭逐条命令沙箱。工具审批(允许/拒绝)仍按原策略生效。
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { hostToolChrome } from './host-tools';
|
|
10
|
+
|
|
9
11
|
export type ExecPolicy = 'workspace' | 'full';
|
|
10
12
|
|
|
11
13
|
export const EXEC_POLICY_STORAGE_KEY = 'agent-exec-policy';
|
|
@@ -16,6 +18,7 @@ export function parseExecPolicy(value: unknown): ExecPolicy {
|
|
|
16
18
|
|
|
17
19
|
export function readStoredExecPolicy(): ExecPolicy {
|
|
18
20
|
if (typeof localStorage === 'undefined') return 'workspace';
|
|
21
|
+
if (!hostToolChrome('local-fs')) return 'workspace';
|
|
19
22
|
return parseExecPolicy(localStorage.getItem(EXEC_POLICY_STORAGE_KEY));
|
|
20
23
|
}
|
|
21
24
|
|