@steerable/agent-shell-web 0.6.50 → 0.6.52
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/brand.ts +7 -0
- package/src/components/AgentSidebar.tsx +284 -188
- package/src/components/ConfirmDialog.tsx +90 -0
- package/src/components/CreateProjectModal.tsx +32 -16
- package/src/components/chat/ApprovalModal.tsx +2 -5
- package/src/components/chat/ApprovalPromptProvider.tsx +38 -22
- package/src/components/chat/AskUserPromptProvider.tsx +1 -32
- package/src/components/chat/ChatInput.tsx +47 -57
- package/src/components/chat/ChatProjectBadge.tsx +1 -1
- package/src/components/chat/EmptyChatHero.tsx +22 -0
- package/src/components/chat/ExecPolicyPicker.tsx +2 -2
- package/src/components/chat/ExecutedActionsCard.tsx +23 -13
- package/src/components/chat/LocalChatPanel.tsx +61 -18
- package/src/components/chat/MessageList.tsx +77 -5
- package/src/components/chat/ModelPicker.tsx +21 -1
- package/src/components/chat/OrchestrationFlowCard.tsx +484 -0
- package/src/components/chat/SessionOrchestrationFlow.tsx +573 -0
- package/src/components/chat/SessionTodoList.tsx +12 -3
- package/src/components/chat/UserMessage.tsx +116 -25
- package/src/components/chat/composer-status-stack.tsx +79 -0
- package/src/components/chat/orchestration-flow-model.ts +401 -0
- package/src/components/chat/prompt-chat-scope.ts +33 -0
- package/src/components/settings/AgentsSettingsPanel.tsx +33 -22
- package/src/components/settings/McpSettingsPanel.tsx +1 -1
- package/src/components/settings/OrchestrationSettingsPanel.tsx +67 -0
- package/src/components/settings/SettingsNavMenu.tsx +55 -0
- package/src/components/settings/SkillsSettingsPanel.tsx +1 -1
- package/src/hooks/useProjects.ts +82 -0
- package/src/lib/electron-bridge.ts +1 -0
- package/src/lib/exec-policy.ts +1 -1
- package/src/lib/local-api.ts +1 -1
- package/src/lib/orchestration-settings.ts +58 -0
- package/src/pages/AgentPage.tsx +116 -64
- package/src/pages/SettingsPage.tsx +338 -55
- package/src/vite-env.d.ts +2 -0
- package/vite.base.js +1 -0
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
*
|
|
42
42
|
* • Active chat id comes from the URL (`useParams().chatId`) rather than
|
|
43
43
|
* a Context (we drive navigation, not vice versa).
|
|
44
|
-
* • Agent CRUD lives on `/settings?section=
|
|
44
|
+
* • Agent CRUD lives on `/settings?section=plugins`(侧栏「插件」里的智能体分类),
|
|
45
45
|
* not a modal. Custom agents show up in ChatInput's expert picker.
|
|
46
46
|
* • Cmd+N / Cmd+T trigger `menu:new-chat` / `menu:open-terminal` via the
|
|
47
47
|
* preload bridge; we subscribe here so the shortcuts work regardless of
|
|
@@ -53,28 +53,19 @@
|
|
|
53
53
|
* ┌─────────────────────────────────┐
|
|
54
54
|
* │ ✨ Product Agent +• │ ← + has a color dot of the selected agent
|
|
55
55
|
* ├─────────────────────────────────┤
|
|
56
|
-
* │ v 专家团队 · 3 │
|
|
57
|
-
* │ ● 教练 (selected) │ ← highlight = selectedAgentId
|
|
58
|
-
* │ ● 文学顾问 · 当前 │ ← "当前" pill = activeChatAgentId
|
|
59
|
-
* │ ● 哲学家 │
|
|
60
|
-
* │ + 用「教练」新建对话 │ ← inline CTA when sel ≠ current
|
|
61
|
-
* ├──── divider ────────────────────┤
|
|
62
56
|
* │ ✎ 新对话 │ ← 只打开落地页,有内容才落库
|
|
63
|
-
* │
|
|
64
|
-
* │ ⬡ Skill 设置 │ ← /settings?section=skills(独立页)
|
|
65
|
-
* │ 🔌 MCP 设置 │ ← /settings?section=mcp(独立页)
|
|
57
|
+
* │ 🧩 插件 │ ← /settings?section=plugins(智能体 / Skills / MCP / 网络搜索)
|
|
66
58
|
* │ 会话 v 📁+ │ ← 📁+ 打开新建项目弹窗
|
|
67
59
|
* │ v 📁 项目A (hover: ✎··)│ ← ✎ 新建对话;·· 菜单:重命名/换目录/访达/删
|
|
68
60
|
* │ ...(项目内对话) │
|
|
69
61
|
* │ 今天 │
|
|
70
62
|
* │ ...(无项目对话,按日期分组) │ ← 无项目排在项目分组之后
|
|
71
63
|
* ├─────────────────────────────────┤
|
|
72
|
-
* │ ▢_ 终端 ⌘T │
|
|
73
64
|
* │ ⚙ 设置 v0.2.2 │ ← /settings;右侧是当前版本,检查更新在设置页
|
|
74
65
|
* └─────────────────────────────────┘
|
|
75
66
|
*
|
|
76
67
|
* 项目模式:项目 = 名字 + 托管家目录(Documents/<应用名>/<项目名>/)+
|
|
77
|
-
*
|
|
68
|
+
* 可选源文件夹。家目录与源文件夹(含各自子目录)都可读写。
|
|
78
69
|
*/
|
|
79
70
|
|
|
80
71
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
@@ -91,12 +82,10 @@ import {
|
|
|
91
82
|
LuSquarePen,
|
|
92
83
|
LuPanelLeftClose,
|
|
93
84
|
LuBlocks,
|
|
94
|
-
LuBot,
|
|
95
85
|
LuFolder,
|
|
96
86
|
LuFolderOpen,
|
|
97
87
|
LuFolderPlus,
|
|
98
88
|
LuPencil,
|
|
99
|
-
LuPlug,
|
|
100
89
|
LuEllipsis,
|
|
101
90
|
LuCircleHelp,
|
|
102
91
|
} from "react-icons/lu";
|
|
@@ -108,7 +97,7 @@ import { hasGeneralSettingsChrome, hostToolChrome, settingsChrome } from "@/lib/
|
|
|
108
97
|
import {
|
|
109
98
|
createProject,
|
|
110
99
|
deleteProject,
|
|
111
|
-
|
|
100
|
+
getChatLiveStream,
|
|
112
101
|
openLocalPath,
|
|
113
102
|
setChatPinned,
|
|
114
103
|
updateProject,
|
|
@@ -119,8 +108,10 @@ import {
|
|
|
119
108
|
import { usePendingAskUserChatIds } from "@/components/chat/AskUserPromptProvider";
|
|
120
109
|
import { usePendingApprovalChatIds } from "@/components/chat/ApprovalPromptProvider";
|
|
121
110
|
import type { UseChatsAndAgentsResult } from "@/hooks/useChatsAndAgents";
|
|
111
|
+
import { useProjects } from "@/hooks/useProjects";
|
|
122
112
|
import type { RightPanelState } from "@/layouts/AgentLayout";
|
|
123
113
|
import { BrandLockup } from "@/components/BrandLockup";
|
|
114
|
+
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
|
124
115
|
import {
|
|
125
116
|
SidebarVersionLabel,
|
|
126
117
|
useAppRelease,
|
|
@@ -139,8 +130,6 @@ function ProjectOverflowMenu({
|
|
|
139
130
|
chatCount,
|
|
140
131
|
anchor,
|
|
141
132
|
revealLabel,
|
|
142
|
-
confirmDelete,
|
|
143
|
-
deleting,
|
|
144
133
|
onClose,
|
|
145
134
|
onRename,
|
|
146
135
|
onChangeFolder,
|
|
@@ -151,8 +140,6 @@ function ProjectOverflowMenu({
|
|
|
151
140
|
chatCount: number;
|
|
152
141
|
anchor: HTMLElement;
|
|
153
142
|
revealLabel: string;
|
|
154
|
-
confirmDelete: boolean;
|
|
155
|
-
deleting: boolean;
|
|
156
143
|
onClose: () => void;
|
|
157
144
|
onRename: () => void;
|
|
158
145
|
onChangeFolder: () => void;
|
|
@@ -228,19 +215,12 @@ function ProjectOverflowMenu({
|
|
|
228
215
|
<button
|
|
229
216
|
type="button"
|
|
230
217
|
role="menuitem"
|
|
231
|
-
|
|
232
|
-
title={
|
|
233
|
-
confirmDelete
|
|
234
|
-
? "再次点击确认删除(会话会保留为无项目对话)"
|
|
235
|
-
: "删除项目(会话保留为无项目对话)"
|
|
236
|
-
}
|
|
218
|
+
title="删除项目(会话保留为无项目对话)"
|
|
237
219
|
onClick={onDelete}
|
|
238
|
-
className=
|
|
239
|
-
confirmDelete ? "text-agent-destructive" : "text-agent-foreground"
|
|
240
|
-
}`}
|
|
220
|
+
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-destructive/10"
|
|
241
221
|
>
|
|
242
|
-
<LuTrash2 className=
|
|
243
|
-
|
|
222
|
+
<LuTrash2 className="h-3.5 w-3.5 shrink-0" />
|
|
223
|
+
删除项目
|
|
244
224
|
</button>
|
|
245
225
|
</div>
|
|
246
226
|
</>,
|
|
@@ -248,6 +228,48 @@ function ProjectOverflowMenu({
|
|
|
248
228
|
);
|
|
249
229
|
}
|
|
250
230
|
|
|
231
|
+
/** 置顶 / 项目 / 最近 的展开状态。缺省键走组件内默认。 */
|
|
232
|
+
const SIDEBAR_SECTIONS_KEY = "deeppath.agent.sidebarSections";
|
|
233
|
+
|
|
234
|
+
type SidebarSectionId = "pinned" | "projects" | "recents";
|
|
235
|
+
|
|
236
|
+
function readStoredSidebarSections(): Partial<Record<SidebarSectionId, boolean>> {
|
|
237
|
+
try {
|
|
238
|
+
const raw = window.localStorage.getItem(SIDEBAR_SECTIONS_KEY);
|
|
239
|
+
if (!raw) return {};
|
|
240
|
+
const parsed: unknown = JSON.parse(raw);
|
|
241
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
242
|
+
const record = parsed as Record<string, unknown>;
|
|
243
|
+
const out: Partial<Record<SidebarSectionId, boolean>> = {};
|
|
244
|
+
for (const id of ["pinned", "projects", "recents"] as const) {
|
|
245
|
+
if (typeof record[id] === "boolean") out[id] = record[id];
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
} catch {
|
|
249
|
+
return {};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** 切走后对后台会话轮询 live-stream 的间隔,与对话页恢复轮询同级。 */
|
|
254
|
+
const BACKGROUND_STREAM_POLL_MS = 750;
|
|
255
|
+
/**
|
|
256
|
+
* 标记成正在生成之后,快照可能还没注册。这段时间内 live-stream 报
|
|
257
|
+
* inactive 不摘指示;见过 active,或超过这段时间仍 inactive,才摘掉。
|
|
258
|
+
*/
|
|
259
|
+
const BACKGROUND_STREAM_GRACE_MS = 3_000;
|
|
260
|
+
|
|
261
|
+
function writeSidebarSectionExpanded(id: SidebarSectionId, expanded: boolean) {
|
|
262
|
+
try {
|
|
263
|
+
const current = readStoredSidebarSections();
|
|
264
|
+
window.localStorage.setItem(
|
|
265
|
+
SIDEBAR_SECTIONS_KEY,
|
|
266
|
+
JSON.stringify({ ...current, [id]: expanded }),
|
|
267
|
+
);
|
|
268
|
+
} catch {
|
|
269
|
+
/* ignore quota / private mode */
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
251
273
|
interface AgentSidebarProps {
|
|
252
274
|
data: UseChatsAndAgentsResult;
|
|
253
275
|
/** 右侧栏当前打开的面板:null / 'terminal' / 包槽位 id(互斥)。 */
|
|
@@ -270,16 +292,24 @@ export function AgentSidebar({
|
|
|
270
292
|
const bridge = getElectronBridge();
|
|
271
293
|
const release = useAppRelease();
|
|
272
294
|
const onSettingsPage = location.pathname === "/settings";
|
|
273
|
-
// /
|
|
295
|
+
// 插件页(及旧的 agents/skills/mcp 深链)高亮「插件」;其余 /settings 高亮底部综合设置。
|
|
274
296
|
const settingsSection = useMemo(
|
|
275
297
|
() => new URLSearchParams(location.search).get("section"),
|
|
276
298
|
[location.search],
|
|
277
299
|
);
|
|
278
|
-
const
|
|
300
|
+
const pluginsAvailable =
|
|
301
|
+
settingsChrome("agents") ||
|
|
302
|
+
settingsChrome("skills") ||
|
|
303
|
+
settingsChrome("mcp") ||
|
|
304
|
+
settingsChrome("web-search");
|
|
305
|
+
const onPluginsSettings =
|
|
306
|
+
pluginsAvailable &&
|
|
279
307
|
onSettingsPage &&
|
|
280
|
-
settingsSection
|
|
281
|
-
|
|
282
|
-
|
|
308
|
+
(settingsSection === "plugins" ||
|
|
309
|
+
settingsSection === "agents" ||
|
|
310
|
+
settingsSection === "skills" ||
|
|
311
|
+
settingsSection === "mcp");
|
|
312
|
+
const onGeneralSettings = onSettingsPage && !onPluginsSettings;
|
|
283
313
|
const onNewChatHome = !currentChatId && !onSettingsPage;
|
|
284
314
|
|
|
285
315
|
const {
|
|
@@ -301,9 +331,10 @@ export function AgentSidebar({
|
|
|
301
331
|
setIsMac(/Mac|iPod|iPhone|iPad/.test(navigator.platform));
|
|
302
332
|
}
|
|
303
333
|
}, []);
|
|
304
|
-
const [
|
|
305
|
-
|
|
306
|
-
|
|
334
|
+
const [pendingDeleteChat, setPendingDeleteChat] = useState<{
|
|
335
|
+
id: string;
|
|
336
|
+
title: string;
|
|
337
|
+
} | null>(null);
|
|
307
338
|
const [deletingChatId, setDeletingChatId] = useState<string | null>(null);
|
|
308
339
|
const [pinningChatId, setPinningChatId] = useState<string | null>(null);
|
|
309
340
|
const [streamingChatIds, setStreamingChatIds] = useState<Set<string>>(
|
|
@@ -339,16 +370,111 @@ export function AgentSidebar({
|
|
|
339
370
|
};
|
|
340
371
|
}, []);
|
|
341
372
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const
|
|
373
|
+
// 当前打开的会话由 AgentChatView 自己报开始/结束。切走后的会话回合还在
|
|
374
|
+
// 后端跑,卸载时不会再报结束,这里用 live-stream 对账,结束后摘掉指示。
|
|
375
|
+
const seenActiveBackgroundRef = useRef<Set<string>>(new Set());
|
|
376
|
+
const backgroundSinceRef = useRef<Map<string, number>>(new Map());
|
|
377
|
+
useEffect(() => {
|
|
378
|
+
for (const id of seenActiveBackgroundRef.current) {
|
|
379
|
+
if (!streamingChatIds.has(id)) seenActiveBackgroundRef.current.delete(id);
|
|
380
|
+
}
|
|
381
|
+
for (const id of backgroundSinceRef.current.keys()) {
|
|
382
|
+
if (!streamingChatIds.has(id)) backgroundSinceRef.current.delete(id);
|
|
383
|
+
}
|
|
384
|
+
const backgroundIds = [...streamingChatIds].filter(
|
|
385
|
+
(id) => id !== currentChatId,
|
|
386
|
+
);
|
|
387
|
+
if (backgroundIds.length === 0 || !isElectron()) return;
|
|
388
|
+
|
|
389
|
+
let cancelled = false;
|
|
390
|
+
let ticking = false;
|
|
391
|
+
const tick = async () => {
|
|
392
|
+
if (ticking || cancelled) return;
|
|
393
|
+
ticking = true;
|
|
394
|
+
try {
|
|
395
|
+
const now = Date.now();
|
|
396
|
+
const settled = await Promise.all(
|
|
397
|
+
backgroundIds.map(async (id) => {
|
|
398
|
+
try {
|
|
399
|
+
const live = await getChatLiveStream(id);
|
|
400
|
+
return { id, active: live?.active === true, ok: true as const };
|
|
401
|
+
} catch {
|
|
402
|
+
return { id, active: false, ok: false as const };
|
|
403
|
+
}
|
|
404
|
+
}),
|
|
405
|
+
);
|
|
406
|
+
if (cancelled) return;
|
|
407
|
+
const drop = new Set<string>();
|
|
408
|
+
for (const row of settled) {
|
|
409
|
+
if (!row.ok) continue;
|
|
410
|
+
if (row.active) {
|
|
411
|
+
seenActiveBackgroundRef.current.add(row.id);
|
|
412
|
+
backgroundSinceRef.current.delete(row.id);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
// 已经对上过运行中的快照:inactive 就是回合结束。
|
|
416
|
+
if (seenActiveBackgroundRef.current.has(row.id)) {
|
|
417
|
+
drop.add(row.id);
|
|
418
|
+
seenActiveBackgroundRef.current.delete(row.id);
|
|
419
|
+
backgroundSinceRef.current.delete(row.id);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
// 还没见过快照(切走时回合可能刚起步)。宽限期内保留指示。
|
|
423
|
+
const since = backgroundSinceRef.current.get(row.id);
|
|
424
|
+
if (since === undefined) {
|
|
425
|
+
backgroundSinceRef.current.set(row.id, now);
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (now - since >= BACKGROUND_STREAM_GRACE_MS) {
|
|
429
|
+
drop.add(row.id);
|
|
430
|
+
backgroundSinceRef.current.delete(row.id);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (drop.size === 0) return;
|
|
434
|
+
setStreamingChatIds((prev) => {
|
|
435
|
+
let changed = false;
|
|
436
|
+
const next = new Set(prev);
|
|
437
|
+
for (const id of drop) {
|
|
438
|
+
if (next.delete(id)) changed = true;
|
|
439
|
+
}
|
|
440
|
+
return changed ? next : prev;
|
|
441
|
+
});
|
|
442
|
+
} finally {
|
|
443
|
+
ticking = false;
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
void tick();
|
|
448
|
+
const timer = window.setInterval(() => {
|
|
449
|
+
void tick();
|
|
450
|
+
}, BACKGROUND_STREAM_POLL_MS);
|
|
451
|
+
return () => {
|
|
452
|
+
cancelled = true;
|
|
453
|
+
window.clearInterval(timer);
|
|
454
|
+
};
|
|
455
|
+
}, [streamingChatIds, currentChatId]);
|
|
456
|
+
|
|
457
|
+
// null = 用户还没点过这一组,走默认(置顶:有置顶会话才展开;项目、最近:展开)。
|
|
458
|
+
// 点过之后写入 localStorage,刷新和重启后保持。
|
|
459
|
+
const [pinnedExpanded, setPinnedExpanded] = useState<boolean | null>(
|
|
460
|
+
() => readStoredSidebarSections().pinned ?? null,
|
|
461
|
+
);
|
|
462
|
+
const [projectsExpanded, setProjectsExpanded] = useState<boolean | null>(
|
|
463
|
+
() => readStoredSidebarSections().projects ?? null,
|
|
464
|
+
);
|
|
465
|
+
const [recentsExpanded, setRecentsExpanded] = useState<boolean | null>(
|
|
466
|
+
() => readStoredSidebarSections().recents ?? null,
|
|
467
|
+
);
|
|
345
468
|
const chatScrollRef = useRef<HTMLDivElement>(null);
|
|
346
469
|
|
|
347
470
|
// ───── 项目模式 ─────
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
|
|
471
|
+
// 项目列表与输入框「关联到项目」共用 useProjects。会话按 projectId
|
|
472
|
+
// 分组:项目分组在上(组头 hover:+ 新建对话 / ·· 菜单),无项目对话在下。
|
|
473
|
+
const {
|
|
474
|
+
projects,
|
|
475
|
+
error: projectsLoadError,
|
|
476
|
+
refresh: fetchProjects,
|
|
477
|
+
} = useProjects();
|
|
352
478
|
const [collapsedProjectIds, setCollapsedProjectIds] = useState<Set<string>>(
|
|
353
479
|
() => new Set(),
|
|
354
480
|
);
|
|
@@ -356,9 +482,8 @@ export function AgentSidebar({
|
|
|
356
482
|
null,
|
|
357
483
|
);
|
|
358
484
|
const [renamingValue, setRenamingValue] = useState("");
|
|
359
|
-
const [
|
|
360
|
-
|
|
361
|
-
>(null);
|
|
485
|
+
const [pendingDeleteProject, setPendingDeleteProject] =
|
|
486
|
+
useState<LocalProject | null>(null);
|
|
362
487
|
const [deletingProjectId, setDeletingProjectId] = useState<string | null>(
|
|
363
488
|
null,
|
|
364
489
|
);
|
|
@@ -372,21 +497,6 @@ export function AgentSidebar({
|
|
|
372
497
|
|
|
373
498
|
const showProjectsChrome = hostToolChrome("projects");
|
|
374
499
|
|
|
375
|
-
const fetchProjects = useCallback(async () => {
|
|
376
|
-
if (!isElectron() || !hostToolChrome("projects")) return;
|
|
377
|
-
try {
|
|
378
|
-
const res = await listProjects();
|
|
379
|
-
setProjects(res.projects || []);
|
|
380
|
-
} catch (err) {
|
|
381
|
-
console.error("获取项目列表失败:", err);
|
|
382
|
-
setProjectError(err instanceof Error ? err.message : String(err));
|
|
383
|
-
}
|
|
384
|
-
}, []);
|
|
385
|
-
|
|
386
|
-
useEffect(() => {
|
|
387
|
-
void fetchProjects();
|
|
388
|
-
}, [fetchProjects]);
|
|
389
|
-
|
|
390
500
|
const handleCreateProject = useCallback(
|
|
391
501
|
async (input: { name: string; sourceFolders: string[] }) => {
|
|
392
502
|
setProjectError(null);
|
|
@@ -449,7 +559,7 @@ export function AgentSidebar({
|
|
|
449
559
|
try {
|
|
450
560
|
setDeletingProjectId(projectId);
|
|
451
561
|
await deleteProject(projectId);
|
|
452
|
-
|
|
562
|
+
setPendingDeleteProject(null);
|
|
453
563
|
setProjectMenu(null);
|
|
454
564
|
setEditingProjectId(null);
|
|
455
565
|
await fetchProjects();
|
|
@@ -465,25 +575,8 @@ export function AgentSidebar({
|
|
|
465
575
|
[deletingProjectId, fetchProjects, data],
|
|
466
576
|
);
|
|
467
577
|
|
|
468
|
-
const handleDeleteProject = useCallback(
|
|
469
|
-
async (projectId: string) => {
|
|
470
|
-
// 与会话删除同款两段确认:第一次点击武装红色按钮,第二次才真删。
|
|
471
|
-
if (confirmDeleteProjectId !== projectId) {
|
|
472
|
-
setConfirmDeleteProjectId(projectId);
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
try {
|
|
476
|
-
await removeProject(projectId);
|
|
477
|
-
} catch {
|
|
478
|
-
// 横幅已写 projectError
|
|
479
|
-
}
|
|
480
|
-
},
|
|
481
|
-
[confirmDeleteProjectId, removeProject],
|
|
482
|
-
);
|
|
483
|
-
|
|
484
578
|
const closeProjectMenu = useCallback(() => {
|
|
485
579
|
setProjectMenu(null);
|
|
486
|
-
setConfirmDeleteProjectId(null);
|
|
487
580
|
}, []);
|
|
488
581
|
|
|
489
582
|
const handleRevealProjectFolder = useCallback(async (folderPath: string) => {
|
|
@@ -630,29 +723,20 @@ export function AgentSidebar({
|
|
|
630
723
|
const isProjectsExpanded = projectsExpanded ?? true;
|
|
631
724
|
const isRecentsExpanded = recentsExpanded ?? true;
|
|
632
725
|
|
|
633
|
-
const handleDeleteChat = useCallback(
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
if (
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
}
|
|
643
|
-
try {
|
|
644
|
-
setDeletingChatId(id);
|
|
645
|
-
const ok = await deleteChat(id);
|
|
646
|
-
if (ok) {
|
|
647
|
-
setConfirmDeleteChatId(null);
|
|
648
|
-
if (id === currentChatId) navigate("/agent");
|
|
649
|
-
}
|
|
650
|
-
} finally {
|
|
651
|
-
setDeletingChatId(null);
|
|
726
|
+
const handleDeleteChat = useCallback(async () => {
|
|
727
|
+
if (!pendingDeleteChat || deletingChatId === pendingDeleteChat.id) return;
|
|
728
|
+
const id = pendingDeleteChat.id;
|
|
729
|
+
try {
|
|
730
|
+
setDeletingChatId(id);
|
|
731
|
+
const ok = await deleteChat(id);
|
|
732
|
+
if (ok) {
|
|
733
|
+
setPendingDeleteChat(null);
|
|
734
|
+
if (id === currentChatId) navigate("/agent");
|
|
652
735
|
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
|
|
736
|
+
} finally {
|
|
737
|
+
setDeletingChatId(null);
|
|
738
|
+
}
|
|
739
|
+
}, [pendingDeleteChat, deleteChat, deletingChatId, currentChatId, navigate]);
|
|
656
740
|
|
|
657
741
|
const handleTogglePinChat = useCallback(
|
|
658
742
|
async (id: string, nextPinned: boolean) => {
|
|
@@ -673,7 +757,6 @@ export function AgentSidebar({
|
|
|
673
757
|
// 单条会话行——无项目日期分组和项目分组共用同一个渲染,避免两份 JSX。
|
|
674
758
|
const renderChatRow = (chat: (typeof normalizedChats)[number]) => {
|
|
675
759
|
const isCurrent = currentChatId === chat.id;
|
|
676
|
-
const isConfirmingDelete = confirmDeleteChatId === chat.id;
|
|
677
760
|
const isDeleting = deletingChatId === chat.id;
|
|
678
761
|
const isPinning = pinningChatId === chat.id;
|
|
679
762
|
const isStreaming = Boolean(
|
|
@@ -695,7 +778,6 @@ export function AgentSidebar({
|
|
|
695
778
|
<button
|
|
696
779
|
type="button"
|
|
697
780
|
onClick={() => {
|
|
698
|
-
setConfirmDeleteChatId(null);
|
|
699
781
|
navigate(`/agent/${chat.id}`);
|
|
700
782
|
}}
|
|
701
783
|
className={[
|
|
@@ -739,47 +821,37 @@ export function AgentSidebar({
|
|
|
739
821
|
className={[
|
|
740
822
|
"absolute inset-y-0 right-0 flex items-center justify-end gap-0.5 pr-1 pl-8 rounded-r-full transition-all duration-200",
|
|
741
823
|
"bg-gradient-to-l from-agent-muted/95 via-agent-muted/80 to-transparent",
|
|
742
|
-
|
|
743
|
-
? "opacity-100 pointer-events-auto"
|
|
744
|
-
: "opacity-0 group-hover/item:opacity-100 focus-within:opacity-100 pointer-events-none group-hover/item:pointer-events-auto focus-within:pointer-events-auto",
|
|
824
|
+
"opacity-0 group-hover/item:opacity-100 focus-within:opacity-100 pointer-events-none group-hover/item:pointer-events-auto focus-within:pointer-events-auto",
|
|
745
825
|
].join(" ")}
|
|
746
826
|
>
|
|
747
|
-
{!isConfirmingDelete && (
|
|
748
|
-
<button
|
|
749
|
-
type="button"
|
|
750
|
-
onClick={(event) => {
|
|
751
|
-
event.stopPropagation();
|
|
752
|
-
void handleTogglePinChat(chat.id, !chat.isPinned);
|
|
753
|
-
}}
|
|
754
|
-
disabled={isPinning}
|
|
755
|
-
className="flex h-6 w-6 items-center justify-center rounded-full text-agent-foreground/70 transition-all duration-200 hover:bg-agent-foreground/10 hover:text-agent-foreground disabled:cursor-not-allowed"
|
|
756
|
-
title={chat.isPinned ? "取消置顶" : "置顶会话"}
|
|
757
|
-
aria-label={chat.isPinned ? "取消置顶" : "置顶会话"}
|
|
758
|
-
data-testid="sidebar-chat-pin"
|
|
759
|
-
>
|
|
760
|
-
{chat.isPinned ? (
|
|
761
|
-
<RiPushpin2Fill className="h-3.5 w-3.5 text-agent-foreground" />
|
|
762
|
-
) : (
|
|
763
|
-
<RiPushpin2Line className="h-3.5 w-3.5 text-agent-foreground/75 hover:text-agent-foreground" />
|
|
764
|
-
)}
|
|
765
|
-
</button>
|
|
766
|
-
)}
|
|
767
827
|
<button
|
|
768
828
|
type="button"
|
|
769
829
|
onClick={(event) => {
|
|
770
830
|
event.stopPropagation();
|
|
771
|
-
void
|
|
831
|
+
void handleTogglePinChat(chat.id, !chat.isPinned);
|
|
832
|
+
}}
|
|
833
|
+
disabled={isPinning}
|
|
834
|
+
className="flex h-6 w-6 items-center justify-center rounded-full text-agent-foreground/70 transition-all duration-200 hover:bg-agent-foreground/10 hover:text-agent-foreground disabled:cursor-not-allowed"
|
|
835
|
+
title={chat.isPinned ? "取消置顶" : "置顶会话"}
|
|
836
|
+
aria-label={chat.isPinned ? "取消置顶" : "置顶会话"}
|
|
837
|
+
data-testid="sidebar-chat-pin"
|
|
838
|
+
>
|
|
839
|
+
{chat.isPinned ? (
|
|
840
|
+
<RiPushpin2Fill className="h-3.5 w-3.5 text-agent-foreground" />
|
|
841
|
+
) : (
|
|
842
|
+
<RiPushpin2Line className="h-3.5 w-3.5 text-agent-foreground/75 hover:text-agent-foreground" />
|
|
843
|
+
)}
|
|
844
|
+
</button>
|
|
845
|
+
<button
|
|
846
|
+
type="button"
|
|
847
|
+
onClick={(event) => {
|
|
848
|
+
event.stopPropagation();
|
|
849
|
+
setPendingDeleteChat({ id: chat.id, title: chat.title });
|
|
772
850
|
}}
|
|
773
851
|
disabled={isDeleting}
|
|
774
|
-
className=
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
? "bg-agent-destructive/10 text-agent-destructive hover:bg-agent-destructive/20"
|
|
778
|
-
: "text-agent-foreground/70 hover:bg-agent-foreground/10 hover:text-agent-destructive",
|
|
779
|
-
"disabled:cursor-not-allowed disabled:opacity-100",
|
|
780
|
-
].join(" ")}
|
|
781
|
-
title={isConfirmingDelete ? "再次点击确认删除" : "删除会话"}
|
|
782
|
-
aria-label={isConfirmingDelete ? "再次点击确认删除" : "删除会话"}
|
|
852
|
+
className="flex h-6 w-6 items-center justify-center rounded-full text-agent-foreground/70 transition-all duration-200 hover:bg-agent-foreground/10 hover:text-agent-destructive disabled:cursor-not-allowed disabled:opacity-100"
|
|
853
|
+
title="删除会话"
|
|
854
|
+
aria-label="删除会话"
|
|
783
855
|
data-testid="sidebar-chat-delete"
|
|
784
856
|
>
|
|
785
857
|
<LuTrash2
|
|
@@ -855,7 +927,7 @@ export function AgentSidebar({
|
|
|
855
927
|
|
|
856
928
|
{/* ───── 会话 ───── */}
|
|
857
929
|
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
858
|
-
{/* 新对话 +
|
|
930
|
+
{/* 新对话 + 插件(智能体 / Skills / MCP / 网络搜索在同一页用分类切换) */}
|
|
859
931
|
<div className="flex-shrink-0 space-y-0.5 px-2.5 pb-0.5">
|
|
860
932
|
<button
|
|
861
933
|
type="button"
|
|
@@ -872,55 +944,21 @@ export function AgentSidebar({
|
|
|
872
944
|
<LuSquarePen className="h-3.5 w-3.5" />
|
|
873
945
|
<span>新对话</span>
|
|
874
946
|
</button>
|
|
875
|
-
{
|
|
947
|
+
{pluginsAvailable && (
|
|
876
948
|
<button
|
|
877
949
|
type="button"
|
|
878
|
-
onClick={() => navigate("/settings?section=
|
|
950
|
+
onClick={() => navigate("/settings?section=plugins")}
|
|
879
951
|
className={[
|
|
880
952
|
"flex h-7 w-full items-center gap-1.5 rounded-full px-2.5 text-xs transition-colors",
|
|
881
|
-
|
|
953
|
+
onPluginsSettings
|
|
882
954
|
? "bg-agent-foreground/10 font-medium text-agent-foreground"
|
|
883
955
|
: "text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground",
|
|
884
956
|
].join(" ")}
|
|
885
|
-
title="
|
|
886
|
-
data-testid="sidebar-
|
|
887
|
-
>
|
|
888
|
-
<LuBot className="h-3.5 w-3.5" />
|
|
889
|
-
<span>智能体管理</span>
|
|
890
|
-
</button>
|
|
891
|
-
)}
|
|
892
|
-
{settingsChrome("skills") && (
|
|
893
|
-
<button
|
|
894
|
-
type="button"
|
|
895
|
-
onClick={() => navigate("/settings?section=skills")}
|
|
896
|
-
className={[
|
|
897
|
-
"flex h-7 w-full items-center gap-1.5 rounded-full px-2.5 text-xs transition-colors",
|
|
898
|
-
onSettingsPage && settingsSection === "skills"
|
|
899
|
-
? "bg-agent-foreground/10 font-medium text-agent-foreground"
|
|
900
|
-
: "text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground",
|
|
901
|
-
].join(" ")}
|
|
902
|
-
title="Skill 设置"
|
|
903
|
-
data-testid="sidebar-skill-settings"
|
|
957
|
+
title="插件"
|
|
958
|
+
data-testid="sidebar-plugins"
|
|
904
959
|
>
|
|
905
960
|
<LuBlocks className="h-3.5 w-3.5" />
|
|
906
|
-
<span
|
|
907
|
-
</button>
|
|
908
|
-
)}
|
|
909
|
-
{settingsChrome("mcp") && (
|
|
910
|
-
<button
|
|
911
|
-
type="button"
|
|
912
|
-
onClick={() => navigate("/settings?section=mcp")}
|
|
913
|
-
className={[
|
|
914
|
-
"flex h-7 w-full items-center gap-1.5 rounded-full px-2.5 text-xs transition-colors",
|
|
915
|
-
onSettingsPage && settingsSection === "mcp"
|
|
916
|
-
? "bg-agent-foreground/10 font-medium text-agent-foreground"
|
|
917
|
-
: "text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground",
|
|
918
|
-
].join(" ")}
|
|
919
|
-
title="MCP 设置"
|
|
920
|
-
data-testid="sidebar-mcp-settings"
|
|
921
|
-
>
|
|
922
|
-
<LuPlug className="h-3.5 w-3.5" />
|
|
923
|
-
<span>MCP 设置</span>
|
|
961
|
+
<span>插件</span>
|
|
924
962
|
</button>
|
|
925
963
|
)}
|
|
926
964
|
</div>
|
|
@@ -942,7 +980,12 @@ export function AgentSidebar({
|
|
|
942
980
|
<div className="group/section flex h-7 items-center justify-between rounded-agent-md px-1.5 text-xs text-agent-muted-foreground transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground">
|
|
943
981
|
<button
|
|
944
982
|
type="button"
|
|
945
|
-
|
|
983
|
+
aria-expanded={isPinnedExpanded}
|
|
984
|
+
onClick={() => {
|
|
985
|
+
const next = !isPinnedExpanded;
|
|
986
|
+
setPinnedExpanded(next);
|
|
987
|
+
writeSidebarSectionExpanded("pinned", next);
|
|
988
|
+
}}
|
|
946
989
|
className="flex flex-1 min-w-0 items-center gap-1 text-left font-medium text-agent-muted-foreground hover:text-agent-foreground"
|
|
947
990
|
>
|
|
948
991
|
<span>置顶</span>
|
|
@@ -967,7 +1010,12 @@ export function AgentSidebar({
|
|
|
967
1010
|
<div className="group/section flex h-7 items-center justify-between rounded-agent-md px-1.5 text-xs text-agent-muted-foreground transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground">
|
|
968
1011
|
<button
|
|
969
1012
|
type="button"
|
|
970
|
-
|
|
1013
|
+
aria-expanded={isProjectsExpanded}
|
|
1014
|
+
onClick={() => {
|
|
1015
|
+
const next = !isProjectsExpanded;
|
|
1016
|
+
setProjectsExpanded(next);
|
|
1017
|
+
writeSidebarSectionExpanded("projects", next);
|
|
1018
|
+
}}
|
|
971
1019
|
className="flex flex-1 min-w-0 items-center gap-1 text-left font-medium text-agent-muted-foreground hover:text-agent-foreground"
|
|
972
1020
|
>
|
|
973
1021
|
<span>项目</span>
|
|
@@ -1065,7 +1113,6 @@ export function AgentSidebar({
|
|
|
1065
1113
|
aria-expanded={projectMenu?.id === project.id}
|
|
1066
1114
|
onClick={(event) => {
|
|
1067
1115
|
const button = event.currentTarget;
|
|
1068
|
-
setConfirmDeleteProjectId(null);
|
|
1069
1116
|
setProjectMenu((current) =>
|
|
1070
1117
|
current?.id === project.id
|
|
1071
1118
|
? null
|
|
@@ -1105,7 +1152,12 @@ export function AgentSidebar({
|
|
|
1105
1152
|
<div className="group/section flex h-7 items-center justify-between rounded-agent-md px-1.5 text-xs text-agent-muted-foreground transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground">
|
|
1106
1153
|
<button
|
|
1107
1154
|
type="button"
|
|
1108
|
-
|
|
1155
|
+
aria-expanded={isRecentsExpanded}
|
|
1156
|
+
onClick={() => {
|
|
1157
|
+
const next = !isRecentsExpanded;
|
|
1158
|
+
setRecentsExpanded(next);
|
|
1159
|
+
writeSidebarSectionExpanded("recents", next);
|
|
1160
|
+
}}
|
|
1109
1161
|
className="flex flex-1 min-w-0 items-center gap-1 text-left font-medium text-agent-muted-foreground hover:text-agent-foreground"
|
|
1110
1162
|
>
|
|
1111
1163
|
<span>最近</span>
|
|
@@ -1177,12 +1229,12 @@ export function AgentSidebar({
|
|
|
1177
1229
|
</div>
|
|
1178
1230
|
</div>
|
|
1179
1231
|
|
|
1180
|
-
{(error || projectError) && (
|
|
1232
|
+
{(error || projectError || projectsLoadError) && (
|
|
1181
1233
|
<div
|
|
1182
1234
|
className="flex-shrink-0 border-t border-agent-destructive/40 bg-agent-destructive/10 px-2.5 py-1.5 text-[11px] text-agent-destructive"
|
|
1183
1235
|
role="alert"
|
|
1184
1236
|
>
|
|
1185
|
-
{error ?? projectError}
|
|
1237
|
+
{error ?? projectError ?? projectsLoadError}
|
|
1186
1238
|
</div>
|
|
1187
1239
|
)}
|
|
1188
1240
|
|
|
@@ -1226,8 +1278,6 @@ export function AgentSidebar({
|
|
|
1226
1278
|
chatCount={chatCount}
|
|
1227
1279
|
anchor={projectMenu.anchor}
|
|
1228
1280
|
revealLabel={isMac ? "在访达中显示" : "在文件管理器中显示"}
|
|
1229
|
-
confirmDelete={confirmDeleteProjectId === project.id}
|
|
1230
|
-
deleting={deletingProjectId === project.id}
|
|
1231
1281
|
onClose={closeProjectMenu}
|
|
1232
1282
|
onRename={() => {
|
|
1233
1283
|
setProjectMenu(null);
|
|
@@ -1246,11 +1296,57 @@ export function AgentSidebar({
|
|
|
1246
1296
|
}
|
|
1247
1297
|
: undefined
|
|
1248
1298
|
}
|
|
1249
|
-
onDelete={() =>
|
|
1299
|
+
onDelete={() => {
|
|
1300
|
+
setProjectMenu(null);
|
|
1301
|
+
setPendingDeleteProject(project);
|
|
1302
|
+
}}
|
|
1250
1303
|
/>
|
|
1251
1304
|
);
|
|
1252
1305
|
})()}
|
|
1253
1306
|
|
|
1307
|
+
<ConfirmDialog
|
|
1308
|
+
open={pendingDeleteChat !== null}
|
|
1309
|
+
title="删除会话"
|
|
1310
|
+
description={
|
|
1311
|
+
pendingDeleteChat
|
|
1312
|
+
? `确定删除「${pendingDeleteChat.title}」?此操作无法撤销。`
|
|
1313
|
+
: ""
|
|
1314
|
+
}
|
|
1315
|
+
pending={
|
|
1316
|
+
pendingDeleteChat !== null && deletingChatId === pendingDeleteChat.id
|
|
1317
|
+
}
|
|
1318
|
+
onCancel={() => {
|
|
1319
|
+
if (deletingChatId) return;
|
|
1320
|
+
setPendingDeleteChat(null);
|
|
1321
|
+
}}
|
|
1322
|
+
onConfirm={() => void handleDeleteChat()}
|
|
1323
|
+
testId="sidebar-delete-chat-dialog"
|
|
1324
|
+
/>
|
|
1325
|
+
<ConfirmDialog
|
|
1326
|
+
open={pendingDeleteProject !== null}
|
|
1327
|
+
title="删除项目"
|
|
1328
|
+
description={
|
|
1329
|
+
pendingDeleteProject
|
|
1330
|
+
? `确定删除项目「${pendingDeleteProject.name}」?会话会保留为无项目对话。`
|
|
1331
|
+
: ""
|
|
1332
|
+
}
|
|
1333
|
+
confirmLabel="删除项目"
|
|
1334
|
+
pending={
|
|
1335
|
+
pendingDeleteProject !== null &&
|
|
1336
|
+
deletingProjectId === pendingDeleteProject.id
|
|
1337
|
+
}
|
|
1338
|
+
onCancel={() => {
|
|
1339
|
+
if (deletingProjectId) return;
|
|
1340
|
+
setPendingDeleteProject(null);
|
|
1341
|
+
}}
|
|
1342
|
+
onConfirm={() => {
|
|
1343
|
+
if (!pendingDeleteProject) return;
|
|
1344
|
+
void removeProject(pendingDeleteProject.id).catch(() => {
|
|
1345
|
+
// 横幅已写 projectError
|
|
1346
|
+
});
|
|
1347
|
+
}}
|
|
1348
|
+
testId="sidebar-delete-project-dialog"
|
|
1349
|
+
/>
|
|
1254
1350
|
<CreateProjectModal
|
|
1255
1351
|
open={createProjectOpen}
|
|
1256
1352
|
onClose={() => setCreateProjectOpen(false)}
|