@steerable/agent-shell-web 0.6.51 → 0.6.53
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 +129 -59
- 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 +22 -12
- package/src/components/chat/LocalChatPanel.tsx +25 -19
- package/src/components/chat/MessageList.tsx +77 -5
- package/src/components/chat/ModelPicker.tsx +21 -1
- package/src/components/chat/OrchestrationFlowCard.tsx +13 -3
- package/src/components/chat/SessionOrchestrationFlow.tsx +66 -19
- 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 +108 -43
- package/src/components/chat/prompt-chat-scope.ts +33 -0
- package/src/components/settings/AgentsSettingsPanel.tsx +33 -22
- package/src/components/settings/McpSettingsPanel.tsx +144 -48
- package/src/components/settings/SkillsSettingsPanel.tsx +1 -1
- package/src/hooks/useProjects.ts +82 -0
- package/src/lib/attachments.ts +8 -0
- package/src/lib/electron-bridge.ts +16 -1
- package/src/lib/exec-policy.ts +1 -1
- package/src/lib/host-file-drop.ts +68 -0
- package/src/lib/host-paste.ts +38 -9
- package/src/lib/local-api.ts +1 -1
- package/src/lib/tauri-bridge.ts +5 -0
- package/src/pages/AgentPage.tsx +114 -64
- package/src/pages/SettingsPage.tsx +131 -46
- package/src/vite-env.d.ts +2 -0
- package/vite.base.js +1 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useLayoutEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from 'react';
|
|
11
|
+
|
|
12
|
+
export type ComposerStatusPanelId = 'orchestration' | 'todos';
|
|
13
|
+
|
|
14
|
+
type ComposerStatusContextValue = {
|
|
15
|
+
activeId: ComposerStatusPanelId | null;
|
|
16
|
+
claim: (id: ComposerStatusPanelId, force: boolean) => void;
|
|
17
|
+
release: (id: ComposerStatusPanelId) => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const ComposerStatusContext = createContext<ComposerStatusContextValue | null>(null);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Right-aligned column for the composer status pills (orchestration + todos).
|
|
24
|
+
* At most one panel is expanded; a user click steals the open slot, while an
|
|
25
|
+
* automatic open only claims it when nothing else is showing.
|
|
26
|
+
*/
|
|
27
|
+
export function ComposerStatusStack({ children }: { children: ReactNode }) {
|
|
28
|
+
const [activeId, setActiveId] = useState<ComposerStatusPanelId | null>(null);
|
|
29
|
+
const claim = useCallback((id: ComposerStatusPanelId, force: boolean) => {
|
|
30
|
+
setActiveId((current) => {
|
|
31
|
+
if (force || current == null || current === id) return id;
|
|
32
|
+
return current;
|
|
33
|
+
});
|
|
34
|
+
}, []);
|
|
35
|
+
const release = useCallback((id: ComposerStatusPanelId) => {
|
|
36
|
+
setActiveId((current) => (current === id ? null : current));
|
|
37
|
+
}, []);
|
|
38
|
+
const value = useMemo(
|
|
39
|
+
() => ({ activeId, claim, release }),
|
|
40
|
+
[activeId, claim, release],
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<ComposerStatusContext.Provider value={value}>
|
|
45
|
+
<div
|
|
46
|
+
className="flex min-w-0 flex-col items-end gap-1.5"
|
|
47
|
+
data-testid="composer-status-stack"
|
|
48
|
+
>
|
|
49
|
+
{children}
|
|
50
|
+
</div>
|
|
51
|
+
</ComposerStatusContext.Provider>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Fold a panel's own open preference into the stack.
|
|
57
|
+
* Outside a stack, `expanded` follows `desired` directly.
|
|
58
|
+
*/
|
|
59
|
+
export function useExclusiveExpand(
|
|
60
|
+
id: ComposerStatusPanelId,
|
|
61
|
+
desired: boolean,
|
|
62
|
+
): { expanded: boolean; requestOpen: () => void } {
|
|
63
|
+
const ctx = useContext(ComposerStatusContext);
|
|
64
|
+
const wasDesired = useRef(false);
|
|
65
|
+
|
|
66
|
+
useLayoutEffect(() => {
|
|
67
|
+
if (!ctx) return;
|
|
68
|
+
if (desired && !wasDesired.current) ctx.claim(id, false);
|
|
69
|
+
if (!desired && wasDesired.current) ctx.release(id);
|
|
70
|
+
wasDesired.current = desired;
|
|
71
|
+
}, [ctx, desired, id]);
|
|
72
|
+
|
|
73
|
+
const requestOpen = useCallback(() => {
|
|
74
|
+
ctx?.claim(id, true);
|
|
75
|
+
}, [ctx, id]);
|
|
76
|
+
|
|
77
|
+
const expanded = ctx ? desired && ctx.activeId === id : desired;
|
|
78
|
+
return { expanded, requestOpen };
|
|
79
|
+
}
|
|
@@ -29,7 +29,8 @@ export type OrchestrationNodeStatus =
|
|
|
29
29
|
| 'completed'
|
|
30
30
|
| 'failed'
|
|
31
31
|
| 'interrupted'
|
|
32
|
-
| 'closed'
|
|
32
|
+
| 'closed'
|
|
33
|
+
| 'cancelled';
|
|
33
34
|
|
|
34
35
|
export interface OrchestrationSteer {
|
|
35
36
|
message: string;
|
|
@@ -70,6 +71,77 @@ interface ActionLike {
|
|
|
70
71
|
result?: unknown;
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
function toNodeStatus(raw: string | undefined): OrchestrationNodeStatus {
|
|
75
|
+
switch (raw) {
|
|
76
|
+
case 'pending':
|
|
77
|
+
case 'running':
|
|
78
|
+
case 'completed':
|
|
79
|
+
case 'failed':
|
|
80
|
+
case 'interrupted':
|
|
81
|
+
case 'closed':
|
|
82
|
+
case 'cancelled':
|
|
83
|
+
return raw;
|
|
84
|
+
case 'error':
|
|
85
|
+
return 'failed';
|
|
86
|
+
default:
|
|
87
|
+
return 'running';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function buildOrchestrationFlow(nodes: OrchestrationChildNode[]): OrchestrationFlowData | null {
|
|
92
|
+
if (nodes.length === 0) return null;
|
|
93
|
+
|
|
94
|
+
const totalCount = nodes.length;
|
|
95
|
+
const completedCount = nodes.filter((n) => n.status === 'completed').length;
|
|
96
|
+
const runningCount = nodes.filter((n) => n.status === 'running' || n.status === 'pending').length;
|
|
97
|
+
const failedCount = nodes.filter((n) => n.status === 'failed').length;
|
|
98
|
+
const interruptedCount = nodes.filter((n) => n.status === 'interrupted').length;
|
|
99
|
+
const closedCount = nodes.filter((n) => n.status === 'closed' || n.status === 'cancelled').length;
|
|
100
|
+
const isAllCompleted = totalCount > 0 && completedCount === totalCount;
|
|
101
|
+
const hasActive = runningCount > 0;
|
|
102
|
+
|
|
103
|
+
let summaryCopy = '';
|
|
104
|
+
if (isAllCompleted) {
|
|
105
|
+
summaryCopy = `${completedCount}/${totalCount} 全部完成`;
|
|
106
|
+
} else if (hasActive) {
|
|
107
|
+
summaryCopy = `协同执行中 (${completedCount}/${totalCount} 完成)...`;
|
|
108
|
+
} else {
|
|
109
|
+
summaryCopy = `${completedCount} 完成 · ${totalCount - completedCount} 结束 (${totalCount} 个子任务)`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
nodes,
|
|
114
|
+
totalCount,
|
|
115
|
+
completedCount,
|
|
116
|
+
runningCount,
|
|
117
|
+
failedCount,
|
|
118
|
+
interruptedCount,
|
|
119
|
+
closedCount,
|
|
120
|
+
isAllCompleted,
|
|
121
|
+
hasActive,
|
|
122
|
+
summaryCopy,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 父回合已经结束时,还标着 running/pending 的分支不会再收到子代理终态
|
|
128
|
+
* (停止发生在流关闭之后)。把它们收成 cancelled,避免胶囊一直停在「协同执行中」。
|
|
129
|
+
*/
|
|
130
|
+
export function settleStoppedOrchestration(flow: OrchestrationFlowData): OrchestrationFlowData {
|
|
131
|
+
if (!flow.nodes.some((node) => node.status === 'running' || node.status === 'pending')) {
|
|
132
|
+
return flow;
|
|
133
|
+
}
|
|
134
|
+
return (
|
|
135
|
+
buildOrchestrationFlow(
|
|
136
|
+
flow.nodes.map((node) =>
|
|
137
|
+
node.status === 'running' || node.status === 'pending'
|
|
138
|
+
? { ...node, status: 'cancelled' }
|
|
139
|
+
: node,
|
|
140
|
+
),
|
|
141
|
+
) ?? flow
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
73
145
|
function safeParseJson(value: unknown): Record<string, unknown> | null {
|
|
74
146
|
if (!value) return null;
|
|
75
147
|
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
@@ -104,7 +176,7 @@ export function extractOrchestrationFlow(
|
|
|
104
176
|
nodeMap.set(c.childId, {
|
|
105
177
|
childId: c.childId,
|
|
106
178
|
task: c.task || '',
|
|
107
|
-
status: (c.status
|
|
179
|
+
status: toNodeStatus(c.status),
|
|
108
180
|
recordId: c.recordId,
|
|
109
181
|
steers: [],
|
|
110
182
|
});
|
|
@@ -189,6 +261,14 @@ export function extractOrchestrationFlow(
|
|
|
189
261
|
} else if (waitStatus === 'failed' || waitStatus === 'error' || envelope.success === false) {
|
|
190
262
|
node.status = 'failed';
|
|
191
263
|
node.error = envelope.error || '执行失败';
|
|
264
|
+
} else if (waitStatus === 'cancelled') {
|
|
265
|
+
node.status = 'cancelled';
|
|
266
|
+
} else if (waitStatus === 'interrupted') {
|
|
267
|
+
node.interrupted = true;
|
|
268
|
+
node.status = 'interrupted';
|
|
269
|
+
} else if (waitStatus === 'closed') {
|
|
270
|
+
node.closed = true;
|
|
271
|
+
if (node.status !== 'completed') node.status = 'closed';
|
|
192
272
|
} else if (waitStatus === 'running') {
|
|
193
273
|
node.status = 'running';
|
|
194
274
|
}
|
|
@@ -232,19 +312,26 @@ export function extractOrchestrationFlow(
|
|
|
232
312
|
if (rawChild && typeof rawChild === 'object') {
|
|
233
313
|
const cid = typeof rawChild.childId === 'string' ? rawChild.childId : '';
|
|
234
314
|
if (!cid) continue;
|
|
315
|
+
const listedStatus =
|
|
316
|
+
typeof rawChild.status === 'string' ? toNodeStatus(rawChild.status) : undefined;
|
|
317
|
+
const listedAnswer =
|
|
318
|
+
(typeof rawChild.answer === 'string' && rawChild.answer) ||
|
|
319
|
+
(typeof rawChild.answerPreview === 'string' && rawChild.answerPreview) ||
|
|
320
|
+
'';
|
|
235
321
|
let node = nodeMap.get(cid);
|
|
236
322
|
if (!node) {
|
|
237
323
|
node = {
|
|
238
324
|
childId: cid,
|
|
239
325
|
task: typeof rawChild.task === 'string' ? rawChild.task : '',
|
|
240
|
-
status:
|
|
241
|
-
answer:
|
|
326
|
+
status: listedStatus ?? 'running',
|
|
327
|
+
answer: listedAnswer || undefined,
|
|
242
328
|
steers: [],
|
|
243
329
|
};
|
|
244
330
|
nodeMap.set(cid, node);
|
|
245
331
|
} else {
|
|
246
332
|
if (!node.task && rawChild.task) node.task = String(rawChild.task);
|
|
247
|
-
if (!node.answer &&
|
|
333
|
+
if (!node.answer && listedAnswer) node.answer = listedAnswer;
|
|
334
|
+
if (listedStatus) node.status = listedStatus;
|
|
248
335
|
}
|
|
249
336
|
}
|
|
250
337
|
}
|
|
@@ -256,39 +343,7 @@ export function extractOrchestrationFlow(
|
|
|
256
343
|
return null;
|
|
257
344
|
}
|
|
258
345
|
|
|
259
|
-
|
|
260
|
-
if (nodes.length === 0) return null;
|
|
261
|
-
|
|
262
|
-
const totalCount = nodes.length;
|
|
263
|
-
const completedCount = nodes.filter((n) => n.status === 'completed').length;
|
|
264
|
-
const runningCount = nodes.filter((n) => n.status === 'running' || n.status === 'pending').length;
|
|
265
|
-
const failedCount = nodes.filter((n) => n.status === 'failed').length;
|
|
266
|
-
const interruptedCount = nodes.filter((n) => n.status === 'interrupted').length;
|
|
267
|
-
const closedCount = nodes.filter((n) => n.status === 'closed').length;
|
|
268
|
-
const isAllCompleted = totalCount > 0 && completedCount === totalCount;
|
|
269
|
-
const hasActive = runningCount > 0;
|
|
270
|
-
|
|
271
|
-
let summaryCopy = '';
|
|
272
|
-
if (isAllCompleted) {
|
|
273
|
-
summaryCopy = `${completedCount}/${totalCount} 全部完成`;
|
|
274
|
-
} else if (hasActive) {
|
|
275
|
-
summaryCopy = `协同执行中 (${completedCount}/${totalCount} 完成)...`;
|
|
276
|
-
} else {
|
|
277
|
-
summaryCopy = `${completedCount} 完成 · ${totalCount - completedCount} 结束 (${totalCount} 个子任务)`;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
return {
|
|
281
|
-
nodes,
|
|
282
|
-
totalCount,
|
|
283
|
-
completedCount,
|
|
284
|
-
runningCount,
|
|
285
|
-
failedCount,
|
|
286
|
-
interruptedCount,
|
|
287
|
-
closedCount,
|
|
288
|
-
isAllCompleted,
|
|
289
|
-
hasActive,
|
|
290
|
-
summaryCopy,
|
|
291
|
-
};
|
|
346
|
+
return buildOrchestrationFlow(Array.from(nodeMap.values()));
|
|
292
347
|
}
|
|
293
348
|
|
|
294
349
|
function actionsFromTimeline(blocks?: ReadonlyArray<{ type: string; actions?: unknown[] }>): ActionLike[] {
|
|
@@ -314,22 +369,32 @@ export function resolveLatestSessionOrchestrationFlow(input: {
|
|
|
314
369
|
orchestrationChildrenByMessageId?: Record<string, ChildInfo[]>;
|
|
315
370
|
timelineByMessageId?: Record<string, Array<{ type: string; actions?: unknown[] }>>;
|
|
316
371
|
currentTurnTimeline?: Array<{ type: string; actions?: unknown[] }>;
|
|
372
|
+
/** 父回合仍在流式输出。结束后残留的 running 分支收成已停止。 */
|
|
373
|
+
turnActive?: boolean;
|
|
317
374
|
}): OrchestrationFlowData | null {
|
|
375
|
+
const finish = (flow: OrchestrationFlowData | null, active: boolean) => {
|
|
376
|
+
if (!flow || flow.totalCount === 0) return null;
|
|
377
|
+
return active ? flow : settleStoppedOrchestration(flow);
|
|
378
|
+
};
|
|
379
|
+
|
|
318
380
|
// 1. 优先实时回合
|
|
319
381
|
const liveActions =
|
|
320
382
|
input.currentTurnActions ?? actionsFromTimeline(input.currentTurnTimeline);
|
|
321
|
-
const liveFlow =
|
|
322
|
-
|
|
383
|
+
const liveFlow = finish(
|
|
384
|
+
extractOrchestrationFlow(liveActions, input.currentTurnChildren),
|
|
385
|
+
input.turnActive !== false,
|
|
386
|
+
);
|
|
387
|
+
if (liveFlow) return liveFlow;
|
|
323
388
|
|
|
324
|
-
// 2.
|
|
389
|
+
// 2. 倒序历史回合。落库的回合已经结束,残留 running 同样收成已停止。
|
|
325
390
|
for (let i = input.messages.length - 1; i >= 0; i -= 1) {
|
|
326
391
|
const id = input.messages[i].id;
|
|
327
392
|
const actions =
|
|
328
393
|
input.executedActionsByMessageId?.[id] ??
|
|
329
394
|
actionsFromTimeline(input.timelineByMessageId?.[id]);
|
|
330
395
|
const children = input.orchestrationChildrenByMessageId?.[id];
|
|
331
|
-
const flow = extractOrchestrationFlow(actions, children);
|
|
332
|
-
if (flow
|
|
396
|
+
const flow = finish(extractOrchestrationFlow(actions, children), false);
|
|
397
|
+
if (flow) return flow;
|
|
333
398
|
}
|
|
334
399
|
|
|
335
400
|
return null;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Chat id from `#/agent/:chatId` (hash router) or `/agent/:chatId`. */
|
|
2
|
+
export function chatIdFromLocation(location: { pathname: string; hash: string }): string | null {
|
|
3
|
+
const hashed = location.hash.startsWith('#') ? location.hash.slice(1) : '';
|
|
4
|
+
const raw = hashed || location.pathname;
|
|
5
|
+
const match = raw.match(/\/agent\/([^/?#]+)/);
|
|
6
|
+
if (!match?.[1]) return null;
|
|
7
|
+
try {
|
|
8
|
+
return decodeURIComponent(match[1]);
|
|
9
|
+
} catch {
|
|
10
|
+
return match[1];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Stamp an unscoped prompt with the chat open when it arrived. */
|
|
15
|
+
export function bindPromptToActiveChat<T extends { chatId?: string }>(request: T): T {
|
|
16
|
+
if (request.chatId) return request;
|
|
17
|
+
const chatId = chatIdFromLocation(window.location);
|
|
18
|
+
if (!chatId) return request;
|
|
19
|
+
return { ...request, chatId };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A prompt with a chat id belongs only to that chat.
|
|
24
|
+
* A prompt without one stays visible only when the composer itself has no chat,
|
|
25
|
+
* so an older request is still answerable.
|
|
26
|
+
*/
|
|
27
|
+
export function promptVisibleInChat(
|
|
28
|
+
request: { chatId?: string },
|
|
29
|
+
chatId: string | null | undefined,
|
|
30
|
+
): boolean {
|
|
31
|
+
if (!request.chatId) return !chatId;
|
|
32
|
+
return request.chatId === chatId;
|
|
33
|
+
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
LuPlus,
|
|
6
6
|
LuTrash2,
|
|
7
7
|
} from 'react-icons/lu';
|
|
8
|
+
import { ConfirmDialog } from '@/components/ConfirmDialog';
|
|
8
9
|
import { isElectron } from '@/lib/electron-bridge';
|
|
9
10
|
import {
|
|
10
11
|
archiveChatAgent,
|
|
@@ -22,7 +23,7 @@ import {
|
|
|
22
23
|
/**
|
|
23
24
|
* AgentsSettingsPanel — 智能体目录(列表 / 新建 / 编辑 / 归档)。
|
|
24
25
|
*
|
|
25
|
-
* 渲染在 `/settings?section=
|
|
26
|
+
* 渲染在 `/settings?section=plugins` 的智能体分类。自定义智能体出现在输入框
|
|
26
27
|
* 上方的专家选择器里。内置智能体可改文案与能力,不能归档。
|
|
27
28
|
*
|
|
28
29
|
* 除文案外,每个智能体还持有真实生效的能力面(勾选技能 / 技能范围 /
|
|
@@ -91,7 +92,10 @@ export function AgentsSettingsPanel({
|
|
|
91
92
|
const [capability, setCapability] = useState<AgentCapabilityDraft>(
|
|
92
93
|
DEFAULT_CAPABILITY_DRAFT,
|
|
93
94
|
);
|
|
94
|
-
const [
|
|
95
|
+
const [pendingArchive, setPendingArchive] = useState<{
|
|
96
|
+
id: string;
|
|
97
|
+
name: string;
|
|
98
|
+
} | null>(null);
|
|
95
99
|
const [archivingId, setArchivingId] = useState<string | null>(null);
|
|
96
100
|
const [error, setError] = useState<string | null>(null);
|
|
97
101
|
|
|
@@ -179,17 +183,14 @@ export function AgentsSettingsPanel({
|
|
|
179
183
|
}
|
|
180
184
|
};
|
|
181
185
|
|
|
182
|
-
const handleArchive = async (
|
|
183
|
-
if (
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
setArchivingId(agent.id);
|
|
186
|
+
const handleArchive = async () => {
|
|
187
|
+
if (!pendingArchive || archivingId === pendingArchive.id) return;
|
|
188
|
+
const id = pendingArchive.id;
|
|
189
|
+
setArchivingId(id);
|
|
189
190
|
setError(null);
|
|
190
191
|
try {
|
|
191
|
-
await archiveChatAgent(
|
|
192
|
-
|
|
192
|
+
await archiveChatAgent(id);
|
|
193
|
+
setPendingArchive(null);
|
|
193
194
|
await fetchAgents();
|
|
194
195
|
await onCatalogChange?.();
|
|
195
196
|
} catch (err) {
|
|
@@ -371,18 +372,12 @@ export function AgentsSettingsPanel({
|
|
|
371
372
|
{!agent.isBuiltin && (
|
|
372
373
|
<button
|
|
373
374
|
type="button"
|
|
374
|
-
onClick={() =>
|
|
375
|
-
|
|
376
|
-
className={`rounded-full p-1 transition-colors ${
|
|
377
|
-
confirmArchiveId === agent.id
|
|
378
|
-
? 'bg-agent-destructive/10 text-agent-destructive hover:bg-agent-destructive/20'
|
|
379
|
-
: 'text-agent-muted-foreground hover:bg-agent-muted hover:text-agent-destructive'
|
|
380
|
-
}`}
|
|
381
|
-
title={
|
|
382
|
-
confirmArchiveId === agent.id
|
|
383
|
-
? '再次点击确认删除'
|
|
384
|
-
: '删除'
|
|
375
|
+
onClick={() =>
|
|
376
|
+
setPendingArchive({ id: agent.id, name: agent.name })
|
|
385
377
|
}
|
|
378
|
+
disabled={archivingId === agent.id}
|
|
379
|
+
className="rounded-full p-1 text-agent-muted-foreground transition-colors hover:bg-agent-muted hover:text-agent-destructive"
|
|
380
|
+
title="删除"
|
|
386
381
|
data-testid={`agent-archive-${agent.id}`}
|
|
387
382
|
>
|
|
388
383
|
{archivingId === agent.id ? (
|
|
@@ -407,6 +402,22 @@ export function AgentsSettingsPanel({
|
|
|
407
402
|
{error}
|
|
408
403
|
</div>
|
|
409
404
|
)}
|
|
405
|
+
<ConfirmDialog
|
|
406
|
+
open={pendingArchive !== null}
|
|
407
|
+
title="删除智能体"
|
|
408
|
+
description={
|
|
409
|
+
pendingArchive
|
|
410
|
+
? `确定删除「${pendingArchive.name}」?它会从专家选择器中移除,已有会话不受影响。`
|
|
411
|
+
: ''
|
|
412
|
+
}
|
|
413
|
+
pending={pendingArchive !== null && archivingId === pendingArchive.id}
|
|
414
|
+
onCancel={() => {
|
|
415
|
+
if (archivingId) return;
|
|
416
|
+
setPendingArchive(null);
|
|
417
|
+
}}
|
|
418
|
+
onConfirm={() => void handleArchive()}
|
|
419
|
+
testId="agent-archive-dialog"
|
|
420
|
+
/>
|
|
410
421
|
</div>
|
|
411
422
|
);
|
|
412
423
|
}
|