@steerable/agent-shell-web 0.6.64 → 0.6.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/components/AgentSidebar.tsx +278 -18
- package/src/components/ChatHeader.tsx +666 -277
- package/src/components/chat/MessageList.tsx +58 -1
- package/src/components/chat/TaskOutcomeCards.tsx +2 -2
- package/src/components/chat/turn-files.ts +57 -0
- package/src/hooks/useProjects.ts +30 -1
- package/src/layouts/AgentLayout.tsx +279 -127
- package/src/layouts/PanelToggleButton.tsx +35 -0
- package/src/layouts/RightPanelTabBar.tsx +214 -0
- package/src/layouts/conversation-preview.ts +87 -0
- package/src/layouts/request-chat-slot.ts +65 -0
- package/src/layouts/right-panel-tabs.ts +309 -0
- package/src/lib/local-api.ts +11 -0
- package/src/lib/project-order.ts +48 -0
- package/src/packs/registry.ts +2 -0
- package/src/pages/AgentPage.tsx +13 -2
|
@@ -3,6 +3,7 @@ import { Outlet, useParams } from 'react-router-dom';
|
|
|
3
3
|
import { LuPanelLeftOpen } from 'react-icons/lu';
|
|
4
4
|
import { AgentSidebar } from '@/components/AgentSidebar';
|
|
5
5
|
import { TerminalPanel } from '@/components/TerminalPanel';
|
|
6
|
+
import { RightPanelTabBar } from '@/layouts/RightPanelTabBar';
|
|
6
7
|
import { TaskProcessPanel, type InspectedTask } from '@/components/chat/TaskProcessPanel';
|
|
7
8
|
import { AskUserPromptProvider } from '@/components/chat/AskUserPromptProvider';
|
|
8
9
|
import { ApprovalPromptProvider } from '@/components/chat/ApprovalPromptProvider';
|
|
@@ -18,6 +19,32 @@ import {
|
|
|
18
19
|
} from '@/hooks/useChatsAndAgents';
|
|
19
20
|
import { hostToolChrome, sanitizeRightPanelKind, settingsChrome } from '@/lib/host-tools';
|
|
20
21
|
import { t } from '@/i18n';
|
|
22
|
+
import {
|
|
23
|
+
closeRightPanelTab,
|
|
24
|
+
collapseRightPanel,
|
|
25
|
+
createRightPanelTab,
|
|
26
|
+
expandRightPanel,
|
|
27
|
+
followRightPanelTab,
|
|
28
|
+
openRightPanelTab,
|
|
29
|
+
parseRightPanelMap,
|
|
30
|
+
revealRightPanelTab,
|
|
31
|
+
serializeRightPanelMap,
|
|
32
|
+
shouldMountRightPanelTab,
|
|
33
|
+
toggleRightPanelEntry,
|
|
34
|
+
type RightPanelMap,
|
|
35
|
+
type RightPanelTabRecord,
|
|
36
|
+
type RightPanelTabTarget,
|
|
37
|
+
type RightPanelTabs,
|
|
38
|
+
} from '@/layouts/right-panel-tabs';
|
|
39
|
+
import {
|
|
40
|
+
parseLegacyChatSlotTabId,
|
|
41
|
+
subscribeChatSlotFollows,
|
|
42
|
+
subscribeChatSlotRequests,
|
|
43
|
+
type ChatSlotRequest,
|
|
44
|
+
} from '@/layouts/request-chat-slot';
|
|
45
|
+
|
|
46
|
+
export { parseRightPanelMap };
|
|
47
|
+
export type { RightPanelMap, RightPanelTabs };
|
|
21
48
|
|
|
22
49
|
/**
|
|
23
50
|
* AgentLayout wraps `/agent`, `/agent/:chatId`, and `/` (default) with a
|
|
@@ -64,57 +91,27 @@ const MIN_SIDEBAR_WIDTH = 180;
|
|
|
64
91
|
const MAX_SIDEBAR_WIDTH = 420;
|
|
65
92
|
|
|
66
93
|
const TERMINAL_OPEN_KEY = 'deeppath.agent.terminalOpen';
|
|
67
|
-
const RIGHT_PANEL_KEY = 'deeppath.agent.rightPanel';
|
|
94
|
+
const RIGHT_PANEL_KEY = 'deeppath.agent.rightPanel.v2';
|
|
95
|
+
const LEGACY_RIGHT_PANEL_KEY = 'deeppath.agent.rightPanel';
|
|
68
96
|
const TERMINAL_WIDTH_KEY = 'deeppath.agent.terminalWidth';
|
|
69
97
|
const DEFAULT_TERMINAL_WIDTH = 520;
|
|
70
98
|
const MIN_TERMINAL_WIDTH = 360;
|
|
71
99
|
const MAX_TERMINAL_WIDTH = 960;
|
|
72
100
|
|
|
73
|
-
/**
|
|
74
|
-
* 右侧栏位状态:null = 都不打开,'terminal' = 终端,其余字符串 = 包注册的
|
|
75
|
-
* 聊天页槽位(chatSlots,如文档包的预览栏位)。栏位同一时刻只开一个
|
|
76
|
-
* (互斥规则在本布局,不在包)。
|
|
77
|
-
*/
|
|
101
|
+
/** 当前看见的标签。null = 这一侧没有打开的标签。 */
|
|
78
102
|
export type RightPanelState = string | null;
|
|
79
103
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
* 无效的会话 / 栏位值会被丢弃,避免脏数据把某个会话卡在打不开的面板上。
|
|
91
|
-
*/
|
|
92
|
-
export function parseRightPanelMap(options: {
|
|
93
|
-
raw: string | null;
|
|
94
|
-
legacyTerminalOpen: string | null;
|
|
95
|
-
chatId: string | null;
|
|
96
|
-
isValidValue: (value: string) => boolean;
|
|
97
|
-
}): RightPanelMap {
|
|
98
|
-
const { raw, legacyTerminalOpen, chatId, isValidValue } = options;
|
|
99
|
-
if (raw !== null) {
|
|
100
|
-
if (raw === '') return {};
|
|
101
|
-
try {
|
|
102
|
-
const parsed = JSON.parse(raw) as unknown;
|
|
103
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
104
|
-
const map: RightPanelMap = {};
|
|
105
|
-
for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {
|
|
106
|
-
if (typeof value === 'string' && isValidValue(value)) map[key] = value;
|
|
107
|
-
}
|
|
108
|
-
return map;
|
|
109
|
-
}
|
|
110
|
-
} catch {
|
|
111
|
-
// 旧格式:整个 key 就是单个值。
|
|
112
|
-
if (isValidValue(raw) && chatId) return { [chatId]: raw };
|
|
113
|
-
}
|
|
114
|
-
return {};
|
|
104
|
+
function contentTitle(contentId: string, fallback: string): string {
|
|
105
|
+
if (contentId.startsWith('/') || /^[A-Za-z]:[\\/]/.test(contentId)) {
|
|
106
|
+
const parts = contentId.split(/[/\\]/);
|
|
107
|
+
return parts.at(-1) || fallback;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const url = new URL(contentId);
|
|
111
|
+
return url.pathname.split('/').filter(Boolean).at(-1) || url.hostname || fallback;
|
|
112
|
+
} catch {
|
|
113
|
+
return fallback;
|
|
115
114
|
}
|
|
116
|
-
if (legacyTerminalOpen === '1' && chatId) return { [chatId]: 'terminal' };
|
|
117
|
-
return {};
|
|
118
115
|
}
|
|
119
116
|
|
|
120
117
|
/** 从聊天页外把一段内容作为普通用户消息发进当前会话(包槽位的 fallback 通道)。 */
|
|
@@ -131,10 +128,12 @@ export type AgentOutletContext = UseChatsAndAgentsResult & {
|
|
|
131
128
|
inspectTask: (task: InspectedTask) => void;
|
|
132
129
|
/** 包槽位(如文档预览):入口渲染在 chat 标题栏,状态按会话隔离。 */
|
|
133
130
|
chatSlots: readonly PackChatSlotContribution[];
|
|
134
|
-
/**
|
|
131
|
+
/** 当前会话正在看的右侧标签(null = 整栏关着或收起)。 */
|
|
135
132
|
rightPanel: RightPanelState;
|
|
136
|
-
/**
|
|
137
|
-
|
|
133
|
+
/** 当前会话已经打开且正在显示的标签,顺序与标签条一致。 */
|
|
134
|
+
openPanelIds: readonly string[];
|
|
135
|
+
/** 标题栏开关:整栏收着或空着时打开。 */
|
|
136
|
+
onOpenRightPanel: () => void;
|
|
138
137
|
};
|
|
139
138
|
|
|
140
139
|
function AgentLayoutContent() {
|
|
@@ -328,38 +327,52 @@ function AgentLayoutContent() {
|
|
|
328
327
|
}, []);
|
|
329
328
|
|
|
330
329
|
// ───── Right-side panel (embedded column beside the chat content) ─────
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
// 状态,首次读取时迁移。
|
|
330
|
+
// 终端与包槽位(PPT、Word、Markdown)共用一条标签。可以同时打开多个,
|
|
331
|
+
// 标签条切换当前看见的那一个。持久化按会话存标签列表;老的单值会迁成一个标签。
|
|
334
332
|
const packChatSlots = getPackChatSlots();
|
|
335
333
|
|
|
334
|
+
const resolveTabTarget = useCallback((request: ChatSlotRequest): RightPanelTabTarget | null => {
|
|
335
|
+
const kind = sanitizeRightPanelKind(request.kind);
|
|
336
|
+
if (!kind) return null;
|
|
337
|
+
if (kind === 'terminal') {
|
|
338
|
+
return { kind, contentId: 'terminal', title: t('Terminal') };
|
|
339
|
+
}
|
|
340
|
+
const slot = packChatSlots.find((item) => item.slotId === kind);
|
|
341
|
+
if (!slot) return null;
|
|
342
|
+
const contentId = request.contentId || slot.defaultContentId || kind;
|
|
343
|
+
return {
|
|
344
|
+
kind,
|
|
345
|
+
contentId,
|
|
346
|
+
title: request.title || contentTitle(contentId, slot.title),
|
|
347
|
+
};
|
|
348
|
+
}, [packChatSlots]);
|
|
349
|
+
|
|
336
350
|
/**
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
* 以前是单个全局值(localStorage 里存一个字符串),会话 1 打开预览会连带
|
|
340
|
-
* 影响会话 2。现在存成映射,每个会话独立;老的单值 key 读取时自动迁移到
|
|
341
|
-
* 当前会话。
|
|
351
|
+
* 右侧标签按会话隔离。以前是单个全局值,会话 1 打开预览会连带影响会话 2。
|
|
352
|
+
* 现在存成映射,每个会话独立;老的单值 key 读取时自动迁移到当前会话。
|
|
342
353
|
*/
|
|
343
354
|
const readRightPanelMap = useCallback((): RightPanelMap => {
|
|
344
|
-
const
|
|
345
|
-
|
|
355
|
+
const isValidKind = (kind: string): boolean =>
|
|
356
|
+
kind === 'terminal' || packChatSlots.some((slot) => slot.slotId === kind);
|
|
357
|
+
const resolveLegacyTab = (value: string): RightPanelTabTarget | null =>
|
|
358
|
+
resolveTabTarget(parseLegacyChatSlotTabId(value));
|
|
346
359
|
try {
|
|
360
|
+
const current = window.localStorage.getItem(RIGHT_PANEL_KEY);
|
|
347
361
|
const map = parseRightPanelMap({
|
|
348
|
-
raw: window.localStorage.getItem(
|
|
362
|
+
raw: current ?? window.localStorage.getItem(LEGACY_RIGHT_PANEL_KEY),
|
|
349
363
|
legacyTerminalOpen: window.localStorage.getItem(TERMINAL_OPEN_KEY),
|
|
350
364
|
chatId: chatId ?? null,
|
|
351
|
-
|
|
365
|
+
isValidKind,
|
|
366
|
+
resolveLegacyTab,
|
|
352
367
|
});
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const next = sanitizeRightPanelKind(value);
|
|
356
|
-
if (next) sanitized[id] = next;
|
|
368
|
+
if (current === null && Object.keys(map).length > 0) {
|
|
369
|
+
window.localStorage.setItem(RIGHT_PANEL_KEY, serializeRightPanelMap(map));
|
|
357
370
|
}
|
|
358
|
-
return
|
|
371
|
+
return map;
|
|
359
372
|
} catch {
|
|
360
373
|
return {};
|
|
361
374
|
}
|
|
362
|
-
}, [chatId, packChatSlots]);
|
|
375
|
+
}, [chatId, packChatSlots, resolveTabTarget]);
|
|
363
376
|
|
|
364
377
|
const [rightPanelMap, setRightPanelMap] = useState<RightPanelMap>(() =>
|
|
365
378
|
readRightPanelMap(),
|
|
@@ -370,19 +383,30 @@ function AgentLayoutContent() {
|
|
|
370
383
|
setRightPanelMap(readRightPanelMap());
|
|
371
384
|
}, [readRightPanelMap]);
|
|
372
385
|
|
|
373
|
-
const
|
|
386
|
+
const panelEntry: RightPanelTabs | null = chatId ? rightPanelMap[chatId] ?? null : null;
|
|
387
|
+
const panelOpen = panelEntry !== null && panelEntry.collapsed !== true;
|
|
388
|
+
const activePanelTab = panelOpen
|
|
389
|
+
? panelEntry.tabs.find((tab) => tab.id === panelEntry.activeTabId) ?? null
|
|
390
|
+
: null;
|
|
391
|
+
const rightPanel: RightPanelState = activePanelTab?.kind ?? null;
|
|
392
|
+
const openPanelIds = panelOpen
|
|
393
|
+
? [...new Set(panelEntry.tabs.map((tab) => tab.kind))]
|
|
394
|
+
: [];
|
|
395
|
+
// 自动展开 / 手动切换回调是挂载时注册的,直接读 state 会拿到过期闭包。
|
|
396
|
+
const panelEntryRef = useRef<RightPanelTabs | null>(panelEntry);
|
|
374
397
|
|
|
375
398
|
const persistRightPanelMap = useCallback((map: RightPanelMap) => {
|
|
376
399
|
try {
|
|
377
|
-
window.localStorage.setItem(RIGHT_PANEL_KEY,
|
|
400
|
+
window.localStorage.setItem(RIGHT_PANEL_KEY, serializeRightPanelMap(map));
|
|
378
401
|
} catch {
|
|
379
402
|
/* ignore */
|
|
380
403
|
}
|
|
381
404
|
}, []);
|
|
382
405
|
|
|
383
|
-
const
|
|
384
|
-
(next:
|
|
406
|
+
const applyPanelEntry = useCallback(
|
|
407
|
+
(next: RightPanelTabs | null) => {
|
|
385
408
|
if (!chatId) return;
|
|
409
|
+
panelEntryRef.current = next;
|
|
386
410
|
setRightPanelMap((prev) => {
|
|
387
411
|
const map = { ...prev };
|
|
388
412
|
if (next === null) delete map[chatId];
|
|
@@ -394,29 +418,105 @@ function AgentLayoutContent() {
|
|
|
394
418
|
[chatId, persistRightPanelMap],
|
|
395
419
|
);
|
|
396
420
|
|
|
397
|
-
// 自动展开 / 手动切换回调是挂载时注册的,直接读 state 会拿到过期闭包,
|
|
398
|
-
// 所以用 ref 跟踪当前栏位状态(见下方同步 effect)。
|
|
399
|
-
const rightPanelRef = useRef<RightPanelState>(rightPanel);
|
|
400
|
-
|
|
401
421
|
useEffect(() => {
|
|
402
|
-
|
|
403
|
-
}, [
|
|
422
|
+
panelEntryRef.current = panelEntry;
|
|
423
|
+
}, [panelEntry]);
|
|
424
|
+
|
|
425
|
+
const closePanelTab = useCallback(
|
|
426
|
+
(tabId: string) => {
|
|
427
|
+
const current = panelEntryRef.current;
|
|
428
|
+
if (!current) return;
|
|
429
|
+
const tab = current.tabs.find((item) => item.id === tabId);
|
|
430
|
+
if (!tab) return;
|
|
431
|
+
const slot = packChatSlots.find((item) => item.slotId === tab.kind);
|
|
432
|
+
if (chatId && slot?.onContentClosed) {
|
|
433
|
+
slot.onContentClosed({ chatId, contentId: tab.contentId });
|
|
434
|
+
}
|
|
435
|
+
applyPanelEntry(closeRightPanelTab(current, tabId));
|
|
436
|
+
},
|
|
437
|
+
[applyPanelEntry, chatId, packChatSlots],
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
const activatePanelTab = useCallback(
|
|
441
|
+
(tabId: string) => {
|
|
442
|
+
const current = panelEntryRef.current;
|
|
443
|
+
if (!current || !current.tabs.some((tab) => tab.id === tabId)) return;
|
|
444
|
+
setInspectedTask(null);
|
|
445
|
+
if (current.activeTabId === tabId) return;
|
|
446
|
+
applyPanelEntry({ tabs: current.tabs, activeTabId: tabId });
|
|
447
|
+
},
|
|
448
|
+
[applyPanelEntry],
|
|
449
|
+
);
|
|
404
450
|
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
451
|
+
const tabForRequest = useCallback((request: ChatSlotRequest): RightPanelTabRecord | null => {
|
|
452
|
+
const target = resolveTabTarget(request);
|
|
453
|
+
return target ? createRightPanelTab(target) : null;
|
|
454
|
+
}, [resolveTabTarget]);
|
|
455
|
+
|
|
456
|
+
const openPanelTab = useCallback(
|
|
457
|
+
(request: ChatSlotRequest) => {
|
|
458
|
+
const tab = tabForRequest(request);
|
|
459
|
+
if (!tab) return;
|
|
460
|
+
setInspectedTask(null);
|
|
461
|
+
applyPanelEntry(openRightPanelTab(panelEntryRef.current, tab));
|
|
462
|
+
},
|
|
463
|
+
[applyPanelEntry, tabForRequest],
|
|
464
|
+
);
|
|
408
465
|
|
|
409
|
-
|
|
410
|
-
|
|
466
|
+
const openPanelKind = useCallback((kind: string) => {
|
|
467
|
+
if (kind === 'terminal') {
|
|
468
|
+
openPanelTab({ kind });
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const slot = packChatSlots.find((item) => item.slotId === kind);
|
|
472
|
+
if (!slot) return;
|
|
473
|
+
const contentId = slot.multiple ? slot.createContentId?.() : undefined;
|
|
474
|
+
if (slot.multiple && !contentId) return;
|
|
475
|
+
openPanelTab({ kind, contentId });
|
|
476
|
+
}, [openPanelTab, packChatSlots]);
|
|
477
|
+
|
|
478
|
+
const expandRightSide = useCallback(() => {
|
|
479
|
+
const current = panelEntryRef.current;
|
|
480
|
+
if (current && current.tabs.length > 0) {
|
|
481
|
+
setInspectedTask(null);
|
|
482
|
+
applyPanelEntry(expandRightPanel(current));
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const first = packChatSlots[0]?.slotId ?? sanitizeRightPanelKind('terminal');
|
|
486
|
+
if (!first) return;
|
|
487
|
+
setInspectedTask(null);
|
|
488
|
+
openPanelKind(first);
|
|
489
|
+
}, [openPanelKind, packChatSlots]);
|
|
490
|
+
|
|
491
|
+
const collapseRightSide = useCallback(() => {
|
|
492
|
+
const current = panelEntryRef.current;
|
|
493
|
+
if (!current || current.tabs.length === 0) return;
|
|
494
|
+
setInspectedTask(null);
|
|
495
|
+
applyPanelEntry(collapseRightPanel(current));
|
|
496
|
+
}, [applyPanelEntry]);
|
|
497
|
+
|
|
498
|
+
/** 快捷键:没开就追加并显示,已开但不是当前就切过去,当前这个就关掉。整栏收着时改为打开。 */
|
|
411
499
|
const toggleRightPanel = useCallback(
|
|
412
500
|
(kind: string) => {
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
501
|
+
const sanitized = sanitizeRightPanelKind(kind);
|
|
502
|
+
if (!sanitized) return;
|
|
503
|
+
const current = panelEntryRef.current;
|
|
504
|
+
const active = current?.tabs.find((tab) => tab.id === current.activeTabId);
|
|
505
|
+
const matching = active?.kind === sanitized
|
|
506
|
+
? active
|
|
507
|
+
: [...(current?.tabs ?? [])].reverse().find((tab) => tab.kind === sanitized);
|
|
508
|
+
const requested = matching ?? tabForRequest({
|
|
509
|
+
kind: sanitized,
|
|
510
|
+
contentId: packChatSlots.find((slot) => slot.slotId === sanitized)?.multiple
|
|
511
|
+
? packChatSlots.find((slot) => slot.slotId === sanitized)?.createContentId?.()
|
|
512
|
+
: undefined,
|
|
513
|
+
});
|
|
514
|
+
if (!requested) return;
|
|
515
|
+
const next = toggleRightPanelEntry(current, requested);
|
|
416
516
|
if (next !== null) setInspectedTask(null);
|
|
417
|
-
|
|
517
|
+
applyPanelEntry(next);
|
|
418
518
|
},
|
|
419
|
-
[
|
|
519
|
+
[applyPanelEntry, packChatSlots, tabForRequest],
|
|
420
520
|
);
|
|
421
521
|
|
|
422
522
|
// Cmd+T from the app menu — wired in src/main.ts:createMenu (sends
|
|
@@ -480,20 +580,23 @@ function AgentLayoutContent() {
|
|
|
480
580
|
const showTerminalFromTask = useCallback(() => {
|
|
481
581
|
if (!hostToolChrome('terminal')) return;
|
|
482
582
|
setInspectedTask(null);
|
|
483
|
-
|
|
484
|
-
|
|
583
|
+
const tab = tabForRequest({ kind: 'terminal' });
|
|
584
|
+
if (tab) applyPanelEntry(openRightPanelTab(panelEntryRef.current, tab));
|
|
585
|
+
}, [applyPanelEntry, tabForRequest]);
|
|
485
586
|
|
|
486
|
-
//
|
|
487
|
-
//
|
|
488
|
-
// 已打开任一面板时,自动展开请求被忽略。订阅逻辑由包自己实现。
|
|
587
|
+
// 包槽位的自动展开(如文档包:后端在本轮产出新稿时广播)。
|
|
588
|
+
// 栏位空着时打开并显示;已经有别的标签时只追加,不抢走当前标签。
|
|
489
589
|
useEffect(() => {
|
|
490
590
|
const cleanups: Array<() => void> = [];
|
|
491
591
|
for (const slot of packChatSlots) {
|
|
492
592
|
const cleanup = slot.setupAutoReveal?.({
|
|
493
|
-
reveal: () => {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
593
|
+
reveal: (target = {}) => {
|
|
594
|
+
const tab = tabForRequest({ kind: slot.slotId, ...target });
|
|
595
|
+
if (!tab) return;
|
|
596
|
+
const next = revealRightPanelTab(panelEntryRef.current, tab);
|
|
597
|
+
if (next === panelEntryRef.current) return;
|
|
598
|
+
if (panelEntryRef.current === null) setInspectedTask(null);
|
|
599
|
+
applyPanelEntry(next);
|
|
497
600
|
},
|
|
498
601
|
getCurrentChatId: () => chatId ?? null,
|
|
499
602
|
});
|
|
@@ -502,9 +605,19 @@ function AgentLayoutContent() {
|
|
|
502
605
|
return () => {
|
|
503
606
|
for (const cleanup of cleanups) cleanup();
|
|
504
607
|
};
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
608
|
+
}, [chatId, applyPanelEntry, tabForRequest]);
|
|
609
|
+
|
|
610
|
+
// 交付文件点击要打开对应栏位。菜单按钮不在文档里时也能切过去。
|
|
611
|
+
useEffect(() => subscribeChatSlotRequests(openPanelTab), [openPanelTab]);
|
|
612
|
+
|
|
613
|
+
// 对话滚动或新产物换右侧正在看的文件。关着的栏不弹出,终端不被抢走。
|
|
614
|
+
useEffect(() => subscribeChatSlotFollows((request) => {
|
|
615
|
+
const tab = tabForRequest(request);
|
|
616
|
+
if (!tab || tab.kind === 'terminal') return;
|
|
617
|
+
const next = followRightPanelTab(panelEntryRef.current, tab);
|
|
618
|
+
if (!next || next === panelEntryRef.current) return;
|
|
619
|
+
applyPanelEntry(next);
|
|
620
|
+
}), [applyPanelEntry, tabForRequest]);
|
|
508
621
|
|
|
509
622
|
// Same drag ergonomics as the sidebar handle, mirrored: the terminal's
|
|
510
623
|
// right edge is pinned to the window's right padding (p-1.5 = 6px), so the
|
|
@@ -610,13 +723,14 @@ function AgentLayoutContent() {
|
|
|
610
723
|
inspectTask,
|
|
611
724
|
chatSlots: packChatSlots,
|
|
612
725
|
rightPanel,
|
|
613
|
-
|
|
726
|
+
openPanelIds,
|
|
727
|
+
onOpenRightPanel: expandRightSide,
|
|
614
728
|
} satisfies AgentOutletContext}
|
|
615
729
|
/>
|
|
616
730
|
</div>
|
|
617
731
|
{settingsChrome('insights') && <InsightsConsentBanner />}
|
|
618
732
|
</div>
|
|
619
|
-
{(
|
|
733
|
+
{(panelOpen || inspectedTask !== null) && (
|
|
620
734
|
<>
|
|
621
735
|
<div
|
|
622
736
|
role="separator"
|
|
@@ -628,40 +742,78 @@ function AgentLayoutContent() {
|
|
|
628
742
|
title={t('Drag to resize the right panel (double-click to reset)')}
|
|
629
743
|
/>
|
|
630
744
|
<div
|
|
631
|
-
className=
|
|
632
|
-
rightPanel === 'terminal' && !inspectedTask ? 'bg-[#0b0b0c]' : 'bg-agent-canvas'
|
|
633
|
-
}`}
|
|
745
|
+
className="flex h-full flex-shrink-0 flex-col overflow-hidden rounded-agent-lg bg-agent-canvas shadow-sm"
|
|
634
746
|
style={{ width: terminalWidth }}
|
|
635
747
|
data-testid={inspectedTask ? 'task-process-dock' : 'terminal-panel'}
|
|
636
748
|
>
|
|
637
|
-
{
|
|
638
|
-
<
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
onClose={closeRightPanel}
|
|
648
|
-
onShowTaskProcess={recentTask ? showRecentTask : undefined}
|
|
649
|
-
taskProcessTitle={recentTask?.title}
|
|
749
|
+
{panelOpen && panelEntry && (
|
|
750
|
+
<RightPanelTabBar
|
|
751
|
+
tabs={panelEntry.tabs}
|
|
752
|
+
activeTabId={panelEntry.activeTabId}
|
|
753
|
+
slots={packChatSlots}
|
|
754
|
+
showTerminal={hostToolChrome('terminal')}
|
|
755
|
+
onActivate={activatePanelTab}
|
|
756
|
+
onClose={closePanelTab}
|
|
757
|
+
onOpen={openPanelKind}
|
|
758
|
+
onCollapse={collapseRightSide}
|
|
650
759
|
/>
|
|
651
|
-
) : (
|
|
652
|
-
(() => {
|
|
653
|
-
const slot = packChatSlots.find((s) => s.slotId === rightPanel);
|
|
654
|
-
if (!slot) return null;
|
|
655
|
-
const SlotPanel = slot.Component;
|
|
656
|
-
return (
|
|
657
|
-
<SlotPanel
|
|
658
|
-
chatId={chatId ?? ''}
|
|
659
|
-
onClose={closeRightPanel}
|
|
660
|
-
onSubmitToChat={sendChatMessage}
|
|
661
|
-
/>
|
|
662
|
-
);
|
|
663
|
-
})()
|
|
664
760
|
)}
|
|
761
|
+
<div className="relative min-h-0 flex-1">
|
|
762
|
+
{inspectedTask ? (
|
|
763
|
+
<TaskProcessPanel
|
|
764
|
+
inspected={inspectedTask}
|
|
765
|
+
onClose={closeTaskProcess}
|
|
766
|
+
onShowTerminal={
|
|
767
|
+
hostToolChrome('terminal') ? showTerminalFromTask : undefined
|
|
768
|
+
}
|
|
769
|
+
/>
|
|
770
|
+
) : (
|
|
771
|
+
panelEntry?.tabs.map((tab) => {
|
|
772
|
+
const visible = tab.id === panelEntry.activeTabId;
|
|
773
|
+
const slot = packChatSlots.find((item) => item.slotId === tab.kind);
|
|
774
|
+
const keepMounted = tab.kind === 'terminal' || slot?.keepMounted === true;
|
|
775
|
+
if (!shouldMountRightPanelTab(
|
|
776
|
+
tab.id,
|
|
777
|
+
panelEntry.activeTabId,
|
|
778
|
+
keepMounted,
|
|
779
|
+
)) return null;
|
|
780
|
+
return (
|
|
781
|
+
<div key={tab.id} className={visible ? 'h-full' : 'hidden'}>
|
|
782
|
+
{tab.kind === 'terminal' ? (
|
|
783
|
+
<TerminalPanel
|
|
784
|
+
onClose={() => closePanelTab(tab.id)}
|
|
785
|
+
onShowTaskProcess={recentTask ? showRecentTask : undefined}
|
|
786
|
+
taskProcessTitle={recentTask?.title}
|
|
787
|
+
/>
|
|
788
|
+
) : (
|
|
789
|
+
(() => {
|
|
790
|
+
if (!slot) return null;
|
|
791
|
+
const SlotPanel = slot.Component;
|
|
792
|
+
return (
|
|
793
|
+
<SlotPanel
|
|
794
|
+
chatId={chatId ?? ''}
|
|
795
|
+
tabId={tab.id}
|
|
796
|
+
contentId={tab.contentId === tab.kind ? undefined : tab.contentId}
|
|
797
|
+
onTitleChange={(title) => {
|
|
798
|
+
const current = panelEntryRef.current;
|
|
799
|
+
if (!current || !title) return;
|
|
800
|
+
const existing = current.tabs.find((item) => item.id === tab.id);
|
|
801
|
+
if (!existing || existing.title === title) return;
|
|
802
|
+
const tabs = current.tabs.map((item) =>
|
|
803
|
+
item.id === tab.id ? { ...item, title } : item);
|
|
804
|
+
applyPanelEntry({ ...current, tabs });
|
|
805
|
+
}}
|
|
806
|
+
onClose={() => closePanelTab(tab.id)}
|
|
807
|
+
onSubmitToChat={sendChatMessage}
|
|
808
|
+
/>
|
|
809
|
+
);
|
|
810
|
+
})()
|
|
811
|
+
)}
|
|
812
|
+
</div>
|
|
813
|
+
);
|
|
814
|
+
})
|
|
815
|
+
)}
|
|
816
|
+
</div>
|
|
665
817
|
</div>
|
|
666
818
|
</>
|
|
667
819
|
)}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { LuCopy } from 'react-icons/lu';
|
|
2
|
+
import { t } from '@/i18n';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 右侧整栏的开关。栏关着时放在对话标题栏,栏开着时放到右侧标签条最右,点一下收起。
|
|
6
|
+
*/
|
|
7
|
+
export function PanelToggleButton({
|
|
8
|
+
pressed,
|
|
9
|
+
onClick,
|
|
10
|
+
}: {
|
|
11
|
+
pressed: boolean;
|
|
12
|
+
onClick: () => void;
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<button
|
|
16
|
+
type="button"
|
|
17
|
+
onClick={onClick}
|
|
18
|
+
className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-xs transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground ${
|
|
19
|
+
pressed
|
|
20
|
+
? 'bg-agent-foreground/10 text-agent-foreground'
|
|
21
|
+
: 'text-agent-muted-foreground'
|
|
22
|
+
}`}
|
|
23
|
+
title={
|
|
24
|
+
pressed
|
|
25
|
+
? t('Close {name}', { name: t('Panels') })
|
|
26
|
+
: t('Open {name}', { name: t('Panels') })
|
|
27
|
+
}
|
|
28
|
+
aria-label={t('Panels')}
|
|
29
|
+
aria-pressed={pressed}
|
|
30
|
+
data-testid="header-chat-panels"
|
|
31
|
+
>
|
|
32
|
+
<LuCopy className="h-4 w-4" />
|
|
33
|
+
</button>
|
|
34
|
+
);
|
|
35
|
+
}
|