@steerable/agent-shell-web 0.6.64 → 0.6.65

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.
@@ -0,0 +1,22 @@
1
+ /**
2
+ * 让对话里的文件点击打开右侧栏位。
3
+ *
4
+ * 直接通知布局:没有这个标签就打开,已经打开就切过去。整栏收着时一并展开。
5
+ */
6
+
7
+ const listeners = new Set<(slotId: string) => void>();
8
+
9
+ /** 打开并聚焦这个栏位。 */
10
+ export function requestChatSlot(slotId: string): void {
11
+ for (const listener of listeners) listener(slotId);
12
+ }
13
+
14
+ /** 布局挂上后接收请求。返回取消订阅。 */
15
+ export function subscribeChatSlotRequests(
16
+ listener: (slotId: string) => void,
17
+ ): () => void {
18
+ listeners.add(listener);
19
+ return () => {
20
+ listeners.delete(listener);
21
+ };
22
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * 右侧栏位的多标签状态。
3
+ *
4
+ * 同一会话可以同时打开终端和若干包槽位。`active` 是当前看见的那一个。
5
+ * `collapsed` 表示整栏收起:标签还留着,右侧不显示,再打开时恢复。
6
+ * 持久化兼容以前每个会话只存一个字符串的格式。
7
+ */
8
+
9
+ export type RightPanelTabs = {
10
+ tabs: string[];
11
+ active: string;
12
+ collapsed?: boolean;
13
+ };
14
+
15
+ /** 每个会话各自的标签:chatId → 打开的标签(没有标签时不落盘)。 */
16
+ export type RightPanelMap = Record<string, RightPanelTabs>;
17
+
18
+ export function normalizeRightPanelEntry(
19
+ value: unknown,
20
+ isValidValue: (value: string) => boolean,
21
+ ): RightPanelTabs | null {
22
+ if (typeof value === 'string') {
23
+ return isValidValue(value) ? { tabs: [value], active: value } : null;
24
+ }
25
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
26
+ const record = value as { tabs?: unknown; active?: unknown; collapsed?: unknown };
27
+ const seen = new Set<string>();
28
+ const tabs: string[] = [];
29
+ if (Array.isArray(record.tabs)) {
30
+ for (const item of record.tabs) {
31
+ if (typeof item !== 'string' || !isValidValue(item) || seen.has(item)) continue;
32
+ seen.add(item);
33
+ tabs.push(item);
34
+ }
35
+ }
36
+ if (tabs.length === 0) return null;
37
+ const active =
38
+ typeof record.active === 'string' && tabs.includes(record.active)
39
+ ? record.active
40
+ : tabs[0]!;
41
+ return record.collapsed === true ? { tabs, active, collapsed: true } : { tabs, active };
42
+ }
43
+
44
+ /**
45
+ * 解析持久化的「会话 → 右侧标签」映射。
46
+ *
47
+ * 兼容这些历史格式:
48
+ * - 多标签:{"<chatId>": { "tabs": ["ppt", "terminal"], "active": "ppt" }};
49
+ * - 单值映射:{"<chatId>": "terminal" | "<slotId>"} → 一个标签;
50
+ * - 整个值就是单个字符串 → 迁到当前会话;
51
+ * - 更老:只记录终端是否打开过的布尔 key。
52
+ * 无效的会话 / 栏位值会被丢弃。
53
+ */
54
+ export function parseRightPanelMap(options: {
55
+ raw: string | null;
56
+ legacyTerminalOpen: string | null;
57
+ chatId: string | null;
58
+ isValidValue: (value: string) => boolean;
59
+ }): RightPanelMap {
60
+ const { raw, legacyTerminalOpen, chatId, isValidValue } = options;
61
+ if (raw !== null) {
62
+ if (raw === '') return {};
63
+ try {
64
+ const parsed = JSON.parse(raw) as unknown;
65
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
66
+ const map: RightPanelMap = {};
67
+ for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {
68
+ const entry = normalizeRightPanelEntry(value, isValidValue);
69
+ if (entry) map[key] = entry;
70
+ }
71
+ return map;
72
+ }
73
+ } catch {
74
+ const entry = normalizeRightPanelEntry(raw, isValidValue);
75
+ if (entry && chatId) return { [chatId]: entry };
76
+ }
77
+ return {};
78
+ }
79
+ if (legacyTerminalOpen === '1' && chatId && isValidValue('terminal')) {
80
+ return { [chatId]: { tabs: ['terminal'], active: 'terminal' } };
81
+ }
82
+ return {};
83
+ }
84
+
85
+ /** 快捷键:整栏收着时打开并显示这一项;没开就追加并显示;已开但不是当前就切过去;当前这个就关掉。 */
86
+ export function toggleRightPanelEntry(
87
+ current: RightPanelTabs | null,
88
+ kind: string,
89
+ ): RightPanelTabs | null {
90
+ if (current?.collapsed) {
91
+ const tabs = current.tabs.includes(kind) ? current.tabs : [...current.tabs, kind];
92
+ return { tabs, active: kind };
93
+ }
94
+ if (!current || !current.tabs.includes(kind)) {
95
+ return { tabs: current ? [...current.tabs, kind] : [kind], active: kind };
96
+ }
97
+ if (current.active !== kind) return { tabs: current.tabs, active: kind };
98
+ return closeRightPanelTab(current, kind);
99
+ }
100
+
101
+ /** 收起整栏。标签保留,下次展开还是这些。 */
102
+ export function collapseRightPanel(current: RightPanelTabs): RightPanelTabs {
103
+ return { tabs: current.tabs, active: current.active, collapsed: true };
104
+ }
105
+
106
+ /** 展开整栏,回到收起前正在看的标签。 */
107
+ export function expandRightPanel(current: RightPanelTabs): RightPanelTabs {
108
+ return { tabs: current.tabs, active: current.active };
109
+ }
110
+
111
+ /** 关掉一个标签。关掉的是当前标签时,改看相邻的那一个。 */
112
+ export function closeRightPanelTab(
113
+ current: RightPanelTabs,
114
+ kind: string,
115
+ ): RightPanelTabs | null {
116
+ const tabs = current.tabs.filter((id) => id !== kind);
117
+ if (tabs.length === 0) return null;
118
+ if (current.active !== kind) return { tabs, active: current.active };
119
+ const index = current.tabs.indexOf(kind);
120
+ const active = tabs[Math.min(index, tabs.length - 1)] ?? tabs[0]!;
121
+ return { tabs, active };
122
+ }
123
+
124
+ /**
125
+ * 自动展开:栏位空着时打开并显示;已经有别的标签时只把这个加进去,不抢走当前标签。
126
+ * 这个标签已经开着则保持原样。
127
+ */
128
+ export function revealRightPanelTab(
129
+ current: RightPanelTabs | null,
130
+ kind: string,
131
+ ): RightPanelTabs {
132
+ if (!current) return { tabs: [kind], active: kind };
133
+ if (current.collapsed) {
134
+ const tabs = current.tabs.includes(kind) ? current.tabs : [...current.tabs, kind];
135
+ return { tabs, active: kind };
136
+ }
137
+ if (current.tabs.includes(kind)) return current;
138
+ return { tabs: [...current.tabs, kind], active: current.active };
139
+ }
140
+
141
+ /** 打开并显示。已经开着时只切换当前标签。 */
142
+ export function openRightPanelTab(
143
+ current: RightPanelTabs | null,
144
+ kind: string,
145
+ ): RightPanelTabs {
146
+ if (!current) return { tabs: [kind], active: kind };
147
+ if (current.tabs.includes(kind)) return { tabs: current.tabs, active: kind };
148
+ return { tabs: [...current.tabs, kind], active: kind };
149
+ }
@@ -1190,6 +1190,12 @@ function AgentChatView({
1190
1190
  ...pendingFollowUps.map((message) => message.content),
1191
1191
  ...remoteFollowUps,
1192
1192
  ];
1193
+ const chatOutputs = useMemo(() => {
1194
+ const merged: TurnFile[] = [];
1195
+ for (const files of Object.values(turnFilesByMessageId)) merged.push(...files);
1196
+ merged.push(...currentTurnFiles);
1197
+ return merged;
1198
+ }, [turnFilesByMessageId, currentTurnFiles]);
1193
1199
  const handleRemovePendingFollowUp = (index: number) => {
1194
1200
  if (index < pendingFollowUps.length) {
1195
1201
  removeFollowUp(index);
@@ -1228,8 +1234,13 @@ function AgentChatView({
1228
1234
  tasks={tasks}
1229
1235
  chatSlots={ctx.chatSlots}
1230
1236
  rightPanel={ctx.rightPanel}
1231
- onToggleChatSlot={ctx.onToggleChatSlot}
1232
- onToggleRightPanel={ctx.onToggleChatSlot}
1237
+ openPanelIds={ctx.openPanelIds}
1238
+ onOpenRightPanel={ctx.onOpenRightPanel}
1239
+ showProject={showProjectsChrome}
1240
+ project={chatProject}
1241
+ outputs={chatOutputs}
1242
+ onShare={hasHostBridge() ? handleShare : undefined}
1243
+ onChatChanged={ctx.refreshChats}
1233
1244
  />
1234
1245
  }
1235
1246
  onRegenerate={