@steerable/agent-shell-web 0.6.31 → 0.6.32
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/components/AgentSidebar.tsx +257 -65
- package/src/components/CreateProjectModal.tsx +151 -64
- package/src/components/chat/ApprovalModal.tsx +2 -2
- package/src/components/chat/ChatProjectBadge.tsx +1 -1
- package/src/components/chat/ExecPolicyPicker.tsx +4 -3
- package/src/lib/exec-policy.ts +2 -2
- package/src/lib/http-bridge.ts +20 -7
- package/src/lib/local-api.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steerable/agent-shell-web",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.32",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Steerable framework — product-neutral renderer SPA source for @steerable/agent-shell (React + Vite). Consumed by product web entry points via the `@/` alias (anchored at this package's src/) and the createProductViteConfig factory. Published as a source package (src/), plus the compiled vite.base config factory (vite.base.js + .d.ts) for Node-side config loading.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"react-router-dom": "^6.30.0",
|
|
47
47
|
"rehype-raw": "^7.0.0",
|
|
48
48
|
"remark-gfm": "^4.0.1",
|
|
49
|
-
"@steerable/agent-protocol": "0.6.
|
|
50
|
-
"@steerable/agent-ui": "0.6.
|
|
49
|
+
"@steerable/agent-protocol": "0.6.32",
|
|
50
|
+
"@steerable/agent-ui": "0.6.32"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@tailwindcss/vite": "^4.3.0",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"typescript": "^5.8.3",
|
|
61
61
|
"vite": "^5.4.21",
|
|
62
62
|
"vitest": "^3.2.4",
|
|
63
|
-
"@steerable/pack-sdk": "0.6.
|
|
63
|
+
"@steerable/pack-sdk": "0.6.32"
|
|
64
64
|
},
|
|
65
65
|
"scripts": {
|
|
66
66
|
"gen": "echo agent-shell-web",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
* │ ⬡ Skill 设置 │ ← /settings?section=skills(独立页)
|
|
65
65
|
* │ 🔌 MCP 设置 │ ← /settings?section=mcp(独立页)
|
|
66
66
|
* │ 会话 v 📁+ │ ← 📁+ 打开新建项目弹窗
|
|
67
|
-
* │ v 📁 项目A · 3 (hover:
|
|
67
|
+
* │ v 📁 项目A · 3 (hover: +··)│ ← + 新建对话;·· 菜单:重命名/换目录/访达/删
|
|
68
68
|
* │ ...(项目内对话) │
|
|
69
69
|
* │ 今天 │
|
|
70
70
|
* │ ...(无项目对话,按日期分组) │ ← 无项目排在项目分组之后
|
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
*/
|
|
79
79
|
|
|
80
80
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
81
|
+
import { createPortal } from "react-dom";
|
|
81
82
|
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
|
82
83
|
import {
|
|
83
84
|
LuChevronDown,
|
|
@@ -97,6 +98,7 @@ import {
|
|
|
97
98
|
LuFolderPlus,
|
|
98
99
|
LuPencil,
|
|
99
100
|
LuPlug,
|
|
101
|
+
LuEllipsis,
|
|
100
102
|
} from "react-icons/lu";
|
|
101
103
|
import { parseChatTitle } from "@/lib/chat-title";
|
|
102
104
|
import { getDateGroupLabel, getDateGroupPriority } from "@/lib/date-groups";
|
|
@@ -105,6 +107,7 @@ import {
|
|
|
105
107
|
createProject,
|
|
106
108
|
deleteProject,
|
|
107
109
|
listProjects,
|
|
110
|
+
openLocalPath,
|
|
108
111
|
updateProject,
|
|
109
112
|
type LocalChat,
|
|
110
113
|
type LocalChatAgent,
|
|
@@ -141,6 +144,125 @@ function AgentDot({
|
|
|
141
144
|
);
|
|
142
145
|
}
|
|
143
146
|
|
|
147
|
+
function placeProjectMenu(anchor: HTMLElement): { top: number; left: number } {
|
|
148
|
+
const box = anchor.getBoundingClientRect();
|
|
149
|
+
const width = 224;
|
|
150
|
+
const left = Math.min(box.right + 4, window.innerWidth - width - 8);
|
|
151
|
+
return { top: Math.max(8, box.top), left };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function ProjectOverflowMenu({
|
|
155
|
+
project,
|
|
156
|
+
chatCount,
|
|
157
|
+
anchor,
|
|
158
|
+
revealLabel,
|
|
159
|
+
confirmDelete,
|
|
160
|
+
deleting,
|
|
161
|
+
onClose,
|
|
162
|
+
onRename,
|
|
163
|
+
onChangeFolder,
|
|
164
|
+
onReveal,
|
|
165
|
+
onDelete,
|
|
166
|
+
}: {
|
|
167
|
+
project: LocalProject;
|
|
168
|
+
chatCount: number;
|
|
169
|
+
anchor: HTMLElement;
|
|
170
|
+
revealLabel: string;
|
|
171
|
+
confirmDelete: boolean;
|
|
172
|
+
deleting: boolean;
|
|
173
|
+
onClose: () => void;
|
|
174
|
+
onRename: () => void;
|
|
175
|
+
onChangeFolder: () => void;
|
|
176
|
+
onReveal: () => void;
|
|
177
|
+
onDelete: () => void;
|
|
178
|
+
}) {
|
|
179
|
+
const pos = placeProjectMenu(anchor);
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
const onKey = (event: KeyboardEvent) => {
|
|
182
|
+
if (event.key === "Escape") onClose();
|
|
183
|
+
};
|
|
184
|
+
window.addEventListener("keydown", onKey);
|
|
185
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
186
|
+
}, [onClose]);
|
|
187
|
+
return createPortal(
|
|
188
|
+
<>
|
|
189
|
+
<div className="fixed inset-0 z-[90]" onClick={onClose} />
|
|
190
|
+
<div
|
|
191
|
+
role="menu"
|
|
192
|
+
className="fixed z-[91] w-56 overflow-hidden rounded-2xl border border-agent-border bg-agent-canvas p-1 shadow-lg"
|
|
193
|
+
style={pos}
|
|
194
|
+
data-testid="project-overflow-menu"
|
|
195
|
+
>
|
|
196
|
+
<div className="px-2 py-1.5">
|
|
197
|
+
<div className="flex items-center gap-2 text-xs font-medium text-agent-foreground">
|
|
198
|
+
<LuFolder className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
|
|
199
|
+
<span className="min-w-0 truncate">{project.name}</span>
|
|
200
|
+
</div>
|
|
201
|
+
<div className="mt-0.5 pl-[22px] text-[10px] text-agent-muted-foreground">
|
|
202
|
+
{chatCount} 个会话
|
|
203
|
+
</div>
|
|
204
|
+
<div
|
|
205
|
+
className="mt-0.5 truncate pl-[22px] font-mono text-[10px] text-agent-muted-foreground/70"
|
|
206
|
+
title={project.folderPath}
|
|
207
|
+
>
|
|
208
|
+
{project.folderPath}
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
<div className="mx-1 my-1 border-t border-agent-border/60" />
|
|
212
|
+
<button
|
|
213
|
+
type="button"
|
|
214
|
+
role="menuitem"
|
|
215
|
+
title="重命名项目"
|
|
216
|
+
onClick={onRename}
|
|
217
|
+
className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-xs text-agent-foreground hover:bg-agent-foreground/5"
|
|
218
|
+
>
|
|
219
|
+
<LuPencil className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
|
|
220
|
+
重命名
|
|
221
|
+
</button>
|
|
222
|
+
<button
|
|
223
|
+
type="button"
|
|
224
|
+
role="menuitem"
|
|
225
|
+
title="编辑项目"
|
|
226
|
+
onClick={onChangeFolder}
|
|
227
|
+
className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-xs text-agent-foreground hover:bg-agent-foreground/5"
|
|
228
|
+
>
|
|
229
|
+
<LuFolderOpen className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
|
|
230
|
+
编辑项目
|
|
231
|
+
</button>
|
|
232
|
+
<button
|
|
233
|
+
type="button"
|
|
234
|
+
role="menuitem"
|
|
235
|
+
title={revealLabel}
|
|
236
|
+
onClick={onReveal}
|
|
237
|
+
className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-xs text-agent-foreground hover:bg-agent-foreground/5"
|
|
238
|
+
>
|
|
239
|
+
<LuFolder className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
|
|
240
|
+
{revealLabel}
|
|
241
|
+
</button>
|
|
242
|
+
<div className="mx-1 my-1 border-t border-agent-border/60" />
|
|
243
|
+
<button
|
|
244
|
+
type="button"
|
|
245
|
+
role="menuitem"
|
|
246
|
+
disabled={deleting}
|
|
247
|
+
title={
|
|
248
|
+
confirmDelete
|
|
249
|
+
? "再次点击确认删除(会话会保留为无项目对话)"
|
|
250
|
+
: "删除项目(会话保留为无项目对话)"
|
|
251
|
+
}
|
|
252
|
+
onClick={onDelete}
|
|
253
|
+
className={`flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-xs hover:bg-agent-destructive/10 ${
|
|
254
|
+
confirmDelete ? "text-agent-destructive" : "text-agent-foreground"
|
|
255
|
+
}`}
|
|
256
|
+
>
|
|
257
|
+
<LuTrash2 className={`h-3.5 w-3.5 shrink-0 ${deleting ? "animate-pulse" : ""}`} />
|
|
258
|
+
{confirmDelete ? "再次点击确认删除" : "删除项目"}
|
|
259
|
+
</button>
|
|
260
|
+
</div>
|
|
261
|
+
</>,
|
|
262
|
+
document.body,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
144
266
|
interface AgentSidebarProps {
|
|
145
267
|
data: UseChatsAndAgentsResult;
|
|
146
268
|
/** 右侧栏当前打开的面板:null / 'terminal' / 包槽位 id(互斥)。 */
|
|
@@ -205,8 +327,8 @@ export function AgentSidebar({
|
|
|
205
327
|
|
|
206
328
|
// ───── 项目模式 ─────
|
|
207
329
|
// 项目列表从 local-backend 拉取(electron-store 持久化)。会话按
|
|
208
|
-
// projectId
|
|
209
|
-
//
|
|
330
|
+
// projectId 分组:项目分组在上(组头 hover:+ 新建对话 / ·· 菜单),
|
|
331
|
+
// 无项目对话在下(日期分组)。
|
|
210
332
|
const [projects, setProjects] = useState<LocalProject[]>([]);
|
|
211
333
|
const [collapsedProjectIds, setCollapsedProjectIds] = useState<Set<string>>(
|
|
212
334
|
() => new Set(),
|
|
@@ -223,6 +345,11 @@ export function AgentSidebar({
|
|
|
223
345
|
);
|
|
224
346
|
const [projectError, setProjectError] = useState<string | null>(null);
|
|
225
347
|
const [createProjectOpen, setCreateProjectOpen] = useState(false);
|
|
348
|
+
const [editingProjectId, setEditingProjectId] = useState<string | null>(null);
|
|
349
|
+
const [projectMenu, setProjectMenu] = useState<{
|
|
350
|
+
id: string;
|
|
351
|
+
anchor: HTMLElement;
|
|
352
|
+
} | null>(null);
|
|
226
353
|
|
|
227
354
|
const fetchProjects = useCallback(async () => {
|
|
228
355
|
if (!isElectron()) return;
|
|
@@ -274,48 +401,80 @@ export function AgentSidebar({
|
|
|
274
401
|
[renamingValue, fetchProjects],
|
|
275
402
|
);
|
|
276
403
|
|
|
277
|
-
const
|
|
278
|
-
async (
|
|
279
|
-
|
|
404
|
+
const handleUpdateProject = useCallback(
|
|
405
|
+
async (
|
|
406
|
+
projectId: string,
|
|
407
|
+
input: { name: string; sourceFolders: string[] },
|
|
408
|
+
) => {
|
|
280
409
|
setProjectError(null);
|
|
281
410
|
try {
|
|
282
|
-
|
|
283
|
-
|
|
411
|
+
await updateProject(projectId, {
|
|
412
|
+
name: input.name,
|
|
413
|
+
sourceFolders: input.sourceFolders,
|
|
284
414
|
});
|
|
285
|
-
if (!result || result.canceled || result.filePaths.length === 0) return;
|
|
286
|
-
await updateProject(projectId, { folderPath: result.filePaths[0] });
|
|
287
415
|
await fetchProjects();
|
|
288
416
|
} catch (err) {
|
|
289
|
-
|
|
417
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
418
|
+
setProjectError(message);
|
|
419
|
+
throw err instanceof Error ? err : new Error(message);
|
|
290
420
|
}
|
|
291
421
|
},
|
|
292
|
-
[
|
|
422
|
+
[fetchProjects],
|
|
293
423
|
);
|
|
294
424
|
|
|
295
|
-
const
|
|
425
|
+
const removeProject = useCallback(
|
|
296
426
|
async (projectId: string) => {
|
|
297
427
|
if (deletingProjectId === projectId) return;
|
|
298
|
-
// 与会话删除同款两段确认:第一次点击武装红色按钮,第二次才真删。
|
|
299
|
-
if (confirmDeleteProjectId !== projectId) {
|
|
300
|
-
setConfirmDeleteProjectId(projectId);
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
428
|
try {
|
|
304
429
|
setDeletingProjectId(projectId);
|
|
305
|
-
// 后端会把该项目下的会话降级为无项目对话(不删会话)。
|
|
306
430
|
await deleteProject(projectId);
|
|
307
431
|
setConfirmDeleteProjectId(null);
|
|
432
|
+
setProjectMenu(null);
|
|
433
|
+
setEditingProjectId(null);
|
|
308
434
|
await fetchProjects();
|
|
309
435
|
await data.refreshChats();
|
|
310
436
|
} catch (err) {
|
|
311
|
-
|
|
437
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
438
|
+
setProjectError(message);
|
|
439
|
+
throw err instanceof Error ? err : new Error(message);
|
|
312
440
|
} finally {
|
|
313
441
|
setDeletingProjectId(null);
|
|
314
442
|
}
|
|
315
443
|
},
|
|
316
|
-
[
|
|
444
|
+
[deletingProjectId, fetchProjects, data],
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
const handleDeleteProject = useCallback(
|
|
448
|
+
async (projectId: string) => {
|
|
449
|
+
// 与会话删除同款两段确认:第一次点击武装红色按钮,第二次才真删。
|
|
450
|
+
if (confirmDeleteProjectId !== projectId) {
|
|
451
|
+
setConfirmDeleteProjectId(projectId);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
await removeProject(projectId);
|
|
456
|
+
} catch {
|
|
457
|
+
// 横幅已写 projectError
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
[confirmDeleteProjectId, removeProject],
|
|
317
461
|
);
|
|
318
462
|
|
|
463
|
+
const closeProjectMenu = useCallback(() => {
|
|
464
|
+
setProjectMenu(null);
|
|
465
|
+
setConfirmDeleteProjectId(null);
|
|
466
|
+
}, []);
|
|
467
|
+
|
|
468
|
+
const handleRevealProjectFolder = useCallback(async (folderPath: string) => {
|
|
469
|
+
setProjectError(null);
|
|
470
|
+
try {
|
|
471
|
+
const res = await openLocalPath(folderPath);
|
|
472
|
+
if (!res.success && res.error) setProjectError(res.error);
|
|
473
|
+
} catch (err) {
|
|
474
|
+
setProjectError(err instanceof Error ? err.message : String(err));
|
|
475
|
+
}
|
|
476
|
+
}, []);
|
|
477
|
+
|
|
319
478
|
const toggleProjectCollapsed = useCallback((projectId: string) => {
|
|
320
479
|
setCollapsedProjectIds((prev) => {
|
|
321
480
|
const next = new Set(prev);
|
|
@@ -733,7 +892,7 @@ export function AgentSidebar({
|
|
|
733
892
|
</div>
|
|
734
893
|
) : (
|
|
735
894
|
<>
|
|
736
|
-
{/* 项目分组在前:组头可折叠,hover
|
|
895
|
+
{/* 项目分组在前:组头可折叠,hover 出 + 新建对话 / ·· 菜单 */}
|
|
737
896
|
{projectGroups.map(({ project, items }) => (
|
|
738
897
|
<div key={project.id} className="mb-1">
|
|
739
898
|
<div className="group/proj relative">
|
|
@@ -767,25 +926,30 @@ export function AgentSidebar({
|
|
|
767
926
|
className="flex w-full min-w-0 items-center px-2.5 pb-0.5 pt-1.5 text-[10px] font-medium uppercase tracking-wider text-agent-muted-foreground/80 transition-colors hover:text-agent-foreground"
|
|
768
927
|
title={`${project.name}\n${project.folderPath}`}
|
|
769
928
|
>
|
|
770
|
-
<
|
|
929
|
+
<LuFolder className="mr-1 h-3 w-3 shrink-0" />
|
|
930
|
+
<span className="min-w-0 truncate normal-case">
|
|
931
|
+
{project.name}
|
|
932
|
+
</span>
|
|
933
|
+
<span className="ml-0.5 shrink-0">
|
|
771
934
|
{collapsedProjectIds.has(project.id) ? (
|
|
772
935
|
<LuChevronDown className="h-3 w-3" />
|
|
773
936
|
) : (
|
|
774
937
|
<LuChevronUp className="h-3 w-3" />
|
|
775
938
|
)}
|
|
776
939
|
</span>
|
|
777
|
-
<LuFolder className="mr-1 h-3 w-3 shrink-0" />
|
|
778
|
-
<span className="min-w-0 truncate normal-case">
|
|
779
|
-
{project.name}
|
|
780
|
-
</span>
|
|
781
940
|
{items.length > 0 && (
|
|
782
941
|
<span className="ml-1 shrink-0 font-normal">
|
|
783
942
|
· {items.length}
|
|
784
943
|
</span>
|
|
785
944
|
)}
|
|
786
945
|
</button>
|
|
787
|
-
|
|
788
|
-
|
|
946
|
+
<div
|
|
947
|
+
className={`absolute right-1 top-1/2 flex -translate-y-1/2 items-center gap-0.5 transition-opacity ${
|
|
948
|
+
projectMenu?.id === project.id
|
|
949
|
+
? "opacity-100"
|
|
950
|
+
: "opacity-0 group-hover/proj:opacity-100"
|
|
951
|
+
}`}
|
|
952
|
+
>
|
|
789
953
|
<button
|
|
790
954
|
type="button"
|
|
791
955
|
onClick={() => handleOpenNewChat(project.id)}
|
|
@@ -796,45 +960,21 @@ export function AgentSidebar({
|
|
|
796
960
|
</button>
|
|
797
961
|
<button
|
|
798
962
|
type="button"
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
963
|
+
aria-label="项目菜单"
|
|
964
|
+
aria-expanded={projectMenu?.id === project.id}
|
|
965
|
+
onClick={(event) => {
|
|
966
|
+
const button = event.currentTarget;
|
|
967
|
+
setConfirmDeleteProjectId(null);
|
|
968
|
+
setProjectMenu((current) =>
|
|
969
|
+
current?.id === project.id
|
|
970
|
+
? null
|
|
971
|
+
: { id: project.id, anchor: button },
|
|
972
|
+
);
|
|
802
973
|
}}
|
|
803
974
|
className="flex h-5 w-5 items-center justify-center rounded-full text-agent-muted-foreground hover:bg-agent-foreground/10 hover:text-agent-foreground"
|
|
804
|
-
title="
|
|
975
|
+
title="项目菜单"
|
|
805
976
|
>
|
|
806
|
-
<
|
|
807
|
-
</button>
|
|
808
|
-
<button
|
|
809
|
-
type="button"
|
|
810
|
-
onClick={() =>
|
|
811
|
-
void handleChangeProjectFolder(project.id)
|
|
812
|
-
}
|
|
813
|
-
className="flex h-5 w-5 items-center justify-center rounded-full text-agent-muted-foreground hover:bg-agent-foreground/10 hover:text-agent-foreground"
|
|
814
|
-
title="更换绑定文件夹"
|
|
815
|
-
>
|
|
816
|
-
<LuFolderOpen className="h-3 w-3" />
|
|
817
|
-
</button>
|
|
818
|
-
<button
|
|
819
|
-
type="button"
|
|
820
|
-
onClick={() =>
|
|
821
|
-
void handleDeleteProject(project.id)
|
|
822
|
-
}
|
|
823
|
-
disabled={deletingProjectId === project.id}
|
|
824
|
-
className={`flex h-5 w-5 items-center justify-center rounded-full transition-colors ${
|
|
825
|
-
confirmDeleteProjectId === project.id
|
|
826
|
-
? "bg-agent-destructive/10 text-agent-destructive hover:bg-agent-destructive/20"
|
|
827
|
-
: "text-agent-muted-foreground hover:bg-agent-foreground/10 hover:text-agent-destructive"
|
|
828
|
-
}`}
|
|
829
|
-
title={
|
|
830
|
-
confirmDeleteProjectId === project.id
|
|
831
|
-
? "再次点击确认删除(会话会保留为无项目对话)"
|
|
832
|
-
: "删除项目(会话保留为无项目对话)"
|
|
833
|
-
}
|
|
834
|
-
>
|
|
835
|
-
<LuTrash2
|
|
836
|
-
className={`h-3 w-3 ${deletingProjectId === project.id ? "animate-pulse" : ""}`}
|
|
837
|
-
/>
|
|
977
|
+
<LuEllipsis className="h-3 w-3" />
|
|
838
978
|
</button>
|
|
839
979
|
</div>
|
|
840
980
|
</>
|
|
@@ -972,11 +1112,63 @@ export function AgentSidebar({
|
|
|
972
1112
|
</button>
|
|
973
1113
|
</div>
|
|
974
1114
|
|
|
1115
|
+
{projectMenu &&
|
|
1116
|
+
(() => {
|
|
1117
|
+
const project = projects.find((item) => item.id === projectMenu.id);
|
|
1118
|
+
if (!project) return null;
|
|
1119
|
+
const chatCount =
|
|
1120
|
+
projectGroups.find((group) => group.project.id === project.id)
|
|
1121
|
+
?.items.length ?? 0;
|
|
1122
|
+
return (
|
|
1123
|
+
<ProjectOverflowMenu
|
|
1124
|
+
project={project}
|
|
1125
|
+
chatCount={chatCount}
|
|
1126
|
+
anchor={projectMenu.anchor}
|
|
1127
|
+
revealLabel={isMac ? "在访达中显示" : "在文件管理器中显示"}
|
|
1128
|
+
confirmDelete={confirmDeleteProjectId === project.id}
|
|
1129
|
+
deleting={deletingProjectId === project.id}
|
|
1130
|
+
onClose={closeProjectMenu}
|
|
1131
|
+
onRename={() => {
|
|
1132
|
+
setProjectMenu(null);
|
|
1133
|
+
setRenamingProjectId(project.id);
|
|
1134
|
+
setRenamingValue(project.name);
|
|
1135
|
+
}}
|
|
1136
|
+
onChangeFolder={() => {
|
|
1137
|
+
setProjectMenu(null);
|
|
1138
|
+
setEditingProjectId(project.id);
|
|
1139
|
+
}}
|
|
1140
|
+
onReveal={() => {
|
|
1141
|
+
setProjectMenu(null);
|
|
1142
|
+
void handleRevealProjectFolder(project.folderPath);
|
|
1143
|
+
}}
|
|
1144
|
+
onDelete={() => void handleDeleteProject(project.id)}
|
|
1145
|
+
/>
|
|
1146
|
+
);
|
|
1147
|
+
})()}
|
|
1148
|
+
|
|
975
1149
|
<CreateProjectModal
|
|
976
1150
|
open={createProjectOpen}
|
|
977
1151
|
onClose={() => setCreateProjectOpen(false)}
|
|
978
1152
|
onCreate={handleCreateProject}
|
|
979
1153
|
/>
|
|
1154
|
+
{editingProjectId &&
|
|
1155
|
+
(() => {
|
|
1156
|
+
const project = projects.find((item) => item.id === editingProjectId);
|
|
1157
|
+
if (!project) return null;
|
|
1158
|
+
return (
|
|
1159
|
+
<CreateProjectModal
|
|
1160
|
+
open
|
|
1161
|
+
mode="edit"
|
|
1162
|
+
initial={{
|
|
1163
|
+
name: project.name,
|
|
1164
|
+
sourceFolders: project.sourceFolders ?? [],
|
|
1165
|
+
}}
|
|
1166
|
+
onClose={() => setEditingProjectId(null)}
|
|
1167
|
+
onCreate={(input) => handleUpdateProject(project.id, input)}
|
|
1168
|
+
onDelete={() => removeProject(project.id)}
|
|
1169
|
+
/>
|
|
1170
|
+
);
|
|
1171
|
+
})()}
|
|
980
1172
|
</div>
|
|
981
1173
|
);
|
|
982
1174
|
}
|
|
@@ -1,56 +1,96 @@
|
|
|
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';
|
|
65
|
+
|
|
66
|
+
const addSourceFolder = (folder: string) => {
|
|
67
|
+
const next = folder.trim();
|
|
68
|
+
if (!next) return;
|
|
69
|
+
setSourceFolders((prev) => (prev.includes(next) ? prev : [...prev, next]));
|
|
70
|
+
setFolderDraft('');
|
|
71
|
+
setError(null);
|
|
72
|
+
};
|
|
41
73
|
|
|
42
74
|
const handleAddFolder = async () => {
|
|
43
|
-
|
|
75
|
+
const typed = folderDraft.trim();
|
|
76
|
+
if (typed) {
|
|
77
|
+
addSourceFolder(typed);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
44
80
|
const result = await getElectronBridge()?.local?.selectDirectory({
|
|
45
81
|
title: '添加源文件夹',
|
|
46
82
|
});
|
|
47
|
-
if (!result || result.canceled || result.filePaths.length === 0)
|
|
48
|
-
|
|
49
|
-
|
|
83
|
+
if (!result || result.canceled || result.filePaths.length === 0) {
|
|
84
|
+
if (!getElectronBridge()?.local?.selectDirectory) {
|
|
85
|
+
setError('请输入文件夹路径');
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
addSourceFolder(result.filePaths[0]);
|
|
50
90
|
};
|
|
51
91
|
|
|
52
92
|
const handleSubmit = async () => {
|
|
53
|
-
if (!trimmed || submitting) return;
|
|
93
|
+
if (!trimmed || submitting || deleting) return;
|
|
54
94
|
setSubmitting(true);
|
|
55
95
|
setError(null);
|
|
56
96
|
try {
|
|
@@ -62,6 +102,23 @@ export function CreateProjectModal({ open, onClose, onCreate }: CreateProjectMod
|
|
|
62
102
|
}
|
|
63
103
|
};
|
|
64
104
|
|
|
105
|
+
const handleDelete = async () => {
|
|
106
|
+
if (!onDelete || deleting || submitting) return;
|
|
107
|
+
if (!confirmDelete) {
|
|
108
|
+
setConfirmDelete(true);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
setDeleting(true);
|
|
112
|
+
setError(null);
|
|
113
|
+
try {
|
|
114
|
+
await onDelete();
|
|
115
|
+
onClose();
|
|
116
|
+
} catch (err) {
|
|
117
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
118
|
+
setDeleting(false);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
65
122
|
return createPortal(
|
|
66
123
|
<div
|
|
67
124
|
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/50 backdrop-blur-sm"
|
|
@@ -70,13 +127,13 @@ export function CreateProjectModal({ open, onClose, onCreate }: CreateProjectMod
|
|
|
70
127
|
}}
|
|
71
128
|
role="dialog"
|
|
72
129
|
aria-modal="true"
|
|
73
|
-
aria-labelledby=
|
|
74
|
-
data-testid=
|
|
130
|
+
aria-labelledby={titleId}
|
|
131
|
+
data-testid={isEdit ? 'edit-project-dialog' : 'create-project-dialog'}
|
|
75
132
|
>
|
|
76
133
|
<div className="flex w-[440px] max-w-[92vw] flex-col overflow-hidden rounded-2xl border border-agent-border bg-agent-canvas shadow-2xl">
|
|
77
134
|
<div className="flex items-center justify-between px-5 pb-1 pt-4">
|
|
78
|
-
<h2 id=
|
|
79
|
-
新建项目
|
|
135
|
+
<h2 id={titleId} className="text-base font-semibold text-agent-foreground">
|
|
136
|
+
{isEdit ? '编辑项目' : '新建项目'}
|
|
80
137
|
</h2>
|
|
81
138
|
<button
|
|
82
139
|
type="button"
|
|
@@ -101,52 +158,64 @@ export function CreateProjectModal({ open, onClose, onCreate }: CreateProjectMod
|
|
|
101
158
|
placeholder="项目名称"
|
|
102
159
|
autoFocus
|
|
103
160
|
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=
|
|
161
|
+
data-testid={isEdit ? 'edit-project-name' : 'create-project-name'}
|
|
105
162
|
/>
|
|
106
163
|
</label>
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
164
|
+
{!isEdit && (
|
|
165
|
+
<p className="px-1 text-[11px] text-agent-muted-foreground">
|
|
166
|
+
默认位置:{homePreview}
|
|
167
|
+
</p>
|
|
168
|
+
)}
|
|
110
169
|
|
|
111
170
|
<div>
|
|
112
171
|
<div className="mb-2 text-sm text-agent-foreground">源文件夹</div>
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
172
|
+
<div className="overflow-hidden rounded-2xl border border-agent-border">
|
|
173
|
+
{sourceFolders.map((folder) => (
|
|
174
|
+
<div
|
|
175
|
+
key={folder}
|
|
176
|
+
className="flex items-center gap-2 border-b border-agent-border/60 px-3 py-2 text-xs text-agent-foreground"
|
|
177
|
+
>
|
|
178
|
+
<LuFolder className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
|
|
179
|
+
<span className="min-w-0 flex-1 truncate font-mono" title={folder}>
|
|
180
|
+
{folder}
|
|
181
|
+
</span>
|
|
182
|
+
<button
|
|
183
|
+
type="button"
|
|
184
|
+
onClick={() =>
|
|
185
|
+
setSourceFolders((prev) => prev.filter((item) => item !== folder))
|
|
186
|
+
}
|
|
187
|
+
className="rounded-full p-0.5 text-agent-muted-foreground hover:text-agent-foreground"
|
|
188
|
+
aria-label={`移除 ${folder}`}
|
|
119
189
|
>
|
|
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>
|
|
190
|
+
<LuX className="h-3.5 w-3.5" />
|
|
191
|
+
</button>
|
|
192
|
+
</div>
|
|
193
|
+
))}
|
|
194
|
+
<div className="flex items-center gap-2 px-3 py-2">
|
|
195
|
+
<LuFolderPlus className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
|
|
196
|
+
<input
|
|
197
|
+
type="text"
|
|
198
|
+
value={folderDraft}
|
|
199
|
+
onChange={(event) => setFolderDraft(event.target.value)}
|
|
200
|
+
onKeyDown={(event) => {
|
|
201
|
+
if (event.key === 'Enter') {
|
|
202
|
+
event.preventDefault();
|
|
203
|
+
void handleAddFolder();
|
|
204
|
+
}
|
|
205
|
+
}}
|
|
206
|
+
placeholder="输入路径或点添加浏览"
|
|
207
|
+
className="h-7 min-w-0 flex-1 bg-transparent text-xs text-agent-foreground outline-none placeholder:text-agent-muted-foreground"
|
|
208
|
+
data-testid={isEdit ? 'edit-project-folder-path' : 'create-project-folder-path'}
|
|
209
|
+
/>
|
|
210
|
+
<button
|
|
211
|
+
type="button"
|
|
212
|
+
onClick={() => void handleAddFolder()}
|
|
213
|
+
className="shrink-0 rounded-full px-2 py-1 text-xs font-medium text-agent-foreground hover:bg-agent-muted"
|
|
214
|
+
data-testid={isEdit ? 'edit-project-add-folder' : 'create-project-add-folder'}
|
|
215
|
+
>
|
|
216
|
+
添加
|
|
217
|
+
</button>
|
|
218
|
+
</div>
|
|
150
219
|
</div>
|
|
151
220
|
</div>
|
|
152
221
|
|
|
@@ -157,22 +226,40 @@ export function CreateProjectModal({ open, onClose, onCreate }: CreateProjectMod
|
|
|
157
226
|
)}
|
|
158
227
|
</div>
|
|
159
228
|
|
|
160
|
-
<div className="flex items-center
|
|
229
|
+
<div className="flex items-center gap-2 px-5 pb-4 pt-1">
|
|
230
|
+
{onDelete && (
|
|
231
|
+
<button
|
|
232
|
+
type="button"
|
|
233
|
+
onClick={() => void handleDelete()}
|
|
234
|
+
disabled={deleting || submitting}
|
|
235
|
+
title={
|
|
236
|
+
confirmDelete
|
|
237
|
+
? '再次点击确认删除(会话会保留为无项目对话)'
|
|
238
|
+
: '删除项目(会话保留为无项目对话)'
|
|
239
|
+
}
|
|
240
|
+
className="mr-auto h-9 rounded-full px-3 text-sm text-agent-destructive transition-colors hover:bg-agent-destructive/10 disabled:opacity-40"
|
|
241
|
+
data-testid="edit-project-delete"
|
|
242
|
+
>
|
|
243
|
+
{confirmDelete ? '再次点击确认删除' : '删除本地项目'}
|
|
244
|
+
</button>
|
|
245
|
+
)}
|
|
161
246
|
<button
|
|
162
247
|
type="button"
|
|
163
248
|
onClick={onClose}
|
|
164
|
-
className=
|
|
249
|
+
className={`h-9 rounded-full px-4 text-sm text-agent-muted-foreground transition-colors hover:bg-agent-muted hover:text-agent-foreground ${
|
|
250
|
+
onDelete ? '' : 'ml-auto'
|
|
251
|
+
}`}
|
|
165
252
|
>
|
|
166
253
|
取消
|
|
167
254
|
</button>
|
|
168
255
|
<button
|
|
169
256
|
type="button"
|
|
170
257
|
onClick={() => void handleSubmit()}
|
|
171
|
-
disabled={!trimmed || submitting}
|
|
258
|
+
disabled={!trimmed || submitting || deleting}
|
|
172
259
|
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=
|
|
260
|
+
data-testid={isEdit ? 'edit-project-submit' : 'create-project-submit'}
|
|
174
261
|
>
|
|
175
|
-
{submitting ? '创建中…' : '创建项目'}
|
|
262
|
+
{submitting ? (isEdit ? '保存中…' : '创建中…') : isEdit ? '保存' : '创建项目'}
|
|
176
263
|
</button>
|
|
177
264
|
</div>
|
|
178
265
|
</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
|
</>
|
|
@@ -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',
|
package/src/lib/exec-policy.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* 会话级命令执行沙箱(类 Codex 的 sandbox_mode)。
|
|
3
3
|
*
|
|
4
4
|
* 作用于下一轮 `chat.stream` 的 `execSandbox`,不是 sidecar 进程沙箱
|
|
5
|
-
* (设置页那一层)。工作区 =
|
|
6
|
-
*
|
|
5
|
+
* (设置页那一层)。工作区 = 只能写入当前项目或本对话工作区;完整权限
|
|
6
|
+
* = 关闭逐条命令沙箱。工具审批(允许/拒绝)仍按原策略生效。
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export type ExecPolicy = 'workspace' | 'full';
|
package/src/lib/http-bridge.ts
CHANGED
|
@@ -44,6 +44,15 @@ function authHeaders(): Record<string, string> {
|
|
|
44
44
|
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function httpErrorMessage(data: unknown, status: number): string {
|
|
48
|
+
if (data && typeof data === 'object') {
|
|
49
|
+
const rec = data as { error?: unknown; detail?: unknown };
|
|
50
|
+
if (typeof rec.error === 'string' && rec.error.trim()) return rec.error;
|
|
51
|
+
if (typeof rec.detail === 'string' && rec.detail.trim()) return rec.detail;
|
|
52
|
+
}
|
|
53
|
+
return `Request failed (${status})`;
|
|
54
|
+
}
|
|
55
|
+
|
|
47
56
|
async function http<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
48
57
|
const res = await fetch(path, {
|
|
49
58
|
method,
|
|
@@ -53,11 +62,9 @@ async function http<T>(method: string, path: string, body?: unknown): Promise<T>
|
|
|
53
62
|
},
|
|
54
63
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
55
64
|
});
|
|
56
|
-
const data = (await res.json().catch(() => null)) as T
|
|
65
|
+
const data = (await res.json().catch(() => null)) as T;
|
|
57
66
|
if (!res.ok) {
|
|
58
|
-
const err = new Error(
|
|
59
|
-
(data as { detail?: string } | null)?.detail || `Request failed (${res.status})`,
|
|
60
|
-
) as Error & { status?: number };
|
|
67
|
+
const err = new Error(httpErrorMessage(data, res.status)) as Error & { status?: number };
|
|
61
68
|
err.status = res.status;
|
|
62
69
|
throw err;
|
|
63
70
|
}
|
|
@@ -197,9 +204,15 @@ export function createHttpBridge(): ElectronBridge {
|
|
|
197
204
|
platform: boot.platform,
|
|
198
205
|
|
|
199
206
|
local: {
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
|
|
207
|
+
// 浏览器 File System Access API 不给真实路径。改走宿主
|
|
208
|
+
// `/host/local/select-directory`(本机系统选择器);取消或无 GUI
|
|
209
|
+
// 时返回 canceled,UI 再走手动输入路径。
|
|
210
|
+
selectDirectory: async (options) =>
|
|
211
|
+
http<{ canceled: boolean; filePaths: string[] }>(
|
|
212
|
+
'POST',
|
|
213
|
+
'/host/local/select-directory',
|
|
214
|
+
options ?? {},
|
|
215
|
+
),
|
|
203
216
|
saveTextFile: async (options) => {
|
|
204
217
|
const blob = new Blob([options.content], { type: 'application/json' });
|
|
205
218
|
const url = URL.createObjectURL(blob);
|
package/src/lib/local-api.ts
CHANGED
|
@@ -353,7 +353,7 @@ export interface ResolvedLocalPath {
|
|
|
353
353
|
|
|
354
354
|
/**
|
|
355
355
|
* 批量确认正文里提到的路径是否真实存在(用于决定行内代码要不要变成可点击)。
|
|
356
|
-
*
|
|
356
|
+
* 相对路径由后端按对话绑定的项目根解析,无项目时按对话工作区;不存在的候选不回。
|
|
357
357
|
*/
|
|
358
358
|
export async function resolveLocalPaths(candidates: string[], chatId?: string | null) {
|
|
359
359
|
return bridge().localBackend.request<{ resolved: ResolvedLocalPath[] }>({
|
|
@@ -489,7 +489,7 @@ export async function createProject(input: {
|
|
|
489
489
|
|
|
490
490
|
export async function updateProject(
|
|
491
491
|
projectId: string,
|
|
492
|
-
updates: { name?: string; folderPath?: string },
|
|
492
|
+
updates: { name?: string; folderPath?: string; sourceFolders?: string[] },
|
|
493
493
|
) {
|
|
494
494
|
return bridge().localBackend.request<{ success: boolean; project: LocalProject }>({
|
|
495
495
|
method: 'PUT',
|