@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
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.33",
|
|
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",
|
|
@@ -15,10 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"exports": {
|
|
17
17
|
"./auth/gate": "./src/auth/gate.ts",
|
|
18
|
-
"./vite.base":
|
|
19
|
-
"types": "./vite.base.d.ts",
|
|
20
|
-
"default": "./vite.base.js"
|
|
21
|
-
},
|
|
18
|
+
"./vite.base": "./vite.base.ts",
|
|
22
19
|
"./package.json": "./package.json"
|
|
23
20
|
},
|
|
24
21
|
"files": [
|
|
@@ -46,8 +43,8 @@
|
|
|
46
43
|
"react-router-dom": "^6.30.0",
|
|
47
44
|
"rehype-raw": "^7.0.0",
|
|
48
45
|
"remark-gfm": "^4.0.1",
|
|
49
|
-
"@steerable/agent-protocol": "0.6.
|
|
50
|
-
"@steerable/agent-ui": "0.6.
|
|
46
|
+
"@steerable/agent-protocol": "0.6.33",
|
|
47
|
+
"@steerable/agent-ui": "0.6.33"
|
|
51
48
|
},
|
|
52
49
|
"devDependencies": {
|
|
53
50
|
"@tailwindcss/vite": "^4.3.0",
|
|
@@ -60,7 +57,7 @@
|
|
|
60
57
|
"typescript": "^5.8.3",
|
|
61
58
|
"vite": "^5.4.21",
|
|
62
59
|
"vitest": "^3.2.4",
|
|
63
|
-
"@steerable/pack-sdk": "0.6.
|
|
60
|
+
"@steerable/pack-sdk": "0.6.33"
|
|
64
61
|
},
|
|
65
62
|
"scripts": {
|
|
66
63
|
"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,14 +98,17 @@ 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";
|
|
103
105
|
import { getElectronBridge, isElectron } from "@/lib/electron-bridge";
|
|
106
|
+
import { hasGeneralSettingsChrome, hostToolChrome, settingsChrome } from "@/lib/host-tools";
|
|
104
107
|
import {
|
|
105
108
|
createProject,
|
|
106
109
|
deleteProject,
|
|
107
110
|
listProjects,
|
|
111
|
+
openLocalPath,
|
|
108
112
|
updateProject,
|
|
109
113
|
type LocalChat,
|
|
110
114
|
type LocalChatAgent,
|
|
@@ -141,6 +145,127 @@ function AgentDot({
|
|
|
141
145
|
);
|
|
142
146
|
}
|
|
143
147
|
|
|
148
|
+
function placeProjectMenu(anchor: HTMLElement): { top: number; left: number } {
|
|
149
|
+
const box = anchor.getBoundingClientRect();
|
|
150
|
+
const width = 224;
|
|
151
|
+
const left = Math.min(box.right + 4, window.innerWidth - width - 8);
|
|
152
|
+
return { top: Math.max(8, box.top), left };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function ProjectOverflowMenu({
|
|
156
|
+
project,
|
|
157
|
+
chatCount,
|
|
158
|
+
anchor,
|
|
159
|
+
revealLabel,
|
|
160
|
+
confirmDelete,
|
|
161
|
+
deleting,
|
|
162
|
+
onClose,
|
|
163
|
+
onRename,
|
|
164
|
+
onChangeFolder,
|
|
165
|
+
onReveal,
|
|
166
|
+
onDelete,
|
|
167
|
+
}: {
|
|
168
|
+
project: LocalProject;
|
|
169
|
+
chatCount: number;
|
|
170
|
+
anchor: HTMLElement;
|
|
171
|
+
revealLabel: string;
|
|
172
|
+
confirmDelete: boolean;
|
|
173
|
+
deleting: boolean;
|
|
174
|
+
onClose: () => void;
|
|
175
|
+
onRename: () => void;
|
|
176
|
+
onChangeFolder: () => void;
|
|
177
|
+
onReveal?: () => void;
|
|
178
|
+
onDelete: () => void;
|
|
179
|
+
}) {
|
|
180
|
+
const pos = placeProjectMenu(anchor);
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
const onKey = (event: KeyboardEvent) => {
|
|
183
|
+
if (event.key === "Escape") onClose();
|
|
184
|
+
};
|
|
185
|
+
window.addEventListener("keydown", onKey);
|
|
186
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
187
|
+
}, [onClose]);
|
|
188
|
+
return createPortal(
|
|
189
|
+
<>
|
|
190
|
+
<div className="fixed inset-0 z-[90]" onClick={onClose} />
|
|
191
|
+
<div
|
|
192
|
+
role="menu"
|
|
193
|
+
className="fixed z-[91] w-56 overflow-hidden rounded-2xl border border-agent-border bg-agent-canvas p-1 shadow-lg"
|
|
194
|
+
style={pos}
|
|
195
|
+
data-testid="project-overflow-menu"
|
|
196
|
+
>
|
|
197
|
+
<div className="px-2 py-1.5">
|
|
198
|
+
<div className="flex items-center gap-2 text-xs font-medium text-agent-foreground">
|
|
199
|
+
<LuFolder className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
|
|
200
|
+
<span className="min-w-0 truncate">{project.name}</span>
|
|
201
|
+
</div>
|
|
202
|
+
<div className="mt-0.5 pl-[22px] text-[10px] text-agent-muted-foreground">
|
|
203
|
+
{chatCount} 个会话
|
|
204
|
+
</div>
|
|
205
|
+
<div
|
|
206
|
+
className="mt-0.5 truncate pl-[22px] font-mono text-[10px] text-agent-muted-foreground/70"
|
|
207
|
+
title={project.folderPath}
|
|
208
|
+
>
|
|
209
|
+
{project.folderPath}
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
<div className="mx-1 my-1 border-t border-agent-border/60" />
|
|
213
|
+
<button
|
|
214
|
+
type="button"
|
|
215
|
+
role="menuitem"
|
|
216
|
+
title="重命名项目"
|
|
217
|
+
onClick={onRename}
|
|
218
|
+
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"
|
|
219
|
+
>
|
|
220
|
+
<LuPencil className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
|
|
221
|
+
重命名
|
|
222
|
+
</button>
|
|
223
|
+
<button
|
|
224
|
+
type="button"
|
|
225
|
+
role="menuitem"
|
|
226
|
+
title="编辑项目"
|
|
227
|
+
onClick={onChangeFolder}
|
|
228
|
+
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"
|
|
229
|
+
>
|
|
230
|
+
<LuFolderOpen className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
|
|
231
|
+
编辑项目
|
|
232
|
+
</button>
|
|
233
|
+
{onReveal && (
|
|
234
|
+
<button
|
|
235
|
+
type="button"
|
|
236
|
+
role="menuitem"
|
|
237
|
+
title={revealLabel}
|
|
238
|
+
onClick={onReveal}
|
|
239
|
+
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"
|
|
240
|
+
>
|
|
241
|
+
<LuFolder className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
|
|
242
|
+
{revealLabel}
|
|
243
|
+
</button>
|
|
244
|
+
)}
|
|
245
|
+
<div className="mx-1 my-1 border-t border-agent-border/60" />
|
|
246
|
+
<button
|
|
247
|
+
type="button"
|
|
248
|
+
role="menuitem"
|
|
249
|
+
disabled={deleting}
|
|
250
|
+
title={
|
|
251
|
+
confirmDelete
|
|
252
|
+
? "再次点击确认删除(会话会保留为无项目对话)"
|
|
253
|
+
: "删除项目(会话保留为无项目对话)"
|
|
254
|
+
}
|
|
255
|
+
onClick={onDelete}
|
|
256
|
+
className={`flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-xs hover:bg-agent-destructive/10 ${
|
|
257
|
+
confirmDelete ? "text-agent-destructive" : "text-agent-foreground"
|
|
258
|
+
}`}
|
|
259
|
+
>
|
|
260
|
+
<LuTrash2 className={`h-3.5 w-3.5 shrink-0 ${deleting ? "animate-pulse" : ""}`} />
|
|
261
|
+
{confirmDelete ? "再次点击确认删除" : "删除项目"}
|
|
262
|
+
</button>
|
|
263
|
+
</div>
|
|
264
|
+
</>,
|
|
265
|
+
document.body,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
144
269
|
interface AgentSidebarProps {
|
|
145
270
|
data: UseChatsAndAgentsResult;
|
|
146
271
|
/** 右侧栏当前打开的面板:null / 'terminal' / 包槽位 id(互斥)。 */
|
|
@@ -205,8 +330,8 @@ export function AgentSidebar({
|
|
|
205
330
|
|
|
206
331
|
// ───── 项目模式 ─────
|
|
207
332
|
// 项目列表从 local-backend 拉取(electron-store 持久化)。会话按
|
|
208
|
-
// projectId
|
|
209
|
-
//
|
|
333
|
+
// projectId 分组:项目分组在上(组头 hover:+ 新建对话 / ·· 菜单),
|
|
334
|
+
// 无项目对话在下(日期分组)。
|
|
210
335
|
const [projects, setProjects] = useState<LocalProject[]>([]);
|
|
211
336
|
const [collapsedProjectIds, setCollapsedProjectIds] = useState<Set<string>>(
|
|
212
337
|
() => new Set(),
|
|
@@ -223,9 +348,16 @@ export function AgentSidebar({
|
|
|
223
348
|
);
|
|
224
349
|
const [projectError, setProjectError] = useState<string | null>(null);
|
|
225
350
|
const [createProjectOpen, setCreateProjectOpen] = useState(false);
|
|
351
|
+
const [editingProjectId, setEditingProjectId] = useState<string | null>(null);
|
|
352
|
+
const [projectMenu, setProjectMenu] = useState<{
|
|
353
|
+
id: string;
|
|
354
|
+
anchor: HTMLElement;
|
|
355
|
+
} | null>(null);
|
|
356
|
+
|
|
357
|
+
const showProjectsChrome = hostToolChrome("projects");
|
|
226
358
|
|
|
227
359
|
const fetchProjects = useCallback(async () => {
|
|
228
|
-
if (!isElectron()) return;
|
|
360
|
+
if (!isElectron() || !hostToolChrome("projects")) return;
|
|
229
361
|
try {
|
|
230
362
|
const res = await listProjects();
|
|
231
363
|
setProjects(res.projects || []);
|
|
@@ -274,48 +406,80 @@ export function AgentSidebar({
|
|
|
274
406
|
[renamingValue, fetchProjects],
|
|
275
407
|
);
|
|
276
408
|
|
|
277
|
-
const
|
|
278
|
-
async (
|
|
279
|
-
|
|
409
|
+
const handleUpdateProject = useCallback(
|
|
410
|
+
async (
|
|
411
|
+
projectId: string,
|
|
412
|
+
input: { name: string; sourceFolders: string[] },
|
|
413
|
+
) => {
|
|
280
414
|
setProjectError(null);
|
|
281
415
|
try {
|
|
282
|
-
|
|
283
|
-
|
|
416
|
+
await updateProject(projectId, {
|
|
417
|
+
name: input.name,
|
|
418
|
+
sourceFolders: input.sourceFolders,
|
|
284
419
|
});
|
|
285
|
-
if (!result || result.canceled || result.filePaths.length === 0) return;
|
|
286
|
-
await updateProject(projectId, { folderPath: result.filePaths[0] });
|
|
287
420
|
await fetchProjects();
|
|
288
421
|
} catch (err) {
|
|
289
|
-
|
|
422
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
423
|
+
setProjectError(message);
|
|
424
|
+
throw err instanceof Error ? err : new Error(message);
|
|
290
425
|
}
|
|
291
426
|
},
|
|
292
|
-
[
|
|
427
|
+
[fetchProjects],
|
|
293
428
|
);
|
|
294
429
|
|
|
295
|
-
const
|
|
430
|
+
const removeProject = useCallback(
|
|
296
431
|
async (projectId: string) => {
|
|
297
432
|
if (deletingProjectId === projectId) return;
|
|
298
|
-
// 与会话删除同款两段确认:第一次点击武装红色按钮,第二次才真删。
|
|
299
|
-
if (confirmDeleteProjectId !== projectId) {
|
|
300
|
-
setConfirmDeleteProjectId(projectId);
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
433
|
try {
|
|
304
434
|
setDeletingProjectId(projectId);
|
|
305
|
-
// 后端会把该项目下的会话降级为无项目对话(不删会话)。
|
|
306
435
|
await deleteProject(projectId);
|
|
307
436
|
setConfirmDeleteProjectId(null);
|
|
437
|
+
setProjectMenu(null);
|
|
438
|
+
setEditingProjectId(null);
|
|
308
439
|
await fetchProjects();
|
|
309
440
|
await data.refreshChats();
|
|
310
441
|
} catch (err) {
|
|
311
|
-
|
|
442
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
443
|
+
setProjectError(message);
|
|
444
|
+
throw err instanceof Error ? err : new Error(message);
|
|
312
445
|
} finally {
|
|
313
446
|
setDeletingProjectId(null);
|
|
314
447
|
}
|
|
315
448
|
},
|
|
316
|
-
[
|
|
449
|
+
[deletingProjectId, fetchProjects, data],
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
const handleDeleteProject = useCallback(
|
|
453
|
+
async (projectId: string) => {
|
|
454
|
+
// 与会话删除同款两段确认:第一次点击武装红色按钮,第二次才真删。
|
|
455
|
+
if (confirmDeleteProjectId !== projectId) {
|
|
456
|
+
setConfirmDeleteProjectId(projectId);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
await removeProject(projectId);
|
|
461
|
+
} catch {
|
|
462
|
+
// 横幅已写 projectError
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
[confirmDeleteProjectId, removeProject],
|
|
317
466
|
);
|
|
318
467
|
|
|
468
|
+
const closeProjectMenu = useCallback(() => {
|
|
469
|
+
setProjectMenu(null);
|
|
470
|
+
setConfirmDeleteProjectId(null);
|
|
471
|
+
}, []);
|
|
472
|
+
|
|
473
|
+
const handleRevealProjectFolder = useCallback(async (folderPath: string) => {
|
|
474
|
+
setProjectError(null);
|
|
475
|
+
try {
|
|
476
|
+
const res = await openLocalPath(folderPath);
|
|
477
|
+
if (!res.success && res.error) setProjectError(res.error);
|
|
478
|
+
} catch (err) {
|
|
479
|
+
setProjectError(err instanceof Error ? err.message : String(err));
|
|
480
|
+
}
|
|
481
|
+
}, []);
|
|
482
|
+
|
|
319
483
|
const toggleProjectCollapsed = useCallback((projectId: string) => {
|
|
320
484
|
setCollapsedProjectIds((prev) => {
|
|
321
485
|
const next = new Set(prev);
|
|
@@ -388,8 +552,11 @@ export function AgentSidebar({
|
|
|
388
552
|
// 顶层按项目分组:无项目对话(含项目已被删但列表还没刷新的孤儿会话)
|
|
389
553
|
// 保持原有的日期分组;每个项目一个分组,组内按 pin + 时间排序。
|
|
390
554
|
const knownProjectIds = useMemo(
|
|
391
|
-
() =>
|
|
392
|
-
|
|
555
|
+
() =>
|
|
556
|
+
showProjectsChrome
|
|
557
|
+
? new Set(projects.map((p) => p.id))
|
|
558
|
+
: new Set<string>(),
|
|
559
|
+
[projects, showProjectsChrome],
|
|
393
560
|
);
|
|
394
561
|
|
|
395
562
|
const noProjectChats = useMemo(
|
|
@@ -402,11 +569,13 @@ export function AgentSidebar({
|
|
|
402
569
|
|
|
403
570
|
const projectGroups = useMemo(
|
|
404
571
|
() =>
|
|
405
|
-
|
|
406
|
-
project
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
572
|
+
showProjectsChrome
|
|
573
|
+
? projects.map((project) => ({
|
|
574
|
+
project,
|
|
575
|
+
items: normalizedChats.filter((c) => c.projectId === project.id),
|
|
576
|
+
}))
|
|
577
|
+
: [],
|
|
578
|
+
[projects, normalizedChats, showProjectsChrome],
|
|
410
579
|
);
|
|
411
580
|
|
|
412
581
|
const chatGroups = useMemo(() => {
|
|
@@ -591,15 +760,17 @@ export function AgentSidebar({
|
|
|
591
760
|
// `menu:open-terminal`). Same pattern as `menu:new-chat` above. The
|
|
592
761
|
// terminal is a toggleable panel beside the chat, so this just flips
|
|
593
762
|
// the layout state owned by AgentLayout.
|
|
763
|
+
const showTerminalChrome = hostToolChrome("terminal");
|
|
764
|
+
|
|
594
765
|
useEffect(() => {
|
|
595
|
-
if (!bridge?.onMenuOpenTerminal) return;
|
|
766
|
+
if (!showTerminalChrome || !bridge?.onMenuOpenTerminal) return;
|
|
596
767
|
bridge.onMenuOpenTerminal(() => {
|
|
597
768
|
onToggleRightPanel("terminal");
|
|
598
769
|
});
|
|
599
770
|
return () => {
|
|
600
771
|
bridge.offMenuOpenTerminal?.();
|
|
601
772
|
};
|
|
602
|
-
}, [bridge, onToggleRightPanel]);
|
|
773
|
+
}, [bridge, onToggleRightPanel, showTerminalChrome]);
|
|
603
774
|
|
|
604
775
|
const hasElectron = isElectron();
|
|
605
776
|
|
|
@@ -640,6 +811,7 @@ export function AgentSidebar({
|
|
|
640
811
|
<LuPlus className="h-3.5 w-3.5" />
|
|
641
812
|
<span>新对话</span>
|
|
642
813
|
</button>
|
|
814
|
+
{settingsChrome("agents") && (
|
|
643
815
|
<button
|
|
644
816
|
type="button"
|
|
645
817
|
onClick={() => navigate("/settings?section=agents")}
|
|
@@ -655,6 +827,8 @@ export function AgentSidebar({
|
|
|
655
827
|
<LuBot className="h-3.5 w-3.5" />
|
|
656
828
|
<span>智能体管理</span>
|
|
657
829
|
</button>
|
|
830
|
+
)}
|
|
831
|
+
{settingsChrome("skills") && (
|
|
658
832
|
<button
|
|
659
833
|
type="button"
|
|
660
834
|
onClick={() => navigate("/settings?section=skills")}
|
|
@@ -670,6 +844,8 @@ export function AgentSidebar({
|
|
|
670
844
|
<LuBlocks className="h-3.5 w-3.5" />
|
|
671
845
|
<span>Skill 设置</span>
|
|
672
846
|
</button>
|
|
847
|
+
)}
|
|
848
|
+
{settingsChrome("mcp") && (
|
|
673
849
|
<button
|
|
674
850
|
type="button"
|
|
675
851
|
onClick={() => navigate("/settings?section=mcp")}
|
|
@@ -685,6 +861,7 @@ export function AgentSidebar({
|
|
|
685
861
|
<LuPlug className="h-3.5 w-3.5" />
|
|
686
862
|
<span>MCP 设置</span>
|
|
687
863
|
</button>
|
|
864
|
+
)}
|
|
688
865
|
</div>
|
|
689
866
|
<div className="flex flex-shrink-0 items-center justify-between py-1 pl-2.5 pr-2.5">
|
|
690
867
|
<button
|
|
@@ -702,7 +879,7 @@ export function AgentSidebar({
|
|
|
702
879
|
</span>
|
|
703
880
|
</button>
|
|
704
881
|
<div className="flex items-center gap-1">
|
|
705
|
-
{hasElectron && (
|
|
882
|
+
{hasElectron && showProjectsChrome && (
|
|
706
883
|
<button
|
|
707
884
|
type="button"
|
|
708
885
|
onClick={() => setCreateProjectOpen(true)}
|
|
@@ -733,8 +910,9 @@ export function AgentSidebar({
|
|
|
733
910
|
</div>
|
|
734
911
|
) : (
|
|
735
912
|
<>
|
|
736
|
-
{/* 项目分组在前:组头可折叠,hover
|
|
737
|
-
{
|
|
913
|
+
{/* 项目分组在前:组头可折叠,hover 出 + 新建对话 / ·· 菜单 */}
|
|
914
|
+
{showProjectsChrome &&
|
|
915
|
+
projectGroups.map(({ project, items }) => (
|
|
738
916
|
<div key={project.id} className="mb-1">
|
|
739
917
|
<div className="group/proj relative">
|
|
740
918
|
{renamingProjectId === project.id ? (
|
|
@@ -767,25 +945,30 @@ export function AgentSidebar({
|
|
|
767
945
|
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
946
|
title={`${project.name}\n${project.folderPath}`}
|
|
769
947
|
>
|
|
770
|
-
<
|
|
948
|
+
<LuFolder className="mr-1 h-3 w-3 shrink-0" />
|
|
949
|
+
<span className="min-w-0 truncate normal-case">
|
|
950
|
+
{project.name}
|
|
951
|
+
</span>
|
|
952
|
+
<span className="ml-0.5 shrink-0">
|
|
771
953
|
{collapsedProjectIds.has(project.id) ? (
|
|
772
954
|
<LuChevronDown className="h-3 w-3" />
|
|
773
955
|
) : (
|
|
774
956
|
<LuChevronUp className="h-3 w-3" />
|
|
775
957
|
)}
|
|
776
958
|
</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
959
|
{items.length > 0 && (
|
|
782
960
|
<span className="ml-1 shrink-0 font-normal">
|
|
783
961
|
· {items.length}
|
|
784
962
|
</span>
|
|
785
963
|
)}
|
|
786
964
|
</button>
|
|
787
|
-
|
|
788
|
-
|
|
965
|
+
<div
|
|
966
|
+
className={`absolute right-1 top-1/2 flex -translate-y-1/2 items-center gap-0.5 transition-opacity ${
|
|
967
|
+
projectMenu?.id === project.id
|
|
968
|
+
? "opacity-100"
|
|
969
|
+
: "opacity-0 group-hover/proj:opacity-100"
|
|
970
|
+
}`}
|
|
971
|
+
>
|
|
789
972
|
<button
|
|
790
973
|
type="button"
|
|
791
974
|
onClick={() => handleOpenNewChat(project.id)}
|
|
@@ -796,45 +979,21 @@ export function AgentSidebar({
|
|
|
796
979
|
</button>
|
|
797
980
|
<button
|
|
798
981
|
type="button"
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
982
|
+
aria-label="项目菜单"
|
|
983
|
+
aria-expanded={projectMenu?.id === project.id}
|
|
984
|
+
onClick={(event) => {
|
|
985
|
+
const button = event.currentTarget;
|
|
986
|
+
setConfirmDeleteProjectId(null);
|
|
987
|
+
setProjectMenu((current) =>
|
|
988
|
+
current?.id === project.id
|
|
989
|
+
? null
|
|
990
|
+
: { id: project.id, anchor: button },
|
|
991
|
+
);
|
|
802
992
|
}}
|
|
803
993
|
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="
|
|
994
|
+
title="项目菜单"
|
|
805
995
|
>
|
|
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
|
-
/>
|
|
996
|
+
<LuEllipsis className="h-3 w-3" />
|
|
838
997
|
</button>
|
|
839
998
|
</div>
|
|
840
999
|
</>
|
|
@@ -896,6 +1055,7 @@ export function AgentSidebar({
|
|
|
896
1055
|
{/* ───── Footer: 右侧面板切换(终端 | 包槽位)+ 设置 ───── */}
|
|
897
1056
|
<div className="flex-shrink-0 border-t border-agent-border/40 px-2.5 py-1.5">
|
|
898
1057
|
{chatSlots.length === 0 ? (
|
|
1058
|
+
showTerminalChrome ? (
|
|
899
1059
|
<button
|
|
900
1060
|
type="button"
|
|
901
1061
|
onClick={() => onToggleRightPanel("terminal")}
|
|
@@ -914,12 +1074,14 @@ export function AgentSidebar({
|
|
|
914
1074
|
{isMac ? "⌘T" : "Ctrl+T"}
|
|
915
1075
|
</span>
|
|
916
1076
|
</button>
|
|
1077
|
+
) : null
|
|
917
1078
|
) : (
|
|
918
1079
|
<div
|
|
919
1080
|
role="group"
|
|
920
1081
|
aria-label="右侧面板切换"
|
|
921
1082
|
className="flex h-7 w-full items-center gap-0.5 rounded-full bg-agent-foreground/5 p-0.5"
|
|
922
1083
|
>
|
|
1084
|
+
{showTerminalChrome ? (
|
|
923
1085
|
<button
|
|
924
1086
|
type="button"
|
|
925
1087
|
onClick={() => onToggleRightPanel("terminal")}
|
|
@@ -935,6 +1097,7 @@ export function AgentSidebar({
|
|
|
935
1097
|
<LuTerminal className="h-3.5 w-3.5" />
|
|
936
1098
|
<span>终端</span>
|
|
937
1099
|
</button>
|
|
1100
|
+
) : null}
|
|
938
1101
|
{chatSlots.map((slot) => (
|
|
939
1102
|
<button
|
|
940
1103
|
key={slot.slotId}
|
|
@@ -955,6 +1118,7 @@ export function AgentSidebar({
|
|
|
955
1118
|
))}
|
|
956
1119
|
</div>
|
|
957
1120
|
)}
|
|
1121
|
+
{hasGeneralSettingsChrome() && (
|
|
958
1122
|
<button
|
|
959
1123
|
type="button"
|
|
960
1124
|
onClick={() => navigate("/settings")}
|
|
@@ -970,13 +1134,70 @@ export function AgentSidebar({
|
|
|
970
1134
|
<LuSettings className="h-3.5 w-3.5" />
|
|
971
1135
|
<span>设置</span>
|
|
972
1136
|
</button>
|
|
1137
|
+
)}
|
|
973
1138
|
</div>
|
|
974
1139
|
|
|
1140
|
+
{projectMenu &&
|
|
1141
|
+
(() => {
|
|
1142
|
+
const project = projects.find((item) => item.id === projectMenu.id);
|
|
1143
|
+
if (!project) return null;
|
|
1144
|
+
const chatCount =
|
|
1145
|
+
projectGroups.find((group) => group.project.id === project.id)
|
|
1146
|
+
?.items.length ?? 0;
|
|
1147
|
+
return (
|
|
1148
|
+
<ProjectOverflowMenu
|
|
1149
|
+
project={project}
|
|
1150
|
+
chatCount={chatCount}
|
|
1151
|
+
anchor={projectMenu.anchor}
|
|
1152
|
+
revealLabel={isMac ? "在访达中显示" : "在文件管理器中显示"}
|
|
1153
|
+
confirmDelete={confirmDeleteProjectId === project.id}
|
|
1154
|
+
deleting={deletingProjectId === project.id}
|
|
1155
|
+
onClose={closeProjectMenu}
|
|
1156
|
+
onRename={() => {
|
|
1157
|
+
setProjectMenu(null);
|
|
1158
|
+
setRenamingProjectId(project.id);
|
|
1159
|
+
setRenamingValue(project.name);
|
|
1160
|
+
}}
|
|
1161
|
+
onChangeFolder={() => {
|
|
1162
|
+
setProjectMenu(null);
|
|
1163
|
+
setEditingProjectId(project.id);
|
|
1164
|
+
}}
|
|
1165
|
+
onReveal={
|
|
1166
|
+
hostToolChrome("local-fs")
|
|
1167
|
+
? () => {
|
|
1168
|
+
setProjectMenu(null);
|
|
1169
|
+
void handleRevealProjectFolder(project.folderPath);
|
|
1170
|
+
}
|
|
1171
|
+
: undefined
|
|
1172
|
+
}
|
|
1173
|
+
onDelete={() => void handleDeleteProject(project.id)}
|
|
1174
|
+
/>
|
|
1175
|
+
);
|
|
1176
|
+
})()}
|
|
1177
|
+
|
|
975
1178
|
<CreateProjectModal
|
|
976
1179
|
open={createProjectOpen}
|
|
977
1180
|
onClose={() => setCreateProjectOpen(false)}
|
|
978
1181
|
onCreate={handleCreateProject}
|
|
979
1182
|
/>
|
|
1183
|
+
{editingProjectId &&
|
|
1184
|
+
(() => {
|
|
1185
|
+
const project = projects.find((item) => item.id === editingProjectId);
|
|
1186
|
+
if (!project) return null;
|
|
1187
|
+
return (
|
|
1188
|
+
<CreateProjectModal
|
|
1189
|
+
open
|
|
1190
|
+
mode="edit"
|
|
1191
|
+
initial={{
|
|
1192
|
+
name: project.name,
|
|
1193
|
+
sourceFolders: project.sourceFolders ?? [],
|
|
1194
|
+
}}
|
|
1195
|
+
onClose={() => setEditingProjectId(null)}
|
|
1196
|
+
onCreate={(input) => handleUpdateProject(project.id, input)}
|
|
1197
|
+
onDelete={() => removeProject(project.id)}
|
|
1198
|
+
/>
|
|
1199
|
+
);
|
|
1200
|
+
})()}
|
|
980
1201
|
</div>
|
|
981
1202
|
);
|
|
982
1203
|
}
|