@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steerable/agent-shell-web",
3
- "version": "0.6.31",
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.31",
50
- "@steerable/agent-ui": "0.6.31"
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.31"
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 handleChangeProjectFolder = useCallback(
278
- async (projectId: string) => {
279
- if (!isElectron()) return;
409
+ const handleUpdateProject = useCallback(
410
+ async (
411
+ projectId: string,
412
+ input: { name: string; sourceFolders: string[] },
413
+ ) => {
280
414
  setProjectError(null);
281
415
  try {
282
- const result = await bridge?.local?.selectDirectory({
283
- title: "重新选择项目文件夹",
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
- setProjectError(err instanceof Error ? err.message : String(err));
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
- [bridge, fetchProjects],
427
+ [fetchProjects],
293
428
  );
294
429
 
295
- const handleDeleteProject = useCallback(
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
- setProjectError(err instanceof Error ? err.message : String(err));
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
- [confirmDeleteProjectId, deletingProjectId, fetchProjects, data],
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
- () => new Set(projects.map((p) => p.id)),
392
- [projects],
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
- projects.map((project) => ({
406
- project,
407
- items: normalizedChats.filter((c) => c.projectId === project.id),
408
- })),
409
- [projects, normalizedChats],
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
- {projectGroups.map(({ project, items }) => (
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
- <span className="mr-1">
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
- <div className="absolute right-1 top-1/2 flex -translate-y-1/2 items-center gap-0.5 opacity-0 transition-opacity group-hover/proj:opacity-100">
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
- onClick={() => {
800
- setRenamingProjectId(project.id);
801
- setRenamingValue(project.name);
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
- <LuPencil className="h-3 w-3" />
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
  }