pilotswarm 0.0.1 → 0.4.0

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.
Files changed (149) hide show
  1. package/README.md +37 -1
  2. package/mcp/README.md +484 -0
  3. package/mcp/dist/bin/pilotswarm-mcp.d.ts +3 -0
  4. package/mcp/dist/bin/pilotswarm-mcp.d.ts.map +1 -0
  5. package/mcp/dist/bin/pilotswarm-mcp.js +367 -0
  6. package/mcp/dist/bin/pilotswarm-mcp.js.map +1 -0
  7. package/mcp/dist/src/auth.d.ts +7 -0
  8. package/mcp/dist/src/auth.d.ts.map +1 -0
  9. package/mcp/dist/src/auth.js +99 -0
  10. package/mcp/dist/src/auth.js.map +1 -0
  11. package/mcp/dist/src/context.d.ts +48 -0
  12. package/mcp/dist/src/context.d.ts.map +1 -0
  13. package/mcp/dist/src/context.js +83 -0
  14. package/mcp/dist/src/context.js.map +1 -0
  15. package/mcp/dist/src/index.d.ts +4 -0
  16. package/mcp/dist/src/index.d.ts.map +1 -0
  17. package/mcp/dist/src/index.js +3 -0
  18. package/mcp/dist/src/index.js.map +1 -0
  19. package/mcp/dist/src/prompts/skills.d.ts +4 -0
  20. package/mcp/dist/src/prompts/skills.d.ts.map +1 -0
  21. package/mcp/dist/src/prompts/skills.js +11 -0
  22. package/mcp/dist/src/prompts/skills.js.map +1 -0
  23. package/mcp/dist/src/resources/agents.d.ts +4 -0
  24. package/mcp/dist/src/resources/agents.d.ts.map +1 -0
  25. package/mcp/dist/src/resources/agents.js +64 -0
  26. package/mcp/dist/src/resources/agents.js.map +1 -0
  27. package/mcp/dist/src/resources/facts.d.ts +4 -0
  28. package/mcp/dist/src/resources/facts.d.ts.map +1 -0
  29. package/mcp/dist/src/resources/facts.js +125 -0
  30. package/mcp/dist/src/resources/facts.js.map +1 -0
  31. package/mcp/dist/src/resources/models.d.ts +4 -0
  32. package/mcp/dist/src/resources/models.d.ts.map +1 -0
  33. package/mcp/dist/src/resources/models.js +43 -0
  34. package/mcp/dist/src/resources/models.js.map +1 -0
  35. package/mcp/dist/src/resources/sessions.d.ts +4 -0
  36. package/mcp/dist/src/resources/sessions.d.ts.map +1 -0
  37. package/mcp/dist/src/resources/sessions.js +190 -0
  38. package/mcp/dist/src/resources/sessions.js.map +1 -0
  39. package/mcp/dist/src/resources/subscriptions.d.ts +9 -0
  40. package/mcp/dist/src/resources/subscriptions.d.ts.map +1 -0
  41. package/mcp/dist/src/resources/subscriptions.js +157 -0
  42. package/mcp/dist/src/resources/subscriptions.js.map +1 -0
  43. package/mcp/dist/src/server.d.ts +4 -0
  44. package/mcp/dist/src/server.d.ts.map +1 -0
  45. package/mcp/dist/src/server.js +59 -0
  46. package/mcp/dist/src/server.js.map +1 -0
  47. package/mcp/dist/src/tools/agents.d.ts +4 -0
  48. package/mcp/dist/src/tools/agents.d.ts.map +1 -0
  49. package/mcp/dist/src/tools/agents.js +317 -0
  50. package/mcp/dist/src/tools/agents.js.map +1 -0
  51. package/mcp/dist/src/tools/facts.d.ts +4 -0
  52. package/mcp/dist/src/tools/facts.d.ts.map +1 -0
  53. package/mcp/dist/src/tools/facts.js +151 -0
  54. package/mcp/dist/src/tools/facts.js.map +1 -0
  55. package/mcp/dist/src/tools/models.d.ts +4 -0
  56. package/mcp/dist/src/tools/models.d.ts.map +1 -0
  57. package/mcp/dist/src/tools/models.js +256 -0
  58. package/mcp/dist/src/tools/models.js.map +1 -0
  59. package/mcp/dist/src/tools/sessions.d.ts +4 -0
  60. package/mcp/dist/src/tools/sessions.d.ts.map +1 -0
  61. package/mcp/dist/src/tools/sessions.js +606 -0
  62. package/mcp/dist/src/tools/sessions.js.map +1 -0
  63. package/mcp/dist/src/util/command.d.ts +52 -0
  64. package/mcp/dist/src/util/command.d.ts.map +1 -0
  65. package/mcp/dist/src/util/command.js +78 -0
  66. package/mcp/dist/src/util/command.js.map +1 -0
  67. package/package.json +81 -6
  68. package/tui/README.md +35 -0
  69. package/tui/bin/tui.js +30 -0
  70. package/tui/plugins/.mcp.json +7 -0
  71. package/tui/plugins/plugin.json +13 -0
  72. package/tui/plugins/session-policy.json +8 -0
  73. package/tui/src/app.js +850 -0
  74. package/tui/src/auth/cli.js +111 -0
  75. package/tui/src/auth/entra-auth.js +218 -0
  76. package/tui/src/bootstrap-env.js +176 -0
  77. package/tui/src/embedded-workers.js +79 -0
  78. package/tui/src/http-transport-host.js +106 -0
  79. package/tui/src/index.js +340 -0
  80. package/tui/src/node-sdk-transport.js +1793 -0
  81. package/tui/src/platform.js +984 -0
  82. package/tui/src/plugin-config.js +239 -0
  83. package/tui/src/portal.js +7 -0
  84. package/tui/src/version.js +7 -0
  85. package/tui/tui-splash.txt +11 -0
  86. package/ui/core/README.md +6 -0
  87. package/ui/core/src/commands.js +93 -0
  88. package/ui/core/src/context-usage.js +212 -0
  89. package/ui/core/src/controller.js +6104 -0
  90. package/ui/core/src/formatting.js +1036 -0
  91. package/ui/core/src/history.js +932 -0
  92. package/ui/core/src/index.js +13 -0
  93. package/ui/core/src/layout.js +332 -0
  94. package/ui/core/src/reducer.js +1935 -0
  95. package/ui/core/src/selectors.js +5409 -0
  96. package/ui/core/src/session-errors.js +14 -0
  97. package/ui/core/src/session-tree.js +151 -0
  98. package/ui/core/src/state.js +248 -0
  99. package/ui/core/src/store.js +23 -0
  100. package/ui/core/src/system-titles.js +24 -0
  101. package/ui/core/src/themes/catppuccin-mocha.js +56 -0
  102. package/ui/core/src/themes/cobalt2.js +56 -0
  103. package/ui/core/src/themes/dark-high-contrast.js +56 -0
  104. package/ui/core/src/themes/daylight.js +62 -0
  105. package/ui/core/src/themes/dracula.js +56 -0
  106. package/ui/core/src/themes/github-dark.js +56 -0
  107. package/ui/core/src/themes/github-light.js +59 -0
  108. package/ui/core/src/themes/gruvbox-dark.js +56 -0
  109. package/ui/core/src/themes/hacker-x-matrix.js +56 -0
  110. package/ui/core/src/themes/hacker-x-orion-prime.js +56 -0
  111. package/ui/core/src/themes/helpers.js +79 -0
  112. package/ui/core/src/themes/high-contrast-mono.js +59 -0
  113. package/ui/core/src/themes/index.js +52 -0
  114. package/ui/core/src/themes/light-high-contrast.js +62 -0
  115. package/ui/core/src/themes/noctis-obscuro.js +56 -0
  116. package/ui/core/src/themes/noctis.js +56 -0
  117. package/ui/core/src/themes/paper-ink.js +62 -0
  118. package/ui/core/src/themes/solarized-ops.js +59 -0
  119. package/ui/core/src/themes/terminal-green.js +59 -0
  120. package/ui/core/src/themes/tokyo-night.js +56 -0
  121. package/ui/react/README.md +5 -0
  122. package/ui/react/src/chat-status.js +39 -0
  123. package/ui/react/src/components.js +1989 -0
  124. package/ui/react/src/index.js +4 -0
  125. package/ui/react/src/platform.js +15 -0
  126. package/ui/react/src/use-controller-state.js +38 -0
  127. package/ui/react/src/web-app.js +4390 -0
  128. package/web/README.md +198 -0
  129. package/web/api/router.js +196 -0
  130. package/web/api/ws.js +152 -0
  131. package/web/auth/authz/engine.js +204 -0
  132. package/web/auth/config.js +115 -0
  133. package/web/auth/index.js +175 -0
  134. package/web/auth/normalize/entra.js +22 -0
  135. package/web/auth/providers/entra.js +76 -0
  136. package/web/auth/providers/none.js +24 -0
  137. package/web/auth.js +10 -0
  138. package/web/bin/serve.js +53 -0
  139. package/web/config.js +20 -0
  140. package/web/dist/app.js +469 -0
  141. package/web/dist/assets/index-CBgQQk-j.css +1 -0
  142. package/web/dist/assets/index-DMefB7Wb.js +24 -0
  143. package/web/dist/assets/msal-CytV9RFv.js +7 -0
  144. package/web/dist/assets/pilotswarm-DyBlqVTY.js +87 -0
  145. package/web/dist/assets/react-BZwsW8pe.js +1 -0
  146. package/web/dist/index.html +16 -0
  147. package/web/runtime.js +454 -0
  148. package/web/server.js +276 -0
  149. package/index.js +0 -1
@@ -0,0 +1,4390 @@
1
+ import React from "react";
2
+ import { appendAnimatedDotsToRuns, useAnimatedDots } from "./chat-status.js";
3
+ import {
4
+ UI_COMMANDS,
5
+ INSPECTOR_TABS,
6
+ appReducer,
7
+ canStopSessionTurn,
8
+ computeLegacyLayout,
9
+ createInitialState,
10
+ createStore,
11
+ getPromptInputRows,
12
+ getTheme,
13
+ parseTerminalMarkupRuns,
14
+ PilotSwarmUiController,
15
+ tokenizeInlineMarkdown,
16
+ selectActivityPane,
17
+ selectAdminConsole,
18
+ selectArtifactPickerModal,
19
+ selectArtifactUploadModal,
20
+ selectChatLines,
21
+ selectChatPaneChrome,
22
+ selectOutboxOverlayLines,
23
+ selectFileBrowserItems,
24
+ selectFilesFilterModal,
25
+ selectFilesScope,
26
+ selectFilesView,
27
+ selectHistoryFormatModal,
28
+ selectInspector,
29
+ selectLogFilterModal,
30
+ selectModelPickerModal,
31
+ selectReasoningEffortPickerModal,
32
+ selectRenameSessionModal,
33
+ selectSessionAgentPickerModal,
34
+ selectSessionGroupNameModal,
35
+ selectSessionGroupPickerModal,
36
+ selectSessionOwnerFilterModal,
37
+ selectSessionRows,
38
+ selectStatusBar,
39
+ selectThemePickerModal,
40
+ selectConfirmModal,
41
+ normalizeStoredLayoutAdjustments,
42
+ normalizeStoredPinnedSessionIds,
43
+ } from "pilotswarm/ui-core";
44
+ import { useControllerSelector } from "./use-controller-state.js";
45
+
46
+ const MOBILE_BREAKPOINT = 920;
47
+ const GRID_CELL_WIDTH = 7;
48
+ const GRID_CELL_HEIGHT = 19;
49
+ const PORTAL_SESSION_COLUMN_RATIO = 0.24;
50
+ const PORTAL_SESSION_CHAT_DIVIDER_PX = 16;
51
+ const PORTAL_WORKSPACE_GAP_PX = 8;
52
+ const PORTAL_MIN_CHAT_COLUMN_PX = 224;
53
+ const PORTAL_SESSION_COMPACT_COLUMN_PX = 190;
54
+ const PORTAL_SESSION_HIDDEN_COLUMN_PX = 48;
55
+ const SCROLL_ROW_HEIGHT = 16;
56
+ const SCROLL_BOTTOM_EPSILON_PX = 0.5;
57
+ const PROGRAMMATIC_SCROLL_TOLERANCE_PX = SCROLL_BOTTOM_EPSILON_PX;
58
+ const PROFILE_SETTINGS_POLL_MS = 5000;
59
+ const REASONING_EFFORT_LABELS = new Set(["low", "medium", "high", "xhigh"]);
60
+ const LEGACY_BROWSER_PREFERENCE_STORAGE_KEYS = [
61
+ "pilotswarm.theme",
62
+ "pilotswarm.sessionOwnerFilter",
63
+ "pilotswarm.chatFocus",
64
+ "pilotswarm.layoutAdjustments",
65
+ "pilotswarm.pinnedSessions",
66
+ ];
67
+ const LEGACY_BROWSER_PREFERENCE_COOKIE_NAMES = [
68
+ "pilotswarm_theme",
69
+ "pilotswarm_session_owner_filter",
70
+ ];
71
+ const INSPECTOR_TAB_LABELS = {
72
+ sequence: "Sequence",
73
+ logs: "Logs",
74
+ nodes: "Node Map",
75
+ history: "History",
76
+ files: "Files",
77
+ stats: "Stats",
78
+ };
79
+
80
+ function cycleTabs(tabs, current, delta) {
81
+ const values = Array.isArray(tabs) ? tabs.filter(Boolean) : [];
82
+ if (values.length === 0) return current;
83
+ const index = values.indexOf(current);
84
+ const safeIndex = index === -1 ? 0 : index;
85
+ const nextIndex = (safeIndex + delta + values.length) % values.length;
86
+ return values[nextIndex];
87
+ }
88
+
89
+ function supportsBrowserFileUploads(controller) {
90
+ return typeof controller?.transport?.uploadArtifactFromFile === "function";
91
+ }
92
+
93
+ function supportsPathArtifactUploads(controller) {
94
+ return typeof controller?.transport?.uploadArtifactFromPath === "function";
95
+ }
96
+
97
+ function supportsArtifactBrowser(controller) {
98
+ return typeof controller?.transport?.listArtifacts === "function";
99
+ }
100
+
101
+ function supportsArtifactDelete(controller) {
102
+ return typeof controller?.transport?.deleteArtifact === "function";
103
+ }
104
+
105
+ function supportsLocalFileOpen(controller) {
106
+ return typeof controller?.transport?.openPathInDefaultApp === "function";
107
+ }
108
+
109
+ function clearBrowserPreferenceCache() {
110
+ if (typeof window === "undefined") return;
111
+ try {
112
+ for (const key of LEGACY_BROWSER_PREFERENCE_STORAGE_KEYS) {
113
+ window.localStorage.removeItem(key);
114
+ }
115
+ } catch {}
116
+ try {
117
+ for (const name of LEGACY_BROWSER_PREFERENCE_COOKIE_NAMES) {
118
+ document.cookie = `${name}=; Path=/; Max-Age=0; SameSite=Lax`;
119
+ }
120
+ } catch {}
121
+ }
122
+
123
+ function normalizeProfileSettings(settings) {
124
+ const candidate = settings && typeof settings === "object" && !Array.isArray(settings) ? settings : {};
125
+ const normalized = {};
126
+ if (typeof candidate.themeId === "string" && candidate.themeId.trim()) {
127
+ normalized.themeId = candidate.themeId.trim();
128
+ }
129
+ if (hasOwn(candidate, "sessionOwnerFilter") && candidate.sessionOwnerFilter && typeof candidate.sessionOwnerFilter === "object") {
130
+ normalized.sessionOwnerFilter = candidate.sessionOwnerFilter;
131
+ }
132
+ if (hasOwn(candidate, "layoutAdjustments")) {
133
+ normalized.layoutAdjustments = normalizeStoredLayoutAdjustments(candidate.layoutAdjustments);
134
+ }
135
+ if (hasOwn(candidate, "pinnedSessionIds")) {
136
+ normalized.pinnedSessionIds = normalizeStoredPinnedSessionIds(candidate.pinnedSessionIds);
137
+ }
138
+ if (hasOwn(candidate, "collapsedSessionIds")) {
139
+ normalized.collapsedSessionIds = normalizeStoredCollapsedSessionIdsToArray(candidate.collapsedSessionIds);
140
+ }
141
+ if (hasOwn(candidate, "activeSessionId")) {
142
+ const id = candidate.activeSessionId == null ? null : String(candidate.activeSessionId).trim();
143
+ normalized.activeSessionId = id || null;
144
+ }
145
+ if (candidate.chatViewMode === "summary" || candidate.chatViewMode === "transcript") {
146
+ normalized.chatViewMode = candidate.chatViewMode;
147
+ }
148
+ return normalized;
149
+ }
150
+
151
+ function normalizeStoredCollapsedSessionIdsToArray(value) {
152
+ if (value instanceof Set) {
153
+ const out = [];
154
+ for (const entry of value) {
155
+ const id = String(entry || "").trim();
156
+ if (id) out.push(id);
157
+ }
158
+ out.sort();
159
+ return out;
160
+ }
161
+ if (!Array.isArray(value)) return [];
162
+ const seen = new Set();
163
+ const out = [];
164
+ for (const entry of value) {
165
+ const id = String(entry || "").trim();
166
+ if (!id || seen.has(id)) continue;
167
+ seen.add(id);
168
+ out.push(id);
169
+ }
170
+ out.sort();
171
+ return out;
172
+ }
173
+
174
+ function hasOwn(value, key) {
175
+ return Boolean(value && typeof value === "object" && Object.prototype.hasOwnProperty.call(value, key));
176
+ }
177
+
178
+ function profileSettingsFromViewState(state) {
179
+ return normalizeProfileSettings({
180
+ themeId: state.themeId,
181
+ sessionOwnerFilter: state.ownerFilter,
182
+ layoutAdjustments: {
183
+ paneAdjust: state.paneAdjust,
184
+ sessionPaneAdjust: state.sessionPaneAdjust,
185
+ portalSessionColumnAdjust: state.portalSessionColumnAdjust,
186
+ activityPaneAdjust: state.activityPaneAdjust,
187
+ },
188
+ pinnedSessionIds: state.pinnedIds,
189
+ collapsedSessionIds: state.collapsedSessionIds,
190
+ activeSessionId: state.activeSessionId,
191
+ chatViewMode: state.chatViewMode,
192
+ });
193
+ }
194
+
195
+ function buildDefaultProfileSettingsFromState(state) {
196
+ return normalizeProfileSettings({
197
+ themeId: state?.ui?.themeId,
198
+ sessionOwnerFilter: state?.sessions?.ownerFilter,
199
+ layoutAdjustments: state?.ui?.layout,
200
+ pinnedSessionIds: state?.sessions?.pinnedIds,
201
+ collapsedSessionIds: state?.sessions?.collapsedIds,
202
+ activeSessionId: state?.sessions?.activeSessionId,
203
+ chatViewMode: state?.ui?.chatViewMode,
204
+ });
205
+ }
206
+
207
+ function materializeProfileSettings(remoteSettings, defaults) {
208
+ const normalizedRemote = normalizeProfileSettings(remoteSettings);
209
+ const normalizedDefaults = normalizeProfileSettings(defaults);
210
+ // For chatViewMode specifically, do NOT fall back to defaults when the
211
+ // remote profile lacks the field. The poll runs every few seconds; if
212
+ // we synthesized a default here, every poll where the server hasn't
213
+ // yet persisted the user's toggle would clobber the local choice and
214
+ // snap the chat pane back from Summary to Chat. Leaving chatViewMode
215
+ // off the materialized settings causes the `profileSettings/apply`
216
+ // reducer's `hasChatViewMode` guard to preserve the current value.
217
+ return normalizeProfileSettings({
218
+ themeId: hasOwn(normalizedRemote, "themeId")
219
+ ? normalizedRemote.themeId
220
+ : normalizedDefaults.themeId,
221
+ sessionOwnerFilter: hasOwn(normalizedRemote, "sessionOwnerFilter")
222
+ ? normalizedRemote.sessionOwnerFilter
223
+ : normalizedDefaults.sessionOwnerFilter,
224
+ layoutAdjustments: hasOwn(normalizedRemote, "layoutAdjustments")
225
+ ? normalizedRemote.layoutAdjustments
226
+ : normalizedDefaults.layoutAdjustments,
227
+ pinnedSessionIds: hasOwn(normalizedRemote, "pinnedSessionIds")
228
+ ? normalizedRemote.pinnedSessionIds
229
+ : normalizedDefaults.pinnedSessionIds,
230
+ ...(hasOwn(normalizedRemote, "collapsedSessionIds")
231
+ ? { collapsedSessionIds: normalizedRemote.collapsedSessionIds }
232
+ : {}),
233
+ ...(hasOwn(normalizedRemote, "activeSessionId")
234
+ ? { activeSessionId: normalizedRemote.activeSessionId }
235
+ : {}),
236
+ ...(hasOwn(normalizedRemote, "chatViewMode")
237
+ ? { chatViewMode: normalizedRemote.chatViewMode }
238
+ : {}),
239
+ });
240
+ }
241
+
242
+ async function saveProfileSettings(controller, settings) {
243
+ if (typeof controller?.transport?.setCurrentUserProfileSettings !== "function") return null;
244
+ return controller.transport.setCurrentUserProfileSettings({
245
+ settings: normalizeProfileSettings(settings),
246
+ });
247
+ }
248
+
249
+ function getVisibleInspectorTabs(controller) {
250
+ return supportsArtifactBrowser(controller)
251
+ ? INSPECTOR_TABS
252
+ : INSPECTOR_TABS.filter((tab) => tab !== "files");
253
+ }
254
+
255
+ function shallowEqualObject(left, right) {
256
+ if (Object.is(left, right)) return true;
257
+ if (!left || !right || typeof left !== "object" || typeof right !== "object") return false;
258
+ const leftKeys = Object.keys(left);
259
+ const rightKeys = Object.keys(right);
260
+ if (leftKeys.length !== rightKeys.length) return false;
261
+ for (const key of leftKeys) {
262
+ if (!Object.prototype.hasOwnProperty.call(right, key)) return false;
263
+ if (!Object.is(left[key], right[key])) return false;
264
+ }
265
+ return true;
266
+ }
267
+
268
+ function clampNumber(value, min, max) {
269
+ return Math.max(min, Math.min(max, value));
270
+ }
271
+
272
+ function portalSessionColumnBounds(width) {
273
+ const safeWidth = Math.max(0, Number(width) || 0);
274
+ const availableWidth = Math.max(
275
+ 0,
276
+ safeWidth - PORTAL_SESSION_CHAT_DIVIDER_PX - (PORTAL_WORKSPACE_GAP_PX * 2),
277
+ );
278
+ const baseWidth = availableWidth * PORTAL_SESSION_COLUMN_RATIO;
279
+ const maxSessionWidth = Math.max(0, availableWidth - PORTAL_MIN_CHAT_COLUMN_PX);
280
+ return {
281
+ minAdjust: -baseWidth,
282
+ maxAdjust: maxSessionWidth - baseWidth,
283
+ };
284
+ }
285
+
286
+ function portalSessionColumnWidth(width, adjust) {
287
+ const bounds = portalSessionColumnBounds(width);
288
+ const safeWidth = Math.max(0, Number(width) || 0);
289
+ const availableWidth = Math.max(
290
+ 0,
291
+ safeWidth - PORTAL_SESSION_CHAT_DIVIDER_PX - (PORTAL_WORKSPACE_GAP_PX * 2),
292
+ );
293
+ const baseWidth = availableWidth * PORTAL_SESSION_COLUMN_RATIO;
294
+ return clampNumber(baseWidth + (Number(adjust) || 0), 0, baseWidth + bounds.maxAdjust);
295
+ }
296
+
297
+ function portalSessionColumnMode(width) {
298
+ if (width <= PORTAL_SESSION_HIDDEN_COLUMN_PX) return "hidden";
299
+ if (width <= PORTAL_SESSION_COMPACT_COLUMN_PX) return "compact";
300
+ return "wrap";
301
+ }
302
+
303
+ function getStatePromptRows(state) {
304
+ const promptRows = Number(state?.ui?.promptRows);
305
+ return Number.isFinite(promptRows) && promptRows > 0
306
+ ? promptRows
307
+ : getPromptInputRows(state?.ui?.prompt || "");
308
+ }
309
+
310
+ function computeStateLayout(state) {
311
+ return computeLegacyLayout({
312
+ width: state.ui.layout?.viewportWidth ?? 120,
313
+ height: state.ui.layout?.viewportHeight ?? 40,
314
+ }, state.ui.layout?.paneAdjust ?? 0, getStatePromptRows(state), state.ui.layout?.sessionPaneAdjust ?? 0);
315
+ }
316
+
317
+ function getPortalInspectorContentWidth(paneWidth, inspectorTab) {
318
+ // Sequence uses preserved-width lines inside padded, scroll-synced containers.
319
+ // Reserve a few extra columns so content that fits visually does not trip a cosmetic x-scrollbar.
320
+ const overflowGuardColumns = inspectorTab === "sequence" ? 3 : 0;
321
+ return Math.max(20, paneWidth - 4 - overflowGuardColumns);
322
+ }
323
+
324
+ function normalizeLines(lines) {
325
+ const normalized = [];
326
+
327
+ const decodeEscapedNewlines = (value) => {
328
+ const text = String(value || "");
329
+ if (!text.includes("\\n") || text.includes("\n")) return text;
330
+ return text.replace(/\\n/g, "\n");
331
+ };
332
+
333
+ for (const line of lines || []) {
334
+ if (line?.kind === "markup") {
335
+ // Markup lines carry preformatted art (splash screens). Tag them
336
+ // so the chat renderer keeps each line intact: the pane's
337
+ // pre-wrap/overflow-wrap rules would rewrap wide art lines at
338
+ // arbitrary points and scramble the image.
339
+ for (const parsedLine of parseTerminalMarkupRuns(line.value || "")) {
340
+ normalized.push({ kind: "runs", runs: parsedLine, preserve: true });
341
+ }
342
+ continue;
343
+ }
344
+ // Sentinel kinds preserved as-is so parseStructuredChatBlocks can
345
+ // recognize and render them (e.g. markdownTable → HTML <table>).
346
+ if (line?.kind === "markdownTable") {
347
+ normalized.push(line);
348
+ continue;
349
+ }
350
+ if (line?.kind === "runs") {
351
+ const runs = Array.isArray(line.runs) ? line.runs : [];
352
+ if (runs.length === 1) {
353
+ const onlyRun = runs[0] || {};
354
+ const decodedText = decodeEscapedNewlines(onlyRun.text || "");
355
+ if (decodedText.includes("\n")) {
356
+ const fragments = decodedText.split("\n");
357
+ for (const fragment of fragments) {
358
+ normalized.push({
359
+ kind: "runs",
360
+ runs: [{ ...onlyRun, text: fragment }],
361
+ });
362
+ }
363
+ continue;
364
+ }
365
+ normalized.push({ kind: "runs", runs: [{ ...onlyRun, text: decodedText }] });
366
+ continue;
367
+ }
368
+ normalized.push({
369
+ kind: "runs",
370
+ runs: runs.map((run) => ({
371
+ ...run,
372
+ text: decodeEscapedNewlines(run?.text || ""),
373
+ })),
374
+ });
375
+ continue;
376
+ }
377
+ if (Array.isArray(line)) {
378
+ normalized.push({ kind: "runs", runs: line });
379
+ continue;
380
+ }
381
+ if (line && typeof line === "object" && typeof line.text === "string") {
382
+ const decodedText = decodeEscapedNewlines(line.text);
383
+ if (decodedText.includes("\n")) {
384
+ const fragments = decodedText.split("\n");
385
+ for (const fragment of fragments) {
386
+ normalized.push({ kind: "text", ...line, text: fragment });
387
+ }
388
+ continue;
389
+ }
390
+ normalized.push({ kind: "text", ...line, text: decodedText });
391
+ continue;
392
+ }
393
+ normalized.push({ kind: "text", ...line });
394
+ }
395
+ return normalized;
396
+ }
397
+
398
+ function resolveColor(theme, token) {
399
+ if (!token) return undefined;
400
+ return theme?.tui?.[token] || theme?.terminal?.[token] || theme?.page?.[token] || token;
401
+ }
402
+
403
+ function runsToText(runs = []) {
404
+ return runs.map((run) => String(run?.text || "")).join("");
405
+ }
406
+
407
+ function flattenTitleText(title) {
408
+ if (Array.isArray(title)) return runsToText(title);
409
+ return String(title || "");
410
+ }
411
+
412
+ function compactTitleRuns(title, maxWidth = 40) {
413
+ if (!Array.isArray(title)) {
414
+ const text = String(title || "");
415
+ return [{ text: text.length > maxWidth ? `${text.slice(0, maxWidth - 1)}…` : text, color: "white", bold: true }];
416
+ }
417
+ const compactRuns = [];
418
+ let remaining = Math.max(8, maxWidth);
419
+ for (const run of title) {
420
+ if (remaining <= 0) break;
421
+ const color = run?.color;
422
+ if (color === "gray" && compactRuns.length > 0) continue;
423
+ const text = String(run?.text || "");
424
+ if (!text) continue;
425
+ const chunk = text.length > remaining && remaining > 1
426
+ ? `${text.slice(0, remaining - 1)}…`
427
+ : text.slice(0, remaining);
428
+ if (!chunk) continue;
429
+ compactRuns.push({ ...run, text: chunk });
430
+ remaining -= chunk.length;
431
+ }
432
+ return compactRuns.length > 0 ? compactRuns : title;
433
+ }
434
+
435
+ function applyDocumentTheme(themeId) {
436
+ const theme = getTheme(themeId);
437
+ if (!theme || typeof document === "undefined") return;
438
+ const root = document.documentElement;
439
+ root.style.setProperty("--ps-page-background", theme.page.background);
440
+ root.style.setProperty("--ps-page-foreground", theme.page.foreground);
441
+ root.style.setProperty("--ps-surface", theme.tui.surface);
442
+ root.style.setProperty("--ps-background", theme.tui.background);
443
+ root.style.setProperty("--ps-foreground", theme.tui.foreground);
444
+ root.style.setProperty("--ps-muted", theme.tui.gray);
445
+ root.style.setProperty("--ps-border", theme.tui.border || theme.tui.gray);
446
+ root.style.setProperty("--ps-selection-background", theme.tui.selectionBackground);
447
+ root.style.setProperty("--ps-selection-foreground", theme.tui.selectionForeground);
448
+ root.style.setProperty("--ps-highlight-background", theme.tui.activeHighlightBackground);
449
+ root.style.setProperty("--ps-highlight-foreground", theme.tui.activeHighlightForeground);
450
+ root.style.setProperty("--ps-modal-backdrop", theme.page.modalBackdrop);
451
+ root.style.setProperty("--ps-modal-background", theme.page.modalBackground);
452
+ root.style.setProperty("--ps-modal-border", theme.page.modalBorder);
453
+ root.style.setProperty("--ps-modal-foreground", theme.page.modalForeground);
454
+ root.style.setProperty("--ps-modal-muted", theme.page.modalMuted);
455
+ root.style.setProperty("--ps-modal-selected-background", theme.page.modalSelectedBackground);
456
+ root.style.setProperty("--ps-modal-selected-border", theme.page.modalSelectedBorder);
457
+ root.style.setProperty("--ps-modal-selected-foreground", theme.page.modalSelectedForeground);
458
+ }
459
+
460
+ function useMeasuredViewport(ref) {
461
+ const [viewport, setViewport] = React.useState({ width: 0, height: 0 });
462
+
463
+ React.useLayoutEffect(() => {
464
+ const element = ref.current;
465
+ if (!element) return undefined;
466
+
467
+ const update = () => {
468
+ setViewport({
469
+ width: element.clientWidth,
470
+ height: element.clientHeight,
471
+ });
472
+ };
473
+
474
+ update();
475
+ const observer = new ResizeObserver(update);
476
+ observer.observe(element);
477
+ window.addEventListener("resize", update);
478
+ return () => {
479
+ observer.disconnect();
480
+ window.removeEventListener("resize", update);
481
+ };
482
+ }, [ref]);
483
+
484
+ return viewport;
485
+ }
486
+
487
+ function computeGridViewport(viewport) {
488
+ const width = Math.max(320, viewport.width || window.innerWidth || 1280);
489
+ const height = Math.max(320, viewport.height || window.innerHeight || 800);
490
+ return {
491
+ width: Math.max(40, Math.floor(width / GRID_CELL_WIDTH)),
492
+ height: Math.max(18, Math.floor(height / GRID_CELL_HEIGHT)),
493
+ };
494
+ }
495
+
496
+ export function getScrollDistanceToBottom(node) {
497
+ if (!node) return 0;
498
+ const maxScroll = Math.max(0, node.scrollHeight - node.clientHeight);
499
+ return Math.max(0, maxScroll - node.scrollTop);
500
+ }
501
+
502
+ export function isScrollViewportAtBottom(node) {
503
+ return getScrollDistanceToBottom(node) <= SCROLL_BOTTOM_EPSILON_PX;
504
+ }
505
+
506
+ function useScrollSync(ref, lines, scrollOffset, scrollMode, paneKey, controller, { stickyBottom = false } = {}) {
507
+ const normalizedLines = React.useMemo(() => normalizeLines(lines), [lines]);
508
+ // Programmatic scrollTop assignments fire a 'scroll' event that would
509
+ // otherwise call onScroll → dispatch ui/scroll → clobber the user's
510
+ // offset (especially when content briefly shrinks during a refresh
511
+ // and clamps nextScrollTop downward). The flag tells onScroll to
512
+ // ignore the matching scroll event after we set scrollTop ourselves.
513
+ const programmaticScrollRef = React.useRef(null);
514
+ const previousViewportStateRef = React.useRef({
515
+ scrollMode,
516
+ scrollOffset,
517
+ });
518
+
519
+ React.useLayoutEffect(() => {
520
+ const node = ref.current;
521
+ if (!node) return;
522
+ const previousViewportState = previousViewportStateRef.current;
523
+ const maxScroll = Math.max(0, node.scrollHeight - node.clientHeight);
524
+ const preservePausedStickyScroll = stickyBottom
525
+ && scrollMode === "top"
526
+ && previousViewportState?.scrollMode === "top"
527
+ && previousViewportState?.scrollOffset === scrollOffset;
528
+ const offsetPixels = Math.max(0, Number(scrollOffset) || 0) * SCROLL_ROW_HEIGHT;
529
+ const nextScrollTop = preservePausedStickyScroll
530
+ ? Math.max(0, Math.min(node.scrollTop, maxScroll))
531
+ : scrollMode === "bottom"
532
+ ? Math.max(0, maxScroll - offsetPixels)
533
+ : Math.min(maxScroll, offsetPixels);
534
+ if (Math.abs(node.scrollTop - nextScrollTop) > PROGRAMMATIC_SCROLL_TOLERANCE_PX) {
535
+ const pendingProgrammaticScroll = { target: nextScrollTop };
536
+ programmaticScrollRef.current = pendingProgrammaticScroll;
537
+ node.scrollTop = nextScrollTop;
538
+ // Clear on the next frame — the scroll event from the assignment
539
+ // above is queued synchronously and handled in the same task.
540
+ window.requestAnimationFrame(() => {
541
+ if (programmaticScrollRef.current === pendingProgrammaticScroll) {
542
+ programmaticScrollRef.current = null;
543
+ }
544
+ });
545
+ }
546
+ previousViewportStateRef.current = {
547
+ scrollMode,
548
+ scrollOffset,
549
+ };
550
+ }, [normalizedLines, ref, scrollMode, scrollOffset, stickyBottom]);
551
+
552
+ const onScroll = React.useCallback(() => {
553
+ const node = ref.current;
554
+ if (!node || !paneKey) return;
555
+ const pendingProgrammatic = programmaticScrollRef.current;
556
+ if (pendingProgrammatic) {
557
+ if (Math.abs(node.scrollTop - pendingProgrammatic.target) <= PROGRAMMATIC_SCROLL_TOLERANCE_PX) {
558
+ return;
559
+ }
560
+ programmaticScrollRef.current = null;
561
+ }
562
+ const maxScroll = Math.max(0, node.scrollHeight - node.clientHeight);
563
+ // When the pane has no scrollable content (transient loading state
564
+ // that briefly collapses the body to one line), the browser auto-
565
+ // clamps scrollTop to 0 and fires a scroll event we did NOT trigger.
566
+ // Writing that into state would clobber the user's saved offset.
567
+ // Skip the dispatch — next render with real content will restore
568
+ // the desired scroll position from preserved state.
569
+ if (maxScroll <= 0) return;
570
+ if (stickyBottom && typeof controller.updatePaneScrollFromViewport === "function") {
571
+ controller.updatePaneScrollFromViewport(
572
+ paneKey,
573
+ Math.max(0, node.scrollTop) / SCROLL_ROW_HEIGHT,
574
+ { atBottom: isScrollViewportAtBottom(node) },
575
+ );
576
+ return;
577
+ }
578
+
579
+ const pixels = scrollMode === "bottom"
580
+ ? Math.max(0, maxScroll - node.scrollTop)
581
+ : Math.max(0, node.scrollTop);
582
+ controller.dispatch({
583
+ type: "ui/scroll",
584
+ pane: paneKey,
585
+ offset: pixels / SCROLL_ROW_HEIGHT,
586
+ });
587
+ if (paneKey === "chat" && scrollMode === "bottom" && node.scrollTop <= PROGRAMMATIC_SCROLL_TOLERANCE_PX) {
588
+ controller.armChatTopHistoryLoad?.();
589
+ }
590
+ }, [controller, paneKey, ref, scrollMode, stickyBottom]);
591
+
592
+ const onWheel = React.useCallback((event) => {
593
+ const node = ref.current;
594
+ if (!node || paneKey !== "chat" || scrollMode !== "bottom" || event.deltaY >= 0) return;
595
+ if (node.scrollTop > PROGRAMMATIC_SCROLL_TOLERANCE_PX) return;
596
+ const maxScroll = Math.max(0, node.scrollHeight - node.clientHeight);
597
+ controller.handleChatTopHistoryScrollIntent?.(maxScroll / SCROLL_ROW_HEIGHT);
598
+ }, [controller, paneKey, ref, scrollMode]);
599
+
600
+ return { normalizedLines, onScroll, onWheel };
601
+ }
602
+
603
+ function Runs({ runs, theme }) {
604
+ return React.createElement(React.Fragment, null,
605
+ (runs || []).map((run, index) => {
606
+ const style = {
607
+ color: resolveColor(theme, run.color),
608
+ backgroundColor: resolveColor(theme, run.backgroundColor),
609
+ fontWeight: run.bold ? 700 : 400,
610
+ textDecoration: run.underline ? "underline" : "none",
611
+ };
612
+ const href = String(run?.href || "").trim();
613
+ const isExternalHref = /^https?:\/\//i.test(href);
614
+
615
+ return isExternalHref
616
+ ? React.createElement("a", {
617
+ key: `${index}:${run.text || ""}`,
618
+ className: "ps-md-link",
619
+ href,
620
+ target: "_blank",
621
+ rel: "noreferrer",
622
+ style,
623
+ }, run.text || "")
624
+ : React.createElement("span", {
625
+ key: `${index}:${run.text || ""}`,
626
+ style,
627
+ }, run.text || "");
628
+ }),
629
+ );
630
+ }
631
+
632
+ function SystemNoticeLine({ line, theme }) {
633
+ const body = String(line?.body || "").trim();
634
+ return React.createElement("details", { className: "ps-system-notice" },
635
+ React.createElement("summary", {
636
+ className: "ps-system-notice-summary",
637
+ style: { color: resolveColor(theme, line?.color) || "var(--ps-muted)" },
638
+ },
639
+ React.createElement("span", { className: "ps-system-notice-summary-text" }, line?.text || "System notice")),
640
+ body
641
+ ? React.createElement("div", { className: "ps-system-notice-body" },
642
+ React.createElement(MarkdownPreviewContent, { content: body, theme }))
643
+ : null);
644
+ }
645
+
646
+ function Line({ line, theme, className = "" }) {
647
+ const lineClassName = className ? `ps-line ${className}` : "ps-line";
648
+ if (!line) {
649
+ return React.createElement("div", { className: lineClassName }, " ");
650
+ }
651
+ if (line.kind === "systemNotice") {
652
+ return React.createElement(SystemNoticeLine, { line, theme });
653
+ }
654
+ if (line.kind === "runs") {
655
+ return React.createElement("div", { className: lineClassName },
656
+ React.createElement(Runs, { runs: line.runs, theme }));
657
+ }
658
+ return React.createElement("div", {
659
+ className: lineClassName,
660
+ style: {
661
+ color: resolveColor(theme, line.color),
662
+ backgroundColor: resolveColor(theme, line.backgroundColor),
663
+ fontWeight: line.bold ? 700 : 400,
664
+ textDecoration: line.underline ? "underline" : "none",
665
+ },
666
+ }, line.text || " ");
667
+ }
668
+
669
+ function lineText(line) {
670
+ if (!line) return "";
671
+ if (line.kind === "runs") return runsToText(line.runs);
672
+ return String(line.text || "");
673
+ }
674
+
675
+ function usePanePixelScroll(ref, scrollOffset, paneKey, controller) {
676
+ // See useScrollSync for rationale on the programmatic-scroll guard.
677
+ const programmaticScrollRef = React.useRef(false);
678
+
679
+ React.useLayoutEffect(() => {
680
+ const node = ref.current;
681
+ if (!node) return;
682
+ const maxScroll = Math.max(0, node.scrollHeight - node.clientHeight);
683
+ const nextScrollTop = Math.min(maxScroll, Math.max(0, Number(scrollOffset) || 0) * SCROLL_ROW_HEIGHT);
684
+ if (Math.abs(node.scrollTop - nextScrollTop) > 2) {
685
+ programmaticScrollRef.current = true;
686
+ node.scrollTop = nextScrollTop;
687
+ window.requestAnimationFrame(() => {
688
+ programmaticScrollRef.current = false;
689
+ });
690
+ }
691
+ }, [ref, scrollOffset]);
692
+
693
+ return React.useCallback(() => {
694
+ if (programmaticScrollRef.current) return;
695
+ const node = ref.current;
696
+ if (!node || !paneKey) return;
697
+ // See useScrollSync — ignore browser auto-clamp during a
698
+ // transient empty/loading body.
699
+ if (node.scrollHeight <= node.clientHeight) return;
700
+ controller.dispatch({
701
+ type: "ui/scroll",
702
+ pane: paneKey,
703
+ offset: Math.max(0, node.scrollTop) / SCROLL_ROW_HEIGHT,
704
+ });
705
+ }, [controller, paneKey, ref]);
706
+ }
707
+
708
+ function renderInlineMarkdown(source, theme, keyPrefix = "md") {
709
+ return tokenizeInlineMarkdown(source).map((token, index) => {
710
+ const key = `${keyPrefix}:${index}`;
711
+ if (token.type === "code") {
712
+ return React.createElement("code", { key, className: "ps-md-inline-code" }, token.text);
713
+ }
714
+ if (token.type === "strong") {
715
+ return React.createElement("strong", { key, className: "ps-md-strong" }, renderInlineMarkdown(token.text, theme, `${key}:strong`));
716
+ }
717
+ if (token.type === "em") {
718
+ return React.createElement("em", { key, className: "ps-md-em" }, renderInlineMarkdown(token.text, theme, `${key}:em`));
719
+ }
720
+ if (token.type === "link") {
721
+ return React.createElement("a", {
722
+ key,
723
+ className: "ps-md-link",
724
+ href: token.href,
725
+ target: "_blank",
726
+ rel: "noreferrer",
727
+ style: { color: resolveColor(theme, "cyan") },
728
+ }, token.text);
729
+ }
730
+ return React.createElement(React.Fragment, { key }, token.text);
731
+ });
732
+ }
733
+
734
+ function normalizeTableCellText(value = "") {
735
+ return String(value || "")
736
+ .replace(/\s+/g, " ")
737
+ .trim();
738
+ }
739
+
740
+ const FIT_WIDTH_FLEX_HEADER_KEYWORDS = [
741
+ "description",
742
+ "mechanism",
743
+ "details",
744
+ "notes",
745
+ "summary",
746
+ "message",
747
+ "comment",
748
+ "reason",
749
+ "body",
750
+ "content",
751
+ "explanation",
752
+ "rationale",
753
+ "one-liner",
754
+ ];
755
+
756
+ const FIT_WIDTH_FLEX_MIN_MAX_LEN = 24;
757
+ const FIT_WIDTH_RIGID_CHAR_CAP = 32;
758
+ const FIT_WIDTH_MIN_RIGID_CHARS = 6;
759
+ const FIT_WIDTH_FLEX_MIN_CHARS = 30;
760
+ const FIT_WIDTH_FLEX_MAX_CHARS = 56;
761
+ const FIT_WIDTH_FLEX_MIN_FRACTION = 0.4;
762
+ const FIT_WIDTH_FLEX_TO_RIGID_RATIO = 1.4;
763
+ const FIT_WIDTH_MIN_EXTRA_CHARS = 2;
764
+
765
+ /**
766
+ * Compute per-column layout for a fit-width markdown / chat table.
767
+ *
768
+ * Strategy:
769
+ * 1. Measure max cell length and "wrappability" (cells with whitespace) per column.
770
+ * 2. Identify a single "flex" column — long, prose-like, ideally with a
771
+ * header keyword like Description / Mechanism / Notes. This column
772
+ * absorbs overflow by wrapping aggressively.
773
+ * 3. Give every other column a budget = clamp(maxLen + padding, MIN, RIGID_CAP),
774
+ * so rigid columns stay at their content-fit width even when one
775
+ * sibling column has hundreds of characters of prose.
776
+ * 4. Give the flex column a bounded share of the rigid-column budget. A
777
+ * very long cell should wrap; it should not steal so much percentage
778
+ * width that compact columns like Count / Status collapse on phones.
779
+ * 5. Return a minimum table width in ch so the wrapper can scroll
780
+ * horizontally rather than forcing all columns below readable size.
781
+ *
782
+ * Returns { widths: ["12.34%", ...], flexIndex: number, minWidth: "64ch" }
783
+ * when a flex column is identified — the table renderer then forces
784
+ * table-layout: fixed, adds an `is-flex-column` class to the chosen column,
785
+ * and gives the table a readable minimum width. Returns null when no flex
786
+ * column is found, in which case the renderer falls back to the browser's
787
+ * auto-table-layout (which is already good for short / uniform tables).
788
+ *
789
+ * Background: the previous behavior used the browser's auto-table-layout
790
+ * unconditionally. That works well when columns are uniform but is biased
791
+ * toward wide columns when one column has prose hundreds of characters
792
+ * long (e.g. a Mechanism / Description column) — auto-layout distributes
793
+ * width proportional to (max-content − min-content), which lets the prose
794
+ * column squeeze the rigid columns down to a few characters each.
795
+ */
796
+ function computeFitWidthColumnLayout(rows = []) {
797
+ const columnCount = Math.max(0, ...rows.map((row) => row.length));
798
+ if (columnCount <= 0) return null;
799
+
800
+ const headerRow = rows[0] || [];
801
+ const dataRowCount = Math.max(1, rows.length - 1);
802
+
803
+ const stats = Array.from({ length: columnCount }, () => ({
804
+ max: 0,
805
+ spaceCells: 0,
806
+ }));
807
+ for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
808
+ const row = rows[rowIndex];
809
+ for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) {
810
+ const text = normalizeTableCellText(row[columnIndex] || "");
811
+ if (text.length > stats[columnIndex].max) stats[columnIndex].max = text.length;
812
+ if (rowIndex > 0 && /\s/.test(text)) stats[columnIndex].spaceCells += 1;
813
+ }
814
+ }
815
+
816
+ let flexIndex = -1;
817
+ let flexScore = 0;
818
+ for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) {
819
+ const stat = stats[columnIndex];
820
+ if (stat.max < FIT_WIDTH_FLEX_MIN_MAX_LEN) continue;
821
+ const spaceRatio = stat.spaceCells / dataRowCount;
822
+ if (spaceRatio < 0.4) continue;
823
+ const headerText = normalizeTableCellText(headerRow[columnIndex] || "").toLowerCase();
824
+ const headerBonus = FIT_WIDTH_FLEX_HEADER_KEYWORDS.some((keyword) => headerText.includes(keyword)) ? 60 : 0;
825
+ const score = stat.max + headerBonus;
826
+ if (score > flexScore) {
827
+ flexScore = score;
828
+ flexIndex = columnIndex;
829
+ }
830
+ }
831
+
832
+ if (flexIndex < 0) return null;
833
+
834
+ const rigidBudgets = stats.map((stat, columnIndex) => {
835
+ if (columnIndex === flexIndex) return 0;
836
+ return Math.max(FIT_WIDTH_MIN_RIGID_CHARS, Math.min(stat.max + 2, FIT_WIDTH_RIGID_CHAR_CAP));
837
+ });
838
+ const sumRigid = rigidBudgets.reduce((sum, value) => sum + value, 0);
839
+ const flexStat = stats[flexIndex];
840
+ const flexBudget = Math.max(
841
+ FIT_WIDTH_FLEX_MIN_CHARS,
842
+ Math.min(
843
+ flexStat.max * 0.45,
844
+ FIT_WIDTH_FLEX_MAX_CHARS,
845
+ Math.max(FIT_WIDTH_FLEX_MIN_CHARS, sumRigid * FIT_WIDTH_FLEX_TO_RIGID_RATIO),
846
+ ),
847
+ );
848
+ const budgets = rigidBudgets.map((value, columnIndex) => (
849
+ columnIndex === flexIndex ? flexBudget : value
850
+ ));
851
+
852
+ const totalBudget = budgets.reduce((sum, value) => sum + value, 0);
853
+ if (totalBudget > 0 && budgets[flexIndex] / totalBudget < FIT_WIDTH_FLEX_MIN_FRACTION) {
854
+ const sumRigid = totalBudget - budgets[flexIndex];
855
+ budgets[flexIndex] = sumRigid * (FIT_WIDTH_FLEX_MIN_FRACTION / (1 - FIT_WIDTH_FLEX_MIN_FRACTION));
856
+ }
857
+
858
+ const finalTotal = budgets.reduce((sum, value) => sum + value, 0);
859
+ if (!(finalTotal > 0)) return null;
860
+ return {
861
+ widths: budgets.map((value) => `${((value / finalTotal) * 100).toFixed(2)}%`),
862
+ flexIndex,
863
+ minWidth: `${Math.ceil(finalTotal + FIT_WIDTH_MIN_EXTRA_CHARS)}ch`,
864
+ };
865
+ }
866
+
867
+ function buildTableColumnLabels(headerRows = [], columnCount = 0) {
868
+ return Array.from({ length: columnCount }, (_, columnIndex) => {
869
+ const label = (Array.isArray(headerRows) ? headerRows : [])
870
+ .map((row) => normalizeTableCellText(row?.[columnIndex] || ""))
871
+ .filter(Boolean)
872
+ .join(" / ");
873
+ return label || null;
874
+ });
875
+ }
876
+
877
+ function isMarkdownSpecialLine(line = "", nextLine = "") {
878
+ const value = String(line || "");
879
+ return /^\s*#{1,6}\s+/.test(value)
880
+ || /^\s*>/.test(value)
881
+ || /^\s*([-*]|\d+\.)\s+/.test(value)
882
+ || /^\s*```/.test(value)
883
+ || (value.includes("|") && /^\s*\|?[\s:-]+(?:\|[\s:-]+)+\|?\s*$/.test(String(nextLine || "")));
884
+ }
885
+
886
+ function splitMarkdownTableRow(line = "") {
887
+ const trimmed = String(line || "").trim().replace(/^\|/, "").replace(/\|$/, "");
888
+ return trimmed.split("|").map((cell) => cell.trim());
889
+ }
890
+
891
+ function parseMarkdownBlocks(source = "") {
892
+ const text = String(source || "").replace(/\r\n?/g, "\n");
893
+ const lines = text.split("\n");
894
+ const blocks = [];
895
+
896
+ for (let index = 0; index < lines.length;) {
897
+ const line = lines[index];
898
+ const trimmed = line.trim();
899
+
900
+ if (!trimmed) {
901
+ index += 1;
902
+ continue;
903
+ }
904
+
905
+ const fenceMatch = /^```(\S*)\s*$/.exec(trimmed);
906
+ if (fenceMatch) {
907
+ const language = fenceMatch[1] || "";
908
+ const codeLines = [];
909
+ index += 1;
910
+ while (index < lines.length && !/^```/.test(lines[index].trim())) {
911
+ codeLines.push(lines[index]);
912
+ index += 1;
913
+ }
914
+ if (index < lines.length) index += 1;
915
+ blocks.push({ type: "code", language, content: codeLines.join("\n") });
916
+ continue;
917
+ }
918
+
919
+ const headingMatch = /^(#{1,6})\s+(.*)$/.exec(line);
920
+ if (headingMatch) {
921
+ blocks.push({ type: "heading", level: headingMatch[1].length, text: headingMatch[2].trim() });
922
+ index += 1;
923
+ continue;
924
+ }
925
+
926
+ if (line.includes("|") && index + 1 < lines.length && /^\s*\|?[\s:-]+(?:\|[\s:-]+)+\|?\s*$/.test(lines[index + 1].trim())) {
927
+ const header = splitMarkdownTableRow(line);
928
+ index += 2;
929
+ const rows = [];
930
+ while (index < lines.length && lines[index].includes("|") && lines[index].trim()) {
931
+ rows.push(splitMarkdownTableRow(lines[index]));
932
+ index += 1;
933
+ }
934
+ blocks.push({ type: "table", header, rows });
935
+ continue;
936
+ }
937
+
938
+ if (/^\s*>/.test(line)) {
939
+ const quoteLines = [];
940
+ while (index < lines.length && /^\s*>/.test(lines[index])) {
941
+ quoteLines.push(lines[index].replace(/^\s*>\s?/, ""));
942
+ index += 1;
943
+ }
944
+ blocks.push({ type: "blockquote", text: quoteLines.join("\n").trim() });
945
+ continue;
946
+ }
947
+
948
+ const listMatch = /^(\s*)([-*]|\d+\.)\s+(.*)$/.exec(line);
949
+ if (listMatch) {
950
+ const ordered = /\d+\./.test(listMatch[2]);
951
+ const items = [];
952
+ while (index < lines.length) {
953
+ const current = lines[index];
954
+ const itemMatch = /^(\s*)([-*]|\d+\.)\s+(.*)$/.exec(current);
955
+ if (!itemMatch || /\d+\./.test(itemMatch[2]) !== ordered) break;
956
+ const itemLines = [itemMatch[3].trim()];
957
+ index += 1;
958
+ while (
959
+ index < lines.length
960
+ && lines[index].trim()
961
+ && !/^(\s*)([-*]|\d+\.)\s+/.test(lines[index])
962
+ && !isMarkdownSpecialLine(lines[index], lines[index + 1] || "")
963
+ ) {
964
+ itemLines.push(lines[index].trim());
965
+ index += 1;
966
+ }
967
+ items.push(itemLines.join(" "));
968
+ if (!lines[index]?.trim()) break;
969
+ }
970
+ blocks.push({ type: "list", ordered, items });
971
+ continue;
972
+ }
973
+
974
+ const paragraphLines = [line.trim()];
975
+ index += 1;
976
+ while (
977
+ index < lines.length
978
+ && lines[index].trim()
979
+ && !isMarkdownSpecialLine(lines[index], lines[index + 1] || "")
980
+ ) {
981
+ paragraphLines.push(lines[index].trim());
982
+ index += 1;
983
+ }
984
+ blocks.push({ type: "paragraph", text: paragraphLines.join(" ") });
985
+ }
986
+
987
+ return blocks;
988
+ }
989
+
990
+ function MarkdownPreviewContent({ content, theme }) {
991
+ const blocks = React.useMemo(() => parseMarkdownBlocks(content), [content]);
992
+ if (blocks.length === 0) {
993
+ return React.createElement("div", { className: "ps-empty-state" }, "No preview content.");
994
+ }
995
+ return React.createElement("div", { className: "ps-markdown-preview" },
996
+ blocks.map((block, index) => {
997
+ if (block.type === "heading") {
998
+ return React.createElement("div", {
999
+ key: `block:${index}`,
1000
+ className: `ps-md-heading is-h${block.level}`,
1001
+ }, renderInlineMarkdown(block.text, theme, `heading:${index}`));
1002
+ }
1003
+ if (block.type === "code") {
1004
+ return React.createElement("section", { key: `block:${index}`, className: "ps-md-code-block" },
1005
+ React.createElement("div", { className: "ps-md-code-header" }, block.language || "text"),
1006
+ React.createElement("pre", { className: "ps-md-code-pre" },
1007
+ React.createElement("code", null, block.content)));
1008
+ }
1009
+ if (block.type === "blockquote") {
1010
+ return React.createElement("blockquote", { key: `block:${index}`, className: "ps-md-quote" },
1011
+ renderInlineMarkdown(block.text, theme, `quote:${index}`));
1012
+ }
1013
+ if (block.type === "list") {
1014
+ const ListTag = block.ordered ? "ol" : "ul";
1015
+ return React.createElement(ListTag, {
1016
+ key: `block:${index}`,
1017
+ className: `ps-md-list${block.ordered ? " is-ordered" : ""}`,
1018
+ }, block.items.map((item, itemIndex) => React.createElement("li", {
1019
+ key: `item:${itemIndex}`,
1020
+ className: "ps-md-list-item",
1021
+ }, renderInlineMarkdown(item, theme, `list:${index}:${itemIndex}`))));
1022
+ }
1023
+ if (block.type === "table") {
1024
+ const columnCount = Math.max(block.header.length || 0, ...block.rows.map((row) => row.length));
1025
+ // Wrap cells by default. Only fall back to horizontal scroll
1026
+ // when there are so many columns that wrapping would crush
1027
+ // every cell (>6 columns is the practical readability cliff
1028
+ // on phones).
1029
+ const fitToWidth = columnCount > 0 && columnCount <= 6;
1030
+ // Default: let the browser's auto-table-layout do column
1031
+ // sizing under the fit-content constraint — that handles
1032
+ // short / uniform tables well. When one column has long
1033
+ // prose (e.g. a Mechanism / Description column), auto-layout
1034
+ // squeezes the rigid columns; computeFitWidthColumnLayout
1035
+ // detects that case, picks a flex column, and assigns
1036
+ // explicit widths so the rigid columns hold their
1037
+ // content-fit width and the flex column wraps to absorb the
1038
+ // remainder.
1039
+ const layout = fitToWidth
1040
+ ? computeFitWidthColumnLayout([block.header, ...block.rows])
1041
+ : null;
1042
+ const hasFlexColumn = !!layout;
1043
+ const flexIndex = layout ? layout.flexIndex : -1;
1044
+ const cellClass = (cellIndex) => (cellIndex === flexIndex ? "is-flex-column" : null);
1045
+ const columnLabels = buildTableColumnLabels([block.header], columnCount);
1046
+ const tableClass = `ps-md-table${fitToWidth ? " is-fit-width" : ""}${hasFlexColumn ? " has-flex-column" : ""}`;
1047
+ const wrapClass = `ps-md-table-wrap${fitToWidth ? " is-fit-width" : ""}${hasFlexColumn ? " has-flex-column" : ""}`;
1048
+ return React.createElement("div", {
1049
+ key: `block:${index}`,
1050
+ className: wrapClass,
1051
+ },
1052
+ React.createElement("table", {
1053
+ className: tableClass,
1054
+ style: layout?.minWidth ? { "--ps-table-min-width": layout.minWidth } : undefined,
1055
+ },
1056
+ layout
1057
+ ? React.createElement("colgroup", null,
1058
+ layout.widths.map((width, columnIndex) => React.createElement("col", {
1059
+ key: `col:${columnIndex}`,
1060
+ className: cellClass(columnIndex) || undefined,
1061
+ style: { width },
1062
+ })))
1063
+ : null,
1064
+ React.createElement("thead", null,
1065
+ React.createElement("tr", null,
1066
+ block.header.map((cell, cellIndex) => React.createElement("th", {
1067
+ key: `head:${cellIndex}`,
1068
+ className: cellClass(cellIndex) || undefined,
1069
+ },
1070
+ renderInlineMarkdown(cell, theme, `table:${index}:head:${cellIndex}`))))),
1071
+ React.createElement("tbody", null,
1072
+ block.rows.map((row, rowIndex) => React.createElement("tr", { key: `row:${rowIndex}` },
1073
+ row.map((cell, cellIndex) => React.createElement("td", {
1074
+ key: `cell:${rowIndex}:${cellIndex}`,
1075
+ className: cellClass(cellIndex) || undefined,
1076
+ "data-label": columnLabels[cellIndex] || undefined,
1077
+ },
1078
+ React.createElement("span", { className: "ps-table-cell-value" },
1079
+ renderInlineMarkdown(cell, theme, `table:${index}:${rowIndex}:${cellIndex}`)))))))));
1080
+ }
1081
+ return React.createElement("p", { key: `block:${index}`, className: "ps-md-paragraph" },
1082
+ renderInlineMarkdown(block.text, theme, `para:${index}`));
1083
+ }));
1084
+ }
1085
+
1086
+ function MarkdownPreviewPanel({ controller, title, color, focused, scrollOffset = 0, paneKey, theme, content }) {
1087
+ const ref = React.useRef(null);
1088
+ const onScroll = usePanePixelScroll(ref, scrollOffset, paneKey, controller);
1089
+
1090
+ return React.createElement(Panel, { title, color, focused, theme },
1091
+ React.createElement("div", {
1092
+ ref,
1093
+ className: "ps-scroll-panel ps-markdown-scroll",
1094
+ onScroll,
1095
+ }, React.createElement(MarkdownPreviewContent, { content, theme })));
1096
+ }
1097
+
1098
+ function formatArtifactPreviewBytes(value) {
1099
+ const bytes = Number(value);
1100
+ if (!Number.isFinite(bytes) || bytes < 0) return "Unknown size";
1101
+ if (bytes < 1024) return `${Math.round(bytes)} B`;
1102
+ if (bytes < 1024 * 1024) {
1103
+ const kb = bytes / 1024;
1104
+ return `${kb >= 10 ? Math.round(kb) : kb.toFixed(1)} KB`;
1105
+ }
1106
+ const mb = bytes / (1024 * 1024);
1107
+ return `${mb >= 10 ? Math.round(mb) : mb.toFixed(1)} MB`;
1108
+ }
1109
+
1110
+ function BinaryArtifactPreviewPanel({ title, color, focused, theme, filename, contentType, sizeBytes, source, uploadedAt }) {
1111
+ const meta = [];
1112
+ if (contentType) meta.push(contentType);
1113
+ if (sizeBytes != null) meta.push(formatArtifactPreviewBytes(sizeBytes));
1114
+ if (source) meta.push(source);
1115
+ const uploadedLabel = uploadedAt
1116
+ ? new Date(uploadedAt).toLocaleString(undefined, {
1117
+ year: "numeric",
1118
+ month: "short",
1119
+ day: "numeric",
1120
+ hour: "numeric",
1121
+ minute: "2-digit",
1122
+ })
1123
+ : "";
1124
+
1125
+ return React.createElement(Panel, { title, color, focused, theme },
1126
+ React.createElement("div", { className: "ps-binary-preview-card" },
1127
+ React.createElement("div", { className: "ps-binary-preview-kicker" }, "Binary artifact"),
1128
+ React.createElement("h3", { className: "ps-binary-preview-title" }, filename || "Artifact"),
1129
+ meta.length > 0
1130
+ ? React.createElement("div", { className: "ps-binary-preview-meta" }, meta.join(" • "))
1131
+ : null,
1132
+ uploadedLabel
1133
+ ? React.createElement("div", { className: "ps-binary-preview-time" }, `Uploaded ${uploadedLabel}`)
1134
+ : null,
1135
+ React.createElement("p", { className: "ps-binary-preview-copy" },
1136
+ "Preview is intentionally disabled for non-text artifacts in the browser workspace. Use Download to save the file and open it in the default app.")));
1137
+ }
1138
+
1139
+ function isBoxTopLine(text) {
1140
+ const value = String(text || "").trim();
1141
+ return value.startsWith("┌") && value.endsWith("┐");
1142
+ }
1143
+
1144
+ function isBoxBottomLine(text) {
1145
+ const value = String(text || "").trim();
1146
+ return value.startsWith("└") && value.endsWith("┘");
1147
+ }
1148
+
1149
+ function isBoxDividerLine(text) {
1150
+ const value = String(text || "").trim();
1151
+ return value.startsWith("├") && value.endsWith("┤");
1152
+ }
1153
+
1154
+ function isBoxContentLine(text) {
1155
+ const value = String(text || "").trim();
1156
+ return value.startsWith("│") && value.endsWith("│");
1157
+ }
1158
+
1159
+ function extractCodeFenceLanguage(line) {
1160
+ const value = String(lineText(line) || "").trim();
1161
+ if (!isBoxTopLine(value)) return "";
1162
+ return value
1163
+ .slice(1, -1)
1164
+ .replace(/^─+/u, "")
1165
+ .replace(/─+$/u, "")
1166
+ .trim();
1167
+ }
1168
+
1169
+ function extractCodeFenceLine(line) {
1170
+ if (line?.kind === "runs" && Array.isArray(line.runs) && line.runs.length >= 3) {
1171
+ return String(line.runs[1]?.text || "").replace(/\s+$/u, "");
1172
+ }
1173
+ const value = lineText(line);
1174
+ if (!isBoxContentLine(value)) return String(value || "");
1175
+ return String(value)
1176
+ .slice(1, -1)
1177
+ .replace(/\s+$/u, "");
1178
+ }
1179
+
1180
+ function trimRunsEdgeWhitespace(runs = []) {
1181
+ const nextRuns = (runs || []).map((run) => ({ ...run }));
1182
+ if (nextRuns.length === 0) return nextRuns;
1183
+ nextRuns[0].text = String(nextRuns[0].text || "").replace(/^\s+/, "");
1184
+ nextRuns[nextRuns.length - 1].text = String(nextRuns[nextRuns.length - 1].text || "").replace(/\s+$/, "");
1185
+ return nextRuns.filter((run, index) => String(run.text || "").length > 0 || nextRuns.length === 1 || index === 0);
1186
+ }
1187
+
1188
+ function extractFramedRuns(line, { fallbackColor = null } = {}) {
1189
+ if (line?.kind === "runs" && Array.isArray(line.runs) && line.runs.length >= 3) {
1190
+ return trimRunsEdgeWhitespace(line.runs.slice(1, -1));
1191
+ }
1192
+ const text = lineText(line)
1193
+ .replace(/^\s*[┌│]\s?/, "")
1194
+ .replace(/\s?[┐│]\s*$/, "")
1195
+ .replace(/^─+/, "")
1196
+ .replace(/─+$/, "")
1197
+ .trim();
1198
+ return [{ text, color: fallbackColor }];
1199
+ }
1200
+
1201
+ function splitBoxTableCells(text) {
1202
+ const value = String(text || "").trim();
1203
+ if (!isBoxContentLine(value)) return [];
1204
+ return value
1205
+ .slice(1, -1)
1206
+ .split("│")
1207
+ .map((cell) => cell.trim());
1208
+ }
1209
+
1210
+ function shouldJoinBoxTableFragmentsWithoutSpace(left = "", right = "") {
1211
+ const previous = String(left || "").trimEnd();
1212
+ const next = String(right || "").trimStart();
1213
+ if (!previous || !next) return false;
1214
+
1215
+ const previousChar = previous.slice(-1);
1216
+ const nextChar = next[0];
1217
+
1218
+ if (/^[,.;:!?%)\]}>]/u.test(nextChar)) return true;
1219
+ if (/^[._/\\-]/u.test(nextChar)) return true;
1220
+ if (/[([{<._/\\-]$/u.test(previousChar)) return true;
1221
+ if (/\.[A-Za-z0-9]{0,4}$/u.test(previous) && /^[A-Za-z0-9]/u.test(nextChar)) return true;
1222
+
1223
+ return false;
1224
+ }
1225
+
1226
+ export function mergeBoxTableCellFragments(fragments = []) {
1227
+ const parts = (Array.isArray(fragments) ? fragments : [fragments])
1228
+ .map((fragment) => String(fragment || "").trim())
1229
+ .filter(Boolean);
1230
+ if (parts.length === 0) return "";
1231
+
1232
+ return parts.slice(1).reduce((merged, fragment) => (
1233
+ shouldJoinBoxTableFragmentsWithoutSpace(merged, fragment)
1234
+ ? `${merged}${fragment}`
1235
+ : `${merged} ${fragment}`
1236
+ ), parts[0]);
1237
+ }
1238
+
1239
+ function mergeBoxTableRowGroup(rowGroup = []) {
1240
+ const columnCount = Math.max(0, ...rowGroup.map((row) => row.length));
1241
+ return Array.from({ length: columnCount }, (_, columnIndex) => mergeBoxTableCellFragments(
1242
+ rowGroup
1243
+ .map((row) => String(row[columnIndex] || "").trim())
1244
+ .filter(Boolean),
1245
+ ));
1246
+ }
1247
+
1248
+ function parseStructuredChatBlocks(lines = []) {
1249
+ const blocks = [];
1250
+
1251
+ for (let index = 0; index < lines.length;) {
1252
+ const currentLine = lines[index];
1253
+
1254
+ // Preformatted markup lines (splash art) bypass box/table/card
1255
+ // detection entirely and render as one preserve block, so no line
1256
+ // ever rewraps or gets reinterpreted as a structured element.
1257
+ if (currentLine?.preserve) {
1258
+ const preserveLines = [];
1259
+ while (index < lines.length && lines[index]?.preserve) {
1260
+ preserveLines.push(lines[index]);
1261
+ index += 1;
1262
+ }
1263
+ blocks.push({ type: "preserve", lines: preserveLines });
1264
+ continue;
1265
+ }
1266
+
1267
+ // Sentinel markdown-table line emitted by parseMarkdownLines when
1268
+ // tableMode === "sentinel". Carries the raw header + rows so the
1269
+ // portal renders a real HTML table with markdown cell content (so
1270
+ // [label](url) inside cells stays clickable, instead of being flattened
1271
+ // into plain text by the box-art width-fitter).
1272
+ if (currentLine?.kind === "markdownTable") {
1273
+ blocks.push({
1274
+ type: "table",
1275
+ headerRows: Array.isArray(currentLine.header) && currentLine.header.length > 0
1276
+ ? [currentLine.header]
1277
+ : [],
1278
+ bodyRows: Array.isArray(currentLine.rows) ? currentLine.rows : [],
1279
+ });
1280
+ index += 1;
1281
+ continue;
1282
+ }
1283
+
1284
+ const currentText = lineText(currentLine);
1285
+
1286
+ if (isBoxTopLine(currentText) && currentText.includes("┬")) {
1287
+ const headerRows = [];
1288
+ const bodyRows = [];
1289
+ let currentRowGroup = [];
1290
+ let inHeader = true;
1291
+ index += 1;
1292
+
1293
+ while (index < lines.length) {
1294
+ const nextLine = lines[index];
1295
+ const nextText = lineText(nextLine);
1296
+ if (isBoxBottomLine(nextText)) {
1297
+ if (currentRowGroup.length > 0) {
1298
+ const mergedRow = mergeBoxTableRowGroup(currentRowGroup);
1299
+ if (mergedRow.length > 0) {
1300
+ if (inHeader) headerRows.push(mergedRow);
1301
+ else bodyRows.push(mergedRow);
1302
+ }
1303
+ }
1304
+ index += 1;
1305
+ break;
1306
+ }
1307
+ if (isBoxDividerLine(nextText)) {
1308
+ if (currentRowGroup.length > 0) {
1309
+ const mergedRow = mergeBoxTableRowGroup(currentRowGroup);
1310
+ if (mergedRow.length > 0) {
1311
+ if (inHeader) headerRows.push(mergedRow);
1312
+ else bodyRows.push(mergedRow);
1313
+ }
1314
+ }
1315
+ currentRowGroup = [];
1316
+ inHeader = false;
1317
+ index += 1;
1318
+ continue;
1319
+ }
1320
+ if (isBoxContentLine(nextText)) {
1321
+ const cells = splitBoxTableCells(nextText);
1322
+ if (cells.length > 0) {
1323
+ currentRowGroup.push(cells);
1324
+ }
1325
+ }
1326
+ index += 1;
1327
+ }
1328
+
1329
+ blocks.push({ type: "table", headerRows, bodyRows });
1330
+ continue;
1331
+ }
1332
+
1333
+ if (
1334
+ currentLine?.kind === "runs"
1335
+ && Array.isArray(currentLine.runs)
1336
+ && currentLine.runs.length === 1
1337
+ && isBoxTopLine(currentText)
1338
+ ) {
1339
+ const language = extractCodeFenceLanguage(currentLine);
1340
+ const codeLines = [];
1341
+ index += 1;
1342
+
1343
+ while (index < lines.length) {
1344
+ const nextLine = lines[index];
1345
+ const nextText = lineText(nextLine);
1346
+ if (isBoxBottomLine(nextText)) {
1347
+ index += 1;
1348
+ break;
1349
+ }
1350
+ if (isBoxContentLine(nextText)) {
1351
+ codeLines.push(extractCodeFenceLine(nextLine));
1352
+ } else {
1353
+ codeLines.push(lineText(nextLine));
1354
+ }
1355
+ index += 1;
1356
+ }
1357
+
1358
+ if (index < lines.length && lineText(lines[index]).trim().length === 0) {
1359
+ index += 1;
1360
+ }
1361
+
1362
+ blocks.push({
1363
+ type: "code",
1364
+ language: language || "text",
1365
+ content: codeLines.join("\n"),
1366
+ });
1367
+ continue;
1368
+ }
1369
+
1370
+ if (
1371
+ currentLine?.kind === "runs"
1372
+ && Array.isArray(currentLine.runs)
1373
+ && currentLine.runs.length > 2
1374
+ && isBoxTopLine(currentText)
1375
+ ) {
1376
+ const headerRuns = extractFramedRuns(currentLine);
1377
+ const borderColor = currentLine.runs[0]?.color || "gray";
1378
+ const bodyLines = [];
1379
+ index += 1;
1380
+
1381
+ while (index < lines.length) {
1382
+ const nextLine = lines[index];
1383
+ const nextText = lineText(nextLine);
1384
+ if (isBoxBottomLine(nextText)) {
1385
+ index += 1;
1386
+ break;
1387
+ }
1388
+ if (isBoxContentLine(nextText)) {
1389
+ bodyLines.push(extractFramedRuns(nextLine));
1390
+ } else {
1391
+ bodyLines.push(nextLine?.kind === "runs"
1392
+ ? nextLine.runs
1393
+ : [{ text: lineText(nextLine), color: nextLine?.color || null }]);
1394
+ }
1395
+ index += 1;
1396
+ }
1397
+
1398
+ if (index < lines.length && lineText(lines[index]).trim().length === 0) {
1399
+ index += 1;
1400
+ }
1401
+
1402
+ blocks.push({ type: "card", headerRuns, bodyLines, borderColor });
1403
+ continue;
1404
+ }
1405
+
1406
+ blocks.push({ type: "line", line: currentLine });
1407
+ index += 1;
1408
+ }
1409
+
1410
+ return blocks;
1411
+ }
1412
+
1413
+ function StructuredChatBlocks({ lines, theme }) {
1414
+ const blocks = React.useMemo(() => parseStructuredChatBlocks(lines), [lines]);
1415
+
1416
+ return React.createElement(React.Fragment, null,
1417
+ blocks.map((block, index) => {
1418
+ if (block.type === "preserve") {
1419
+ return React.createElement("div", { key: `preserve:${index}`, className: "ps-chat-preserve-block" },
1420
+ block.lines.map((line, lineIndex) => React.createElement(Line, {
1421
+ key: `preserve:${index}:${lineIndex}`,
1422
+ line,
1423
+ theme,
1424
+ className: "ps-line-preserve",
1425
+ })));
1426
+ }
1427
+
1428
+ if (block.type === "code") {
1429
+ return React.createElement("section", { key: `code:${index}`, className: "ps-md-code-block ps-chat-code-block" },
1430
+ React.createElement("div", { className: "ps-md-code-header" }, block.language || "text"),
1431
+ React.createElement("pre", { className: "ps-md-code-pre" },
1432
+ React.createElement("code", null, block.content || "")));
1433
+ }
1434
+
1435
+ if (block.type === "card") {
1436
+ return React.createElement("section", {
1437
+ key: `card:${index}`,
1438
+ className: "ps-chat-card",
1439
+ style: { "--ps-chat-card-accent": resolveColor(theme, block.borderColor) || "var(--ps-border)" },
1440
+ },
1441
+ React.createElement("header", { className: "ps-chat-card-header" },
1442
+ React.createElement(Runs, { runs: block.headerRuns, theme })),
1443
+ React.createElement("div", { className: "ps-chat-card-body" },
1444
+ (block.bodyLines || []).map((bodyRuns, bodyIndex) => React.createElement("div", {
1445
+ key: `card:${index}:line:${bodyIndex}`,
1446
+ className: "ps-chat-card-line",
1447
+ }, React.createElement(Runs, { runs: bodyRuns, theme })) )));
1448
+ }
1449
+
1450
+ if (block.type === "table") {
1451
+ const headerRows = block.headerRows || [];
1452
+ const bodyRows = block.bodyRows || [];
1453
+ const columnCount = Math.max(
1454
+ 1,
1455
+ ...headerRows.map((row) => row.length),
1456
+ ...bodyRows.map((row) => row.length),
1457
+ );
1458
+ const fitToWidth = columnCount <= 6;
1459
+ // Default: rely on the browser's auto-table-layout under
1460
+ // fit-content for short / uniform tables (best
1461
+ // proportional sizing). When one column has long prose
1462
+ // (Mechanism / Description / Notes), auto-layout squeezes
1463
+ // the rigid columns; computeFitWidthColumnLayout picks a
1464
+ // flex column and assigns explicit widths so the rigid
1465
+ // columns stay at content-fit and the flex column wraps to
1466
+ // absorb the remainder.
1467
+ const layout = fitToWidth
1468
+ ? computeFitWidthColumnLayout([...headerRows, ...bodyRows])
1469
+ : null;
1470
+ const hasFlexColumn = !!layout;
1471
+ const flexIndex = layout ? layout.flexIndex : -1;
1472
+ const cellClass = (cellIndex) => (cellIndex === flexIndex ? "is-flex-column" : null);
1473
+ const columnLabels = buildTableColumnLabels(headerRows, columnCount);
1474
+ const tableClass = `ps-chat-table${fitToWidth ? " is-fit-width" : ""}${hasFlexColumn ? " has-flex-column" : ""}`;
1475
+ const wrapClass = `ps-chat-table-wrap${fitToWidth ? " is-fit-width" : ""}${hasFlexColumn ? " has-flex-column" : ""}`;
1476
+ return React.createElement("div", {
1477
+ key: `table:${index}`,
1478
+ className: wrapClass,
1479
+ },
1480
+ React.createElement("table", {
1481
+ className: tableClass,
1482
+ style: layout?.minWidth ? { "--ps-table-min-width": layout.minWidth } : undefined,
1483
+ },
1484
+ layout
1485
+ ? React.createElement("colgroup", null,
1486
+ layout.widths.map((width, columnIndex) => React.createElement("col", {
1487
+ key: `col:${columnIndex}`,
1488
+ className: cellClass(columnIndex) || undefined,
1489
+ style: { width },
1490
+ })))
1491
+ : null,
1492
+ headerRows.length > 0
1493
+ ? React.createElement("thead", null,
1494
+ headerRows.map((row, rowIndex) => React.createElement("tr", { key: `thead:${rowIndex}` },
1495
+ Array.from({ length: columnCount }, (_, cellIndex) => React.createElement("th", {
1496
+ key: `th:${rowIndex}:${cellIndex}`,
1497
+ className: cellClass(cellIndex) || undefined,
1498
+ },
1499
+ renderInlineMarkdown(row[cellIndex] || "", theme, `chat-table:${index}:head:${rowIndex}:${cellIndex}`))))))
1500
+ : null,
1501
+ React.createElement("tbody", null,
1502
+ bodyRows.map((row, rowIndex) => React.createElement("tr", { key: `tbody:${rowIndex}` },
1503
+ Array.from({ length: columnCount }, (_, cellIndex) => React.createElement("td", {
1504
+ key: `td:${rowIndex}:${cellIndex}`,
1505
+ className: cellClass(cellIndex) || undefined,
1506
+ "data-label": columnLabels[cellIndex] || undefined,
1507
+ },
1508
+ React.createElement("span", { className: "ps-table-cell-value" },
1509
+ renderInlineMarkdown(row[cellIndex] || "", theme, `chat-table:${index}:${rowIndex}:${cellIndex}`)))))))));
1510
+ }
1511
+
1512
+ return React.createElement(Line, { key: `line:${index}`, line: block.line, theme });
1513
+ }));
1514
+ }
1515
+
1516
+ function Panel({ title, titleRight = null, color = "gray", focused = false, actions = null, children, theme, className = "" }) {
1517
+ const accent = resolveColor(theme, color);
1518
+ return React.createElement("section", {
1519
+ className: `ps-panel${focused ? " is-focused" : ""}${className ? ` ${className}` : ""}`,
1520
+ style: { "--ps-panel-accent": accent || "var(--ps-border)" },
1521
+ },
1522
+ React.createElement("header", { className: "ps-panel-header" },
1523
+ React.createElement("div", { className: "ps-panel-title" },
1524
+ Array.isArray(title)
1525
+ ? React.createElement(Runs, { runs: title, theme })
1526
+ : flattenTitleText(title)),
1527
+ titleRight || actions
1528
+ ? React.createElement("div", { className: "ps-panel-header-right" },
1529
+ titleRight
1530
+ ? React.createElement("div", { className: "ps-panel-title-right" },
1531
+ Array.isArray(titleRight)
1532
+ ? React.createElement(Runs, { runs: titleRight, theme })
1533
+ : flattenTitleText(titleRight))
1534
+ : null,
1535
+ actions ? React.createElement("div", { className: "ps-panel-actions" }, actions) : null,
1536
+ )
1537
+ : null,
1538
+ ),
1539
+ React.createElement("div", { className: "ps-panel-body" }, children));
1540
+ }
1541
+
1542
+ function PortalSequenceLines({ lines, theme, completionByTurn }) {
1543
+ const [expanded, setExpanded] = React.useState(() => new Set());
1544
+ const toggle = React.useCallback((key) => {
1545
+ setExpanded((current) => {
1546
+ const next = new Set(current);
1547
+ if (next.has(key)) next.delete(key);
1548
+ else next.add(key);
1549
+ return next;
1550
+ });
1551
+ }, []);
1552
+ const compactNumber = (value) => Number(value || 0).toLocaleString("en-US");
1553
+ const formatTokenK = (value) => {
1554
+ const tokens = Number(value || 0);
1555
+ if (!Number.isFinite(tokens)) return "?K";
1556
+ const scaled = tokens / 1000;
1557
+ const decimals = Math.abs(scaled) >= 10 ? 1 : 2;
1558
+ return `${scaled.toFixed(decimals)}K`;
1559
+ };
1560
+ const formatDurationSeconds = (value) => {
1561
+ const ms = Number(value || 0);
1562
+ if (!Number.isFinite(ms)) return "?s";
1563
+ return `${(ms / 1000).toFixed(1)}s`;
1564
+ };
1565
+ const shortModelOnly = (value) => {
1566
+ const model = String(value || "").trim();
1567
+ if (!model) return "unknown";
1568
+ const parts = model.split(":").filter(Boolean);
1569
+ const last = parts[parts.length - 1] || "";
1570
+ const modelIndex = parts.length > 1 && REASONING_EFFORT_LABELS.has(last.toLowerCase())
1571
+ ? parts.length - 2
1572
+ : parts.length - 1;
1573
+ const maybeModel = parts[Math.max(0, modelIndex)] || parts[0];
1574
+ return maybeModel || "unknown";
1575
+ };
1576
+
1577
+ return React.createElement(React.Fragment, null,
1578
+ lines.map((line, index) => {
1579
+ // The selector tags the completed-turn run structurally; the display
1580
+ // text is width-truncated in narrow columns, so never regex it.
1581
+ const completedTurn = line?.kind === "runs" && Array.isArray(line.runs)
1582
+ ? line.runs.find((run) => run?.completedTurn != null)?.completedTurn
1583
+ : null;
1584
+ const completion = completedTurn != null ? completionByTurn.get(Number(completedTurn)) : null;
1585
+ if (!completion) {
1586
+ return React.createElement(Line, { key: `line:${index}`, line, theme });
1587
+ }
1588
+ const data = completion.data || {};
1589
+ const key = String(completion.seq ?? `${data.turnIndex ?? completedTurn}:${completion.createdAt ?? index}`);
1590
+ const isExpanded = expanded.has(key);
1591
+ const fullModelLabel = data.reasoningEffort ? `${data.model || "(unknown)"}:${data.reasoningEffort}` : (data.model || "(unknown)");
1592
+ const modelLabel = shortModelOnly(data.model);
1593
+ const duration = data.durationMs != null ? formatDurationSeconds(data.durationMs) : "duration n/a";
1594
+ const durationDetail = data.durationMs != null ? `${compactNumber(data.durationMs)} ms` : "duration n/a";
1595
+ const tokens = `${formatTokenK(data.tokensInput)} / ${formatTokenK(data.tokensOutput)}`;
1596
+ const details = [
1597
+ ["model", fullModelLabel],
1598
+ ["started", data.startedAt ? new Date(data.startedAt).toLocaleString() : null],
1599
+ ["ended", data.endedAt ? new Date(data.endedAt).toLocaleString() : null],
1600
+ ["duration", durationDetail],
1601
+ ["tokens", `${compactNumber(data.tokensInput)} in / ${compactNumber(data.tokensOutput)} out`],
1602
+ ["cache", `${compactNumber(data.tokensCacheRead)} read / ${compactNumber(data.tokensCacheWrite)} write`],
1603
+ ["tools", `${compactNumber(data.toolCalls)} calls / ${compactNumber(data.toolErrors)} errors`],
1604
+ ["names", Array.isArray(data.toolNames) && data.toolNames.length ? data.toolNames.join(", ") : null],
1605
+ ["worker", data.workerNodeId || null],
1606
+ ["result", data.resultType || null],
1607
+ ["error", data.errorMessage || null],
1608
+ ].filter(([, value]) => value != null && value !== "");
1609
+ return React.createElement(React.Fragment, { key: `seq:${index}` },
1610
+ React.createElement(Line, { line, theme }),
1611
+ React.createElement("button", {
1612
+ type: "button",
1613
+ className: "ps-sequence-turn-divider",
1614
+ onClick: () => toggle(key),
1615
+ },
1616
+ React.createElement("span", { className: "ps-sequence-turn-caret" }, isExpanded ? "v" : ">"),
1617
+ React.createElement("span", { className: "ps-sequence-turn-rule" }, "--- "),
1618
+ React.createElement("span", { className: "ps-sequence-turn-key" }, "Mod: "),
1619
+ React.createElement("span", { className: "ps-sequence-turn-model" }, modelLabel),
1620
+ React.createElement("span", { className: "ps-sequence-turn-rule" }, " ("),
1621
+ React.createElement("span", { className: "ps-sequence-turn-key" }, "tok: "),
1622
+ React.createElement("span", { className: "ps-sequence-turn-tokens" }, tokens),
1623
+ React.createElement("span", { className: "ps-sequence-turn-rule" }, ", "),
1624
+ React.createElement("span", { className: "ps-sequence-turn-key" }, "dur: "),
1625
+ React.createElement("span", { className: "ps-sequence-turn-duration" }, duration),
1626
+ React.createElement("span", { className: "ps-sequence-turn-rule" }, ") ---"),
1627
+ ),
1628
+ isExpanded
1629
+ ? React.createElement("div", { className: "ps-sequence-turn-details" },
1630
+ details.map(([label, value]) => React.createElement("div", { key: label },
1631
+ React.createElement("span", { className: "ps-sequence-turn-detail-label" }, `${label}: `),
1632
+ React.createElement("span", null, String(value)),
1633
+ )))
1634
+ : null,
1635
+ );
1636
+ }),
1637
+ );
1638
+ }
1639
+
1640
+ function ScrollLinesPanel({ title, titleRight = null, color, focused, actions, lines, stickyLines = [], bottomStickyLines = [], scrollOffset = 0, scrollMode = "top", paneKey, controller, className = "", panelClassName = "", topContent = null, bottomContent = null, structuredBlocks = false, stickyBottom = false, renderBody = null }) {
1641
+ const themeId = useControllerSelector(controller, (state) => state.ui.themeId);
1642
+ const theme = getTheme(themeId);
1643
+ const ref = React.useRef(null);
1644
+ const stickyRef = React.useRef(null);
1645
+ const syncingHorizontalRef = React.useRef(false);
1646
+ const { normalizedLines, onScroll, onWheel } = useScrollSync(ref, lines, scrollOffset, scrollMode, paneKey, controller, { stickyBottom });
1647
+ const normalizedSticky = React.useMemo(() => normalizeLines(stickyLines), [stickyLines]);
1648
+ const normalizedBottomSticky = React.useMemo(() => normalizeLines(bottomStickyLines), [bottomStickyLines]);
1649
+ const preserveHorizontalScroll = className.includes("is-preserve") && panelClassName.includes("has-preserved-sticky");
1650
+
1651
+ const syncScrollLeft = React.useCallback((source, target) => {
1652
+ if (!source || !target) return;
1653
+ if (Math.abs((target.scrollLeft || 0) - (source.scrollLeft || 0)) <= 1) return;
1654
+ syncingHorizontalRef.current = true;
1655
+ target.scrollLeft = source.scrollLeft;
1656
+ window.requestAnimationFrame(() => {
1657
+ syncingHorizontalRef.current = false;
1658
+ });
1659
+ }, []);
1660
+
1661
+ const handleBodyScroll = React.useCallback((event) => {
1662
+ onScroll();
1663
+ if (!preserveHorizontalScroll || syncingHorizontalRef.current) return;
1664
+ syncScrollLeft(event.currentTarget, stickyRef.current);
1665
+ }, [onScroll, preserveHorizontalScroll, syncScrollLeft]);
1666
+
1667
+ const handleStickyScroll = React.useCallback((event) => {
1668
+ if (!preserveHorizontalScroll || syncingHorizontalRef.current) return;
1669
+ syncScrollLeft(event.currentTarget, ref.current);
1670
+ }, [preserveHorizontalScroll, syncScrollLeft]);
1671
+
1672
+ return React.createElement(Panel, { title, titleRight, color, focused, actions, theme, className: panelClassName },
1673
+ topContent,
1674
+ normalizedSticky.length > 0
1675
+ ? React.createElement("div", {
1676
+ ref: stickyRef,
1677
+ className: `ps-panel-sticky${preserveHorizontalScroll ? " is-scroll-sync" : ""}`,
1678
+ onScroll: handleStickyScroll,
1679
+ },
1680
+ normalizedSticky.map((line, index) => React.createElement(Line, { key: `sticky:${index}`, line, theme })),
1681
+ )
1682
+ : null,
1683
+ React.createElement("div", { ref, className: `ps-scroll-panel ${className}`.trim(), onScroll: handleBodyScroll, onWheel },
1684
+ typeof renderBody === "function"
1685
+ ? renderBody(normalizedLines, theme)
1686
+ : structuredBlocks
1687
+ ? React.createElement(StructuredChatBlocks, { lines: normalizedLines, theme })
1688
+ : normalizedLines.map((line, index) => React.createElement(Line, { key: `line:${index}`, line, theme })),
1689
+ ),
1690
+ normalizedBottomSticky.length > 0
1691
+ ? React.createElement("div", {
1692
+ className: "ps-panel-bottom-sticky",
1693
+ },
1694
+ normalizedBottomSticky.map((line, index) => React.createElement(Line, { key: `bottom-sticky:${index}`, line, theme })),
1695
+ )
1696
+ : null,
1697
+ bottomContent);
1698
+ }
1699
+
1700
+ function SessionRowContent({ row, theme, structured = false }) {
1701
+ const hasStructuredRuns = structured && Array.isArray(row.titleRuns);
1702
+ if (!hasStructuredRuns) {
1703
+ return Array.isArray(row.runs)
1704
+ ? React.createElement(Runs, { runs: row.runs, theme })
1705
+ : row.text;
1706
+ }
1707
+
1708
+ const hasMeta = Array.isArray(row.metaRuns) && row.metaRuns.length > 0;
1709
+ const hasBadges = Array.isArray(row.badgeRuns) && row.badgeRuns.length > 0;
1710
+ const hasSelectedMeta = row.active && Array.isArray(row.selectedMetaRuns) && row.selectedMetaRuns.length > 0;
1711
+
1712
+ return React.createElement(React.Fragment, null,
1713
+ React.createElement("div", { className: "ps-session-row-title" },
1714
+ React.createElement(Runs, { runs: row.titleRuns, theme }),
1715
+ // Meta (timestamp / member count) sits inline on the title line.
1716
+ hasMeta
1717
+ ? React.createElement("span", { className: "ps-session-row-meta" },
1718
+ React.createElement(Runs, { runs: [{ text: " ", color: "gray" }, ...row.metaRuns], theme }))
1719
+ : null),
1720
+ hasBadges
1721
+ ? React.createElement("div", { className: "ps-session-row-badges" },
1722
+ React.createElement(Runs, { runs: row.badgeRuns, theme }))
1723
+ : null,
1724
+ hasSelectedMeta
1725
+ ? React.createElement("div", { className: "ps-session-row-selected-meta" },
1726
+ React.createElement(Runs, { runs: row.selectedMetaRuns, theme }))
1727
+ : null);
1728
+ }
1729
+
1730
+ function SessionPane({ controller, actions = null, panelClassName = "", structuredRows = false }) {
1731
+ const themeId = useControllerSelector(controller, (state) => state.ui.themeId);
1732
+ const theme = getTheme(themeId);
1733
+ const sessionButtonRefs = React.useRef(new Map());
1734
+ const viewState = useControllerSelector(controller, (state) => ({
1735
+ activeSessionId: state.sessions.activeSessionId,
1736
+ sessionsById: state.sessions.byId,
1737
+ sessionsFlat: state.sessions.flat,
1738
+ filterQuery: state.sessions.filterQuery || "",
1739
+ ownerFilter: state.sessions.ownerFilter,
1740
+ pinnedIds: state.sessions.pinnedIds,
1741
+ selectedIds: state.sessions.selectedIds,
1742
+ selectMode: state.sessions.selectMode,
1743
+ auth: state.auth,
1744
+ connectionMode: state.connection?.mode || "local",
1745
+ modalOpen: Boolean(state.ui.modal),
1746
+ focused: state.ui.focusRegion === "sessions",
1747
+ }), shallowEqualObject);
1748
+ const rows = React.useMemo(() => selectSessionRows({
1749
+ sessions: {
1750
+ activeSessionId: viewState.activeSessionId,
1751
+ byId: viewState.sessionsById,
1752
+ flat: viewState.sessionsFlat,
1753
+ filterQuery: viewState.filterQuery,
1754
+ ownerFilter: viewState.ownerFilter,
1755
+ pinnedIds: viewState.pinnedIds,
1756
+ selectedIds: viewState.selectedIds,
1757
+ selectMode: viewState.selectMode,
1758
+ },
1759
+ auth: viewState.auth,
1760
+ connection: {
1761
+ mode: viewState.connectionMode,
1762
+ },
1763
+ }), [viewState.activeSessionId, viewState.auth, viewState.connectionMode, viewState.filterQuery, viewState.ownerFilter, viewState.pinnedIds, viewState.selectedIds, viewState.selectMode, viewState.sessionsById, viewState.sessionsFlat]);
1764
+ const activeSession = viewState.activeSessionId
1765
+ ? viewState.sessionsById[viewState.activeSessionId] || null
1766
+ : null;
1767
+ const canRenameActiveSession = Boolean(activeSession && !activeSession.isSystem);
1768
+ const selectedCount = Array.isArray(viewState.selectedIds) ? viewState.selectedIds.length : 0;
1769
+ const isBulkSelection = selectedCount > 1;
1770
+ const canPinActiveSession = Boolean(
1771
+ activeSession
1772
+ && !activeSession.isSystem
1773
+ && (activeSession.isGroup || (!activeSession.parentSessionId && !activeSession.groupId)),
1774
+ );
1775
+ const isActivePinned = Boolean(
1776
+ activeSession
1777
+ && Array.isArray(viewState.pinnedIds)
1778
+ && viewState.pinnedIds.includes(activeSession.sessionId),
1779
+ );
1780
+ const activeGroupCanDelete = Boolean(activeSession?.isGroup && Number(activeSession.memberCount || 0) === 0);
1781
+ const canTerminate = isBulkSelection
1782
+ ? Array.isArray(viewState.selectedIds) && viewState.selectedIds.some((id) => {
1783
+ const s = viewState.sessionsById[id];
1784
+ return s && !s.isSystem && !s.isGroup;
1785
+ })
1786
+ : activeSession?.isGroup ? true : Boolean(activeSession);
1787
+ const activeSessionActionLabel = activeSession?.isGroup
1788
+ ? "Delete"
1789
+ : isBulkSelection
1790
+ ? `Terminate (${selectedCount})`
1791
+ : activeSession?.isSystem
1792
+ ? "Restart"
1793
+ : "Terminate";
1794
+ const hasExplicitSelection = selectedCount > 0;
1795
+ const groupableIds = hasExplicitSelection
1796
+ ? viewState.selectedIds.filter((id) => {
1797
+ const session = viewState.sessionsById[id];
1798
+ return session && !session.isSystem && !session.isGroup && !session.parentSessionId;
1799
+ })
1800
+ : (activeSession && !activeSession.isSystem && !activeSession.isGroup && !activeSession.parentSessionId ? [activeSession.sessionId] : []);
1801
+ const canMoveToGroup = groupableIds.length > 0;
1802
+ const combinedPanelClassName = `ps-session-pane${panelClassName ? ` ${panelClassName}` : ""}`;
1803
+ const setSessionButtonRef = React.useCallback((sessionId, node) => {
1804
+ if (!sessionId) return;
1805
+ if (node) {
1806
+ sessionButtonRefs.current.set(sessionId, node);
1807
+ } else {
1808
+ sessionButtonRefs.current.delete(sessionId);
1809
+ }
1810
+ }, []);
1811
+
1812
+ React.useEffect(() => {
1813
+ if (viewState.modalOpen || !viewState.focused || !viewState.activeSessionId) return;
1814
+ const activeButton = sessionButtonRefs.current.get(viewState.activeSessionId);
1815
+ if (!activeButton) return;
1816
+
1817
+ if (document.activeElement !== activeButton) {
1818
+ activeButton.focus({ preventScroll: true });
1819
+ }
1820
+ // Only pull the active row into view when the active session itself
1821
+ // changes (or on focus/modal transitions). Re-running scrollIntoView
1822
+ // on every `sessions/loaded` refresh would yank the list back to the
1823
+ // active row even when the user has deliberately scrolled away to
1824
+ // browse older sessions in a long list.
1825
+ activeButton.scrollIntoView({ block: "nearest" });
1826
+ }, [viewState.activeSessionId, viewState.focused, viewState.modalOpen]);
1827
+
1828
+ const panelActions = React.createElement(React.Fragment, null,
1829
+ isBulkSelection
1830
+ ? React.createElement("span", {
1831
+ className: "ps-mini-button-label",
1832
+ style: { padding: "0 6px", fontSize: "12px", opacity: 0.85 },
1833
+ title: "Multiple sessions selected. Move to group or cancel selected sessions; click Clear to exit.",
1834
+ }, `${selectedCount} selected`)
1835
+ : null,
1836
+ isBulkSelection
1837
+ ? React.createElement("button", {
1838
+ type: "button",
1839
+ className: "ps-mini-button",
1840
+ onClick: () => controller.handleCommand(UI_COMMANDS.CLEAR_SESSION_SELECTION).catch(() => {}),
1841
+ title: "Clear multi-selection",
1842
+ }, "Clear")
1843
+ : React.createElement("button", {
1844
+ type: "button",
1845
+ className: "ps-mini-button",
1846
+ onClick: () => controller.handleCommand(UI_COMMANDS.PIN_SESSION).catch(() => {}),
1847
+ disabled: !canPinActiveSession,
1848
+ title: canPinActiveSession
1849
+ ? (isActivePinned
1850
+ ? "Unpin this session"
1851
+ : "Pin this session to the top of the list")
1852
+ : "Only top-level non-system sessions can be pinned",
1853
+ }, isActivePinned ? "Unpin" : "Pin"),
1854
+ React.createElement("button", {
1855
+ type: "button",
1856
+ className: "ps-mini-button",
1857
+ onClick: () => controller.handleCommand(UI_COMMANDS.OPEN_MOVE_TO_GROUP).catch(() => {}),
1858
+ disabled: !canMoveToGroup,
1859
+ title: canMoveToGroup
1860
+ ? (groupableIds.length > 1 ? `Move ${groupableIds.length} selected sessions to a group` : "Move this session to a group")
1861
+ : "Select a top-level non-system session to move to a group",
1862
+ }, groupableIds.length > 1 ? `Group (${groupableIds.length})` : "Group"),
1863
+ React.createElement("button", {
1864
+ type: "button",
1865
+ className: "ps-mini-button",
1866
+ onClick: () => controller.handleCommand(UI_COMMANDS.OPEN_RENAME_SESSION).catch(() => {}),
1867
+ disabled: !canRenameActiveSession || isBulkSelection,
1868
+ title: isBulkSelection ? "Disabled while multiple sessions are selected" : undefined,
1869
+ }, "Rename"),
1870
+ React.createElement("button", {
1871
+ type: "button",
1872
+ className: "ps-mini-button",
1873
+ onClick: () => controller.handleCommand(activeSession?.isGroup ? UI_COMMANDS.DELETE_SESSION : UI_COMMANDS.OPEN_TERMINATE_PICKER).catch(() => {}),
1874
+ disabled: !canTerminate,
1875
+ title: isBulkSelection
1876
+ ? `Terminate ${selectedCount} selected sessions (Mark Completed, Cancel, or Delete)`
1877
+ : activeSession?.isGroup
1878
+ ? activeGroupCanDelete ? "Delete this empty group" : "Show why this group cannot be deleted yet"
1879
+ : activeSession?.isSystem
1880
+ ? "Restart this system session; choose complete, terminate, or hard delete disposition"
1881
+ : "Mark Completed, Cancel, or Delete the active session",
1882
+ }, activeSessionActionLabel),
1883
+ actions);
1884
+
1885
+ return React.createElement(Panel, {
1886
+ title: [{ text: "Sessions", color: "yellow", bold: true }],
1887
+ color: "yellow",
1888
+ focused: viewState.focused,
1889
+ theme,
1890
+ actions: panelActions,
1891
+ className: combinedPanelClassName,
1892
+ },
1893
+ React.createElement("div", { className: "ps-action-list ps-session-list" },
1894
+ rows.length === 0
1895
+ ? React.createElement("div", { className: "ps-empty-state" }, viewState.filterQuery
1896
+ ? `No sessions matched "@@${viewState.filterQuery}".`
1897
+ : "No sessions yet.")
1898
+ : rows.map((row) => React.createElement("button", {
1899
+ key: row.sessionId,
1900
+ type: "button",
1901
+ ref: (node) => setSessionButtonRef(row.sessionId, node),
1902
+ className: `ps-list-button ps-session-list-button${row.active ? " is-selected" : ""}${row.selected ? " is-multiselected" : ""}${row.pinned ? " is-pinned" : ""}`,
1903
+ tabIndex: row.active ? 0 : -1,
1904
+ "aria-selected": row.active ? "true" : "false",
1905
+ onClick: (event) => {
1906
+ // Cmd/Ctrl-click toggles multi-selection (any row).
1907
+ if (event.metaKey || event.ctrlKey) {
1908
+ event.preventDefault();
1909
+ if (!viewState.selectMode) {
1910
+ controller.dispatch({ type: "sessions/selectMode", enabled: true });
1911
+ }
1912
+ controller.dispatch({ type: "sessions/selectToggle", sessionId: row.sessionId });
1913
+ controller.setFocus("sessions");
1914
+ return;
1915
+ }
1916
+ // Shift-click selects a contiguous range from the active row.
1917
+ if (event.shiftKey && viewState.activeSessionId) {
1918
+ event.preventDefault();
1919
+ const ids = rows.map((r) => r.sessionId);
1920
+ const startIndex = ids.indexOf(viewState.activeSessionId);
1921
+ const endIndex = ids.indexOf(row.sessionId);
1922
+ if (startIndex >= 0 && endIndex >= 0) {
1923
+ const [from, to] = startIndex <= endIndex
1924
+ ? [startIndex, endIndex]
1925
+ : [endIndex, startIndex];
1926
+ const range = ids.slice(from, to + 1);
1927
+ const merged = Array.from(new Set([...(viewState.selectedIds || []), ...range]));
1928
+ controller.setSessionSelection(merged);
1929
+ controller.setFocus("sessions");
1930
+ return;
1931
+ }
1932
+ }
1933
+ const shouldToggleChildren = row.hasChildren && row.active;
1934
+ if (shouldToggleChildren) {
1935
+ controller.dispatch({
1936
+ type: row.collapsed ? "sessions/expand" : "sessions/collapse",
1937
+ sessionId: row.sessionId,
1938
+ });
1939
+ controller.setFocus("sessions");
1940
+ return;
1941
+ }
1942
+ // A normal click on a different row clears any
1943
+ // multi-selection and switches the active session.
1944
+ if (viewState.selectMode) {
1945
+ controller.dispatch({ type: "sessions/selectClear" });
1946
+ }
1947
+ controller.setFocus("sessions");
1948
+ if (!row.active) {
1949
+ controller.loadSession(row.sessionId).catch(() => {});
1950
+ }
1951
+ },
1952
+ },
1953
+ React.createElement("div", {
1954
+ className: "ps-line ps-session-row-content",
1955
+ style: { paddingInlineStart: `${Math.max(0, row.depth) * 18}px` },
1956
+ },
1957
+ React.createElement(SessionRowContent, { row, theme, structured: structuredRows })),
1958
+ )),
1959
+ ));
1960
+ }
1961
+
1962
+ function ChatPane({ controller, mobile = false, fullWidth = false, showComposer = true }) {
1963
+ const themeId = useControllerSelector(controller, (state) => state.ui.themeId);
1964
+ const theme = getTheme(themeId);
1965
+ const viewState = useControllerSelector(controller, (state) => {
1966
+ const activeSessionId = state.sessions.activeSessionId;
1967
+ const layout = computeStateLayout(state);
1968
+ const paneWidth = fullWidth || mobile
1969
+ ? layout.totalWidth
1970
+ : layout.leftWidth;
1971
+ const contentWidth = Math.max(20, paneWidth - 4);
1972
+ return {
1973
+ activeSessionId,
1974
+ activeHistory: activeSessionId ? state.history.bySessionId.get(activeSessionId) || null : null,
1975
+ activeOutbox: activeSessionId && state.outbox?.bySessionId?.[activeSessionId]
1976
+ ? state.outbox.bySessionId[activeSessionId]
1977
+ : [],
1978
+ branding: state.branding,
1979
+ connection: state.connection,
1980
+ sessionsById: state.sessions.byId,
1981
+ sessionsFlat: state.sessions.flat,
1982
+ inspectorTab: state.ui.inspectorTab,
1983
+ chatViewMode: state.ui.chatViewMode || "transcript",
1984
+ activeSessionIsGroup: Boolean(activeSessionId && state.sessions.byId[activeSessionId]?.isGroup),
1985
+ focused: state.ui.focusRegion === "chat",
1986
+ scroll: state.ui.scroll.chat,
1987
+ contentWidth,
1988
+ };
1989
+ }, shallowEqualObject);
1990
+ const selectorState = React.useMemo(() => ({
1991
+ branding: viewState.branding,
1992
+ connection: viewState.connection,
1993
+ sessions: {
1994
+ activeSessionId: viewState.activeSessionId,
1995
+ byId: viewState.sessionsById,
1996
+ flat: viewState.sessionsFlat,
1997
+ },
1998
+ history: {
1999
+ bySessionId: viewState.activeSessionId && viewState.activeHistory
2000
+ ? new Map([[viewState.activeSessionId, viewState.activeHistory]])
2001
+ : new Map(),
2002
+ },
2003
+ outbox: {
2004
+ bySessionId: viewState.activeSessionId
2005
+ ? { [viewState.activeSessionId]: viewState.activeOutbox }
2006
+ : {},
2007
+ },
2008
+ ui: {
2009
+ inspectorTab: viewState.inspectorTab,
2010
+ chatViewMode: viewState.chatViewMode,
2011
+ },
2012
+ }), [
2013
+ viewState.activeHistory,
2014
+ viewState.activeSessionId,
2015
+ viewState.activeOutbox,
2016
+ viewState.branding,
2017
+ viewState.connection,
2018
+ viewState.chatViewMode,
2019
+ viewState.inspectorTab,
2020
+ viewState.sessionsById,
2021
+ viewState.sessionsFlat,
2022
+ ]);
2023
+ const chrome = React.useMemo(
2024
+ () => selectChatPaneChrome(selectorState, { width: viewState.contentWidth }),
2025
+ [selectorState, viewState.contentWidth],
2026
+ );
2027
+ const animatedDots = useAnimatedDots(Boolean(chrome.animateTitleRight));
2028
+ const titleRight = React.useMemo(
2029
+ () => appendAnimatedDotsToRuns(chrome.titleRight, chrome.animateTitleRight ? animatedDots : ""),
2030
+ [animatedDots, chrome.animateTitleRight, chrome.titleRight],
2031
+ );
2032
+ const lines = React.useMemo(
2033
+ () => selectChatLines(selectorState, viewState.contentWidth, { tableMode: "sentinel" }),
2034
+ [selectorState, viewState.contentWidth],
2035
+ );
2036
+ const outboxLines = React.useMemo(
2037
+ () => selectOutboxOverlayLines(selectorState, viewState.contentWidth, { tableMode: "sentinel" }),
2038
+ [selectorState, viewState.contentWidth],
2039
+ );
2040
+ const composer = showComposer && !viewState.activeSessionIsGroup && viewState.chatViewMode !== "summary"
2041
+ ? React.createElement("div", { className: "ps-chat-composer" },
2042
+ React.createElement(PromptComposer, { controller, mobile, active: true }))
2043
+ : null;
2044
+
2045
+ return React.createElement(ScrollLinesPanel, {
2046
+ controller,
2047
+ title: mobile ? compactTitleRuns(chrome.title, 28) : chrome.title,
2048
+ titleRight: mobile && titleRight ? compactTitleRuns(titleRight, 18) : titleRight,
2049
+ // Chat/Summary toggling lives on the top toolbar so the pane chrome
2050
+ // stays clean. The toolbar button is disabled for group sessions.
2051
+ actions: null,
2052
+ color: chrome.color,
2053
+ focused: viewState.focused,
2054
+ lines,
2055
+ bottomStickyLines: outboxLines,
2056
+ scrollOffset: viewState.scroll,
2057
+ scrollMode: "bottom",
2058
+ paneKey: "chat",
2059
+ className: "is-wrapped",
2060
+ panelClassName: "ps-chat-panel",
2061
+ bottomContent: composer,
2062
+ structuredBlocks: true,
2063
+ });
2064
+ }
2065
+
2066
+ function MobileWorkspace({ controller, sessionsCollapsed, setSessionsCollapsed }) {
2067
+ const themeId = useControllerSelector(controller, (state) => state.ui.themeId);
2068
+ const theme = getTheme(themeId);
2069
+ const sessionToggle = React.createElement("button", {
2070
+ type: "button",
2071
+ className: "ps-mini-button",
2072
+ onClick: () => setSessionsCollapsed((current) => !current),
2073
+ }, sessionsCollapsed ? "Show" : "Hide");
2074
+
2075
+ return React.createElement("div", { className: "ps-mobile-workspace" },
2076
+ sessionsCollapsed
2077
+ ? React.createElement(Panel, {
2078
+ title: [{ text: "Sessions", color: "yellow", bold: true }],
2079
+ color: "yellow",
2080
+ focused: false,
2081
+ theme,
2082
+ actions: sessionToggle,
2083
+ className: "ps-mobile-session-collapsed",
2084
+ },
2085
+ React.createElement("div", { className: "ps-mobile-session-summary" }, "Session list collapsed."))
2086
+ : React.createElement(SessionPane, {
2087
+ controller,
2088
+ actions: sessionToggle,
2089
+ panelClassName: "ps-mobile-session-pane",
2090
+ }),
2091
+ React.createElement("div", { className: "ps-mobile-chat-pane" },
2092
+ React.createElement(ChatPane, { controller, mobile: true, fullWidth: true })));
2093
+ }
2094
+
2095
+ function InspectorTabs({ activeTab, controller }) {
2096
+ const visibleTabs = React.useMemo(() => getVisibleInspectorTabs(controller), [controller]);
2097
+ // Tab labels in mobile fall on a single row only when each label is
2098
+ // short. "Node Map" is the only multi-word label, so we shorten it
2099
+ // to "Map" below the mobile breakpoint to keep all six tabs on one
2100
+ // line.
2101
+ const isMobile = typeof window !== "undefined"
2102
+ && (window.innerWidth || 0) > 0
2103
+ && (window.innerWidth || 0) < MOBILE_BREAKPOINT;
2104
+ const labelFor = (tab) => {
2105
+ if (isMobile && tab === "nodes") return "Map";
2106
+ return INSPECTOR_TAB_LABELS[tab] || tab;
2107
+ };
2108
+ return React.createElement("div", { className: "ps-tab-row" },
2109
+ visibleTabs.map((tab) => React.createElement("button", {
2110
+ key: tab,
2111
+ type: "button",
2112
+ className: `ps-tab${activeTab === tab ? " is-active" : ""}`,
2113
+ title: `Switch to ${INSPECTOR_TAB_LABELS[tab] || tab}`,
2114
+ "aria-pressed": activeTab === tab,
2115
+ onClick: () => {
2116
+ controller.setFocus("inspector");
2117
+ controller.selectInspectorTab(tab).catch(() => {});
2118
+ },
2119
+ }, labelFor(tab))));
2120
+ }
2121
+
2122
+ function FilesPane({ controller, focused, mobile = false }) {
2123
+ const themeId = useControllerSelector(controller, (state) => state.ui.themeId);
2124
+ const theme = getTheme(themeId);
2125
+ const fileInputRef = React.useRef(null);
2126
+ const viewState = useControllerSelector(controller, (state) => {
2127
+ const activeSessionId = state.sessions.activeSessionId;
2128
+ const layout = computeStateLayout(state);
2129
+ const paneWidth = (mobile || state.files.fullscreen)
2130
+ ? (state.ui.layout?.viewportWidth ?? 120)
2131
+ : layout.rightWidth;
2132
+ return {
2133
+ activeSessionId,
2134
+ sessionsById: state.sessions.byId,
2135
+ sessionsFlat: state.sessions.flat,
2136
+ filesBySessionId: state.files.bySessionId,
2137
+ filesFilter: state.files.filter,
2138
+ selectedArtifactId: state.files.selectedArtifactId,
2139
+ focused,
2140
+ previewScroll: state.ui.scroll.filePreview,
2141
+ fullscreen: Boolean(state.files.fullscreen),
2142
+ contentWidth: Math.max(20, paneWidth - 4),
2143
+ canBrowserUpload: supportsBrowserFileUploads(controller),
2144
+ canPathUpload: supportsPathArtifactUploads(controller),
2145
+ canDeleteArtifacts: supportsArtifactDelete(controller),
2146
+ canOpenLocally: supportsLocalFileOpen(controller),
2147
+ };
2148
+ }, shallowEqualObject);
2149
+ const selectorState = React.useMemo(() => ({
2150
+ sessions: {
2151
+ activeSessionId: viewState.activeSessionId,
2152
+ byId: viewState.sessionsById,
2153
+ flat: viewState.sessionsFlat,
2154
+ },
2155
+ files: {
2156
+ bySessionId: viewState.filesBySessionId,
2157
+ selectedArtifactId: viewState.selectedArtifactId,
2158
+ filter: viewState.filesFilter,
2159
+ fullscreen: viewState.fullscreen,
2160
+ },
2161
+ ui: {
2162
+ scroll: {
2163
+ filePreview: viewState.previewScroll,
2164
+ },
2165
+ },
2166
+ }), [
2167
+ viewState.activeSessionId,
2168
+ viewState.filesBySessionId,
2169
+ viewState.filesFilter,
2170
+ viewState.fullscreen,
2171
+ viewState.previewScroll,
2172
+ viewState.selectedArtifactId,
2173
+ viewState.sessionsById,
2174
+ viewState.sessionsFlat,
2175
+ ]);
2176
+ const filesView = React.useMemo(() => selectFilesView(selectorState, {
2177
+ listWidth: Math.max(18, viewState.contentWidth - 4),
2178
+ previewWidth: Math.max(18, viewState.contentWidth - 4),
2179
+ showHints: false,
2180
+ }), [selectorState, viewState.contentWidth]);
2181
+ const items = React.useMemo(() => selectFileBrowserItems(selectorState), [selectorState]);
2182
+ const hasSelection = items.length > 0;
2183
+
2184
+ const uploadFiles = React.useCallback((files) => {
2185
+ const nextFiles = Array.isArray(files) ? files.filter(Boolean) : [];
2186
+ if (nextFiles.length === 0) return;
2187
+ controller.uploadArtifactFiles(nextFiles).catch(() => {});
2188
+ }, [controller]);
2189
+
2190
+ const openUploadPicker = React.useCallback(() => {
2191
+ if (viewState.canBrowserUpload && fileInputRef.current) {
2192
+ fileInputRef.current.click();
2193
+ return;
2194
+ }
2195
+ if (viewState.canPathUpload) {
2196
+ controller.handleCommand(UI_COMMANDS.OPEN_ARTIFACT_UPLOAD).catch(() => {});
2197
+ }
2198
+ }, [controller, viewState.canBrowserUpload, viewState.canPathUpload]);
2199
+
2200
+ const panelActions = React.createElement(React.Fragment, null,
2201
+ React.createElement("input", {
2202
+ ref: fileInputRef,
2203
+ type: "file",
2204
+ className: "ps-hidden-file-input",
2205
+ multiple: true,
2206
+ tabIndex: -1,
2207
+ "aria-hidden": "true",
2208
+ onChange: (event) => {
2209
+ uploadFiles(Array.from(event.currentTarget.files || []));
2210
+ event.currentTarget.value = "";
2211
+ },
2212
+ }),
2213
+ React.createElement("button", {
2214
+ type: "button",
2215
+ className: "ps-mini-button",
2216
+ onClick: openUploadPicker,
2217
+ disabled: !viewState.canBrowserUpload && !viewState.canPathUpload,
2218
+ }, "Up"),
2219
+ React.createElement("button", {
2220
+ type: "button",
2221
+ className: "ps-mini-button",
2222
+ onClick: () => controller.handleCommand(UI_COMMANDS.DOWNLOAD_SELECTED_FILE).catch(() => {}),
2223
+ disabled: !hasSelection,
2224
+ }, "Down"),
2225
+ viewState.canDeleteArtifacts ? React.createElement("button", {
2226
+ type: "button",
2227
+ className: "ps-mini-button",
2228
+ onClick: () => controller.handleCommand(UI_COMMANDS.DELETE_SELECTED_FILE).catch(() => {}),
2229
+ disabled: !hasSelection,
2230
+ }, "Delete") : null,
2231
+ viewState.canOpenLocally ? React.createElement("button", {
2232
+ type: "button",
2233
+ className: "ps-mini-button",
2234
+ onClick: () => controller.handleCommand(UI_COMMANDS.OPEN_SELECTED_FILE).catch(() => {}),
2235
+ disabled: !hasSelection,
2236
+ }, "Open") : null,
2237
+ React.createElement("button", {
2238
+ type: "button",
2239
+ className: "ps-mini-button",
2240
+ onClick: () => controller.handleCommand(UI_COMMANDS.OPEN_FILES_FILTER).catch(() => {}),
2241
+ }, "Filter"),
2242
+ React.createElement("button", {
2243
+ type: "button",
2244
+ className: "ps-mini-button",
2245
+ onClick: () => controller.handleCommand(UI_COMMANDS.TOGGLE_FILE_PREVIEW_FULLSCREEN).catch(() => {}),
2246
+ }, viewState.fullscreen ? "Close" : "FS"));
2247
+
2248
+ const listContent = items.length === 0
2249
+ ? normalizeLines(filesView.listBodyLines || []).map((line, index) => React.createElement(Line, {
2250
+ key: `empty:${index}`,
2251
+ line,
2252
+ theme,
2253
+ }))
2254
+ : items.map((item, index) => React.createElement("button", {
2255
+ key: item.id,
2256
+ type: "button",
2257
+ className: `ps-list-button${index === filesView.selectedIndex ? " is-selected" : ""}`,
2258
+ onClick: () => {
2259
+ controller.setFocus("inspector");
2260
+ controller.selectFileBrowserItem(item).catch(() => {});
2261
+ },
2262
+ }, React.createElement(Line, {
2263
+ line: normalizeLines([filesView.listBodyLines?.[index]])[0],
2264
+ theme,
2265
+ })));
2266
+
2267
+ const previewPane = filesView.previewRenderMode === "markdown"
2268
+ && !filesView.previewLoading
2269
+ && !filesView.previewError
2270
+ ? React.createElement(MarkdownPreviewPanel, {
2271
+ controller,
2272
+ title: filesView.previewTitle,
2273
+ color: "cyan",
2274
+ focused: false,
2275
+ scrollOffset: viewState.previewScroll,
2276
+ paneKey: "filePreview",
2277
+ theme,
2278
+ content: filesView.previewContent || "",
2279
+ })
2280
+ : filesView.previewIsBinary
2281
+ && !filesView.previewLoading
2282
+ && !filesView.previewError
2283
+ ? React.createElement(BinaryArtifactPreviewPanel, {
2284
+ title: filesView.previewTitle,
2285
+ color: "cyan",
2286
+ focused: false,
2287
+ theme,
2288
+ filename: filesView.selectedFilename,
2289
+ contentType: filesView.previewContentType,
2290
+ sizeBytes: filesView.previewSizeBytes,
2291
+ source: filesView.previewSource,
2292
+ uploadedAt: filesView.previewUploadedAt,
2293
+ })
2294
+ : React.createElement(ScrollLinesPanel, {
2295
+ controller,
2296
+ title: filesView.previewTitle,
2297
+ color: "cyan",
2298
+ focused: false,
2299
+ lines: filesView.previewLines,
2300
+ scrollOffset: viewState.previewScroll,
2301
+ scrollMode: "top",
2302
+ paneKey: "filePreview",
2303
+ className: "is-preview is-wrapped",
2304
+ });
2305
+ const view = viewState;
2306
+
2307
+ return React.createElement(Panel, {
2308
+ title: view.fullscreen
2309
+ ? filesView.fullscreenTitle
2310
+ : (mobile && filesView.panelTitleMobile)
2311
+ ? filesView.panelTitleMobile
2312
+ : filesView.panelTitle,
2313
+ color: "magenta",
2314
+ focused: view.focused,
2315
+ actions: panelActions,
2316
+ theme,
2317
+ },
2318
+ React.createElement(InspectorTabs, { activeTab: "files", controller }),
2319
+ // Fullscreen files mode shows only the preview pane; the list stays hidden.
2320
+ view.fullscreen
2321
+ ? previewPane
2322
+ : React.createElement("div", { className: "ps-files-grid" },
2323
+ React.createElement(Panel, { title: filesView.listTitle, color: "cyan", theme },
2324
+ React.createElement("div", { className: "ps-action-list" }, listContent)),
2325
+ previewPane,
2326
+ ));
2327
+ }
2328
+
2329
+ function InspectorPane({ controller, mobile = false, panelClassName = "", extraActions = null }) {
2330
+ const viewState = useControllerSelector(controller, (state) => {
2331
+ const layout = computeStateLayout(state);
2332
+ const inspectorTab = state.ui.inspectorTab;
2333
+ const paneWidth = mobile
2334
+ ? (state.ui.layout?.viewportWidth ?? 120)
2335
+ : layout.rightWidth;
2336
+ return {
2337
+ inspectorTab,
2338
+ statsViewMode: state.ui.statsViewMode,
2339
+ activeSessionId: state.sessions.activeSessionId,
2340
+ sessionsById: state.sessions.byId,
2341
+ sessionsFlat: state.sessions.flat,
2342
+ historyBySessionId: state.history.bySessionId,
2343
+ sessionStats: state.sessionStats,
2344
+ fleetStats: state.fleetStats,
2345
+ connection: state.connection,
2346
+ orchestrationBySessionId: state.orchestration.bySessionId,
2347
+ executionHistoryBySessionId: state.executionHistory?.bySessionId || {},
2348
+ executionHistoryFormat: state.executionHistory?.format || "pretty",
2349
+ logs: state.logs,
2350
+ files: state.files,
2351
+ focused: state.ui.focusRegion === "inspector",
2352
+ scroll: state.ui.scroll.inspector,
2353
+ followBottom: state.ui.followBottom?.inspector !== false,
2354
+ logsTailing: state.logs.tailing,
2355
+ filesFullscreen: Boolean(state.files.fullscreen),
2356
+ contentWidth: getPortalInspectorContentWidth(paneWidth, inspectorTab),
2357
+ };
2358
+ }, shallowEqualObject);
2359
+ const selectorState = React.useMemo(() => ({
2360
+ sessions: {
2361
+ activeSessionId: viewState.activeSessionId,
2362
+ byId: viewState.sessionsById,
2363
+ flat: viewState.sessionsFlat,
2364
+ },
2365
+ history: {
2366
+ bySessionId: viewState.historyBySessionId,
2367
+ },
2368
+ sessionStats: viewState.sessionStats,
2369
+ fleetStats: viewState.fleetStats,
2370
+ connection: viewState.connection,
2371
+ ui: {
2372
+ inspectorTab: viewState.inspectorTab,
2373
+ statsViewMode: viewState.statsViewMode,
2374
+ scroll: {
2375
+ inspector: viewState.scroll,
2376
+ },
2377
+ },
2378
+ logs: viewState.logs,
2379
+ files: viewState.files,
2380
+ orchestration: {
2381
+ bySessionId: viewState.orchestrationBySessionId,
2382
+ },
2383
+ executionHistory: {
2384
+ bySessionId: viewState.executionHistoryBySessionId,
2385
+ format: viewState.executionHistoryFormat,
2386
+ },
2387
+ }), [
2388
+ viewState.activeSessionId,
2389
+ viewState.connection,
2390
+ viewState.executionHistoryBySessionId,
2391
+ viewState.executionHistoryFormat,
2392
+ viewState.fleetStats,
2393
+ viewState.files,
2394
+ viewState.historyBySessionId,
2395
+ viewState.inspectorTab,
2396
+ viewState.sessionStats,
2397
+ viewState.statsViewMode,
2398
+ viewState.logs,
2399
+ viewState.orchestrationBySessionId,
2400
+ viewState.scroll,
2401
+ viewState.sessionsById,
2402
+ viewState.sessionsFlat,
2403
+ ]);
2404
+ const inspector = React.useMemo(() => selectInspector(selectorState, {
2405
+ width: viewState.contentWidth,
2406
+ allowWideColumns: mobile,
2407
+ }), [mobile, selectorState, viewState.contentWidth]);
2408
+ const completionByTurn = React.useMemo(() => {
2409
+ const history = viewState.activeSessionId
2410
+ ? viewState.historyBySessionId?.get(viewState.activeSessionId) || null
2411
+ : null;
2412
+ const map = new Map();
2413
+ for (const event of history?.events || []) {
2414
+ if (event?.eventType !== "session.turn_completed") continue;
2415
+ const turn = Number(event?.data?.turnIndex ?? event?.data?.iteration);
2416
+ if (Number.isFinite(turn)) map.set(turn, event);
2417
+ }
2418
+ return map;
2419
+ }, [viewState.activeSessionId, viewState.historyBySessionId]);
2420
+ const renderSequenceBody = React.useCallback((lines, theme) => (
2421
+ React.createElement(PortalSequenceLines, { lines, theme, completionByTurn })
2422
+ ), [completionByTurn]);
2423
+
2424
+ if (viewState.inspectorTab === "files" && !inspector.disabled) {
2425
+ return React.createElement(FilesPane, { controller, focused: viewState.focused, mobile });
2426
+ }
2427
+
2428
+ const actions = [];
2429
+ if (viewState.inspectorTab === "logs") {
2430
+ actions.push(React.createElement("button", {
2431
+ key: "tail",
2432
+ type: "button",
2433
+ className: "ps-mini-button",
2434
+ onClick: () => controller.handleCommand(UI_COMMANDS.TOGGLE_LOG_TAIL).catch(() => {}),
2435
+ }, viewState.logsTailing ? "Stop Tail" : "Tail"));
2436
+ actions.push(React.createElement("button", {
2437
+ key: "filter",
2438
+ type: "button",
2439
+ className: "ps-mini-button",
2440
+ onClick: () => controller.handleCommand(UI_COMMANDS.OPEN_LOG_FILTER).catch(() => {}),
2441
+ }, "Filter"));
2442
+ } else if (viewState.inspectorTab === "history") {
2443
+ actions.push(React.createElement("button", {
2444
+ key: "refresh",
2445
+ type: "button",
2446
+ className: "ps-mini-button",
2447
+ onClick: () => controller.handleCommand(UI_COMMANDS.REFRESH_EXECUTION_HISTORY).catch(() => {}),
2448
+ }, "Refresh"));
2449
+ actions.push(React.createElement("button", {
2450
+ key: "save",
2451
+ type: "button",
2452
+ className: "ps-mini-button",
2453
+ onClick: () => controller.handleCommand(UI_COMMANDS.EXPORT_EXECUTION_HISTORY).catch(() => {}),
2454
+ }, "Artifact"));
2455
+ } else if (viewState.inspectorTab === "stats") {
2456
+ for (const mode of ["session", "fleet", "users"]) {
2457
+ actions.push(React.createElement("button", {
2458
+ key: `stats-view:${mode}`,
2459
+ type: "button",
2460
+ className: `ps-mini-button${viewState.statsViewMode === mode ? " is-active" : ""}`,
2461
+ onClick: () => controller.setStatsViewMode(mode),
2462
+ }, mode.replace(/^./u, (char) => char.toUpperCase())));
2463
+ }
2464
+ }
2465
+
2466
+ const panelActions = extraActions
2467
+ ? actions.concat(extraActions)
2468
+ : actions;
2469
+
2470
+ return React.createElement(ScrollLinesPanel, {
2471
+ controller,
2472
+ title: inspector.title,
2473
+ color: "magenta",
2474
+ focused: viewState.focused,
2475
+ actions: panelActions,
2476
+ topContent: React.createElement(InspectorTabs, { activeTab: inspector.activeTab, controller }),
2477
+ stickyLines: inspector.stickyLines || [],
2478
+ lines: inspector.lines,
2479
+ renderBody: inspector.activeTab === "sequence" ? renderSequenceBody : null,
2480
+ scrollOffset: viewState.scroll,
2481
+ scrollMode: inspector.activeTab === "sequence"
2482
+ ? "bottom"
2483
+ : inspector.activeTab === "logs" && viewState.followBottom
2484
+ ? "bottom"
2485
+ : "top",
2486
+ stickyBottom: inspector.activeTab === "logs",
2487
+ paneKey: "inspector",
2488
+ className: inspector.activeTab === "history" || inspector.activeTab === "logs" ? "is-wrapped" : "is-preserve",
2489
+ panelClassName: `${inspector.activeTab === "sequence" ? "has-preserved-sticky" : ""}${panelClassName ? ` ${panelClassName}` : ""}`.trim(),
2490
+ });
2491
+ }
2492
+
2493
+ function ActivityPane({ controller, panelClassName = "", extraActions = null }) {
2494
+ const viewState = useControllerSelector(controller, (state) => {
2495
+ const activeSessionId = state.sessions.activeSessionId;
2496
+ const layout = computeStateLayout(state);
2497
+ const maxLines = Math.max(3, layout.activityPaneHeight - 2);
2498
+ return {
2499
+ activeSessionId,
2500
+ activeSession: activeSessionId ? state.sessions.byId[activeSessionId] || null : null,
2501
+ activeHistory: activeSessionId ? state.history.bySessionId.get(activeSessionId) || null : null,
2502
+ focused: state.ui.focusRegion === "activity",
2503
+ scroll: state.ui.scroll.activity,
2504
+ followBottom: state.ui.followBottom?.activity !== false,
2505
+ maxLines,
2506
+ };
2507
+ }, shallowEqualObject);
2508
+ const selectorState = React.useMemo(() => ({
2509
+ sessions: {
2510
+ activeSessionId: viewState.activeSessionId,
2511
+ byId: viewState.activeSessionId && viewState.activeSession
2512
+ ? { [viewState.activeSessionId]: viewState.activeSession }
2513
+ : {},
2514
+ },
2515
+ history: {
2516
+ bySessionId: viewState.activeSessionId && viewState.activeHistory
2517
+ ? new Map([[viewState.activeSessionId, viewState.activeHistory]])
2518
+ : new Map(),
2519
+ },
2520
+ }), [viewState.activeHistory, viewState.activeSession, viewState.activeSessionId]);
2521
+ const activity = React.useMemo(
2522
+ () => selectActivityPane(selectorState, viewState.maxLines),
2523
+ [selectorState, viewState.maxLines],
2524
+ );
2525
+
2526
+ return React.createElement(ScrollLinesPanel, {
2527
+ controller,
2528
+ title: activity.title,
2529
+ color: "gray",
2530
+ focused: viewState.focused,
2531
+ actions: extraActions,
2532
+ lines: activity.lines,
2533
+ scrollOffset: viewState.scroll,
2534
+ scrollMode: viewState.followBottom ? "bottom" : "top",
2535
+ stickyBottom: true,
2536
+ paneKey: "activity",
2537
+ className: "is-wrapped",
2538
+ panelClassName,
2539
+ });
2540
+ }
2541
+
2542
+ const CHAT_FOCUS_PANES = [
2543
+ { id: "sessions", label: "Sessions", side: "left" },
2544
+ { id: "inspector", label: "Inspector", side: "right" },
2545
+ { id: "activity", label: "Activity", side: "right" },
2546
+ ];
2547
+
2548
+ function ChatFocusOverlay({ controller, pane, onClose, mobile = false }) {
2549
+ if (!pane) return null;
2550
+
2551
+ let content = null;
2552
+ if (pane === "sessions") {
2553
+ content = React.createElement(SessionPane, {
2554
+ controller,
2555
+ panelClassName: "ps-chat-focus-pane",
2556
+ structuredRows: mobile,
2557
+ actions: React.createElement("button", {
2558
+ type: "button",
2559
+ className: "ps-mini-button",
2560
+ onClick: onClose,
2561
+ }, "Close"),
2562
+ });
2563
+ } else if (pane === "inspector") {
2564
+ content = React.createElement(InspectorPane, {
2565
+ controller,
2566
+ mobile: false,
2567
+ panelClassName: "ps-chat-focus-pane",
2568
+ extraActions: React.createElement("button", {
2569
+ type: "button",
2570
+ className: "ps-mini-button",
2571
+ onClick: onClose,
2572
+ }, "Close"),
2573
+ });
2574
+ } else if (pane === "activity") {
2575
+ content = React.createElement(ActivityPane, {
2576
+ controller,
2577
+ panelClassName: "ps-chat-focus-pane",
2578
+ extraActions: React.createElement("button", {
2579
+ type: "button",
2580
+ className: "ps-mini-button",
2581
+ onClick: onClose,
2582
+ }, "Close"),
2583
+ });
2584
+ }
2585
+
2586
+ const paneMeta = CHAT_FOCUS_PANES.find((entry) => entry.id === pane);
2587
+ return React.createElement("div", {
2588
+ className: `ps-chat-focus-overlay${paneMeta?.side === "left" ? " is-left" : " is-right"}`,
2589
+ }, content);
2590
+ }
2591
+
2592
+ function ChatFocusWorkspace({ controller, openPane, onTogglePane, onExitFocus, mobile = false }) {
2593
+ const focusRegion = useControllerSelector(controller, (state) => state.ui.focusRegion);
2594
+
2595
+ const rail = mobile
2596
+ ? React.createElement("div", { className: "ps-chat-focus-rail" },
2597
+ React.createElement("button", {
2598
+ type: "button",
2599
+ className: "ps-mini-button ps-chat-focus-button",
2600
+ onClick: onExitFocus,
2601
+ }, "Exit Focus"),
2602
+ React.createElement("button", {
2603
+ type: "button",
2604
+ className: `ps-mini-button ps-chat-focus-button${openPane === "sessions" ? " is-active" : ""}`,
2605
+ onClick: () => onTogglePane("sessions"),
2606
+ }, "Sessions"))
2607
+ : React.createElement("div", { className: "ps-chat-focus-rail" },
2608
+ CHAT_FOCUS_PANES.map((pane) => React.createElement("button", {
2609
+ key: pane.id,
2610
+ type: "button",
2611
+ className: `ps-mini-button ps-chat-focus-button${openPane === pane.id ? " is-active" : ""}`,
2612
+ "aria-pressed": openPane === pane.id ? "true" : "false",
2613
+ onClick: () => onTogglePane(pane.id),
2614
+ }, pane.label)),
2615
+ React.createElement("div", { className: "ps-chat-focus-status" },
2616
+ openPane
2617
+ ? `Focused: ${CHAT_FOCUS_PANES.find((pane) => pane.id === openPane)?.label || openPane}`
2618
+ : `Focused: ${focusRegion === "prompt" ? "Prompt" : "Chat"}`));
2619
+
2620
+ return React.createElement("div", { className: "ps-chat-focus-shell" },
2621
+ rail,
2622
+ React.createElement("div", { className: "ps-chat-focus-body" },
2623
+ React.createElement(ChatPane, { controller, mobile, fullWidth: true }),
2624
+ React.createElement(ChatFocusOverlay, {
2625
+ controller,
2626
+ pane: openPane,
2627
+ onClose: () => onTogglePane(openPane),
2628
+ mobile,
2629
+ })));
2630
+ }
2631
+
2632
+ function PromptComposer({ controller, mobile, active = true, onAfterSend = null }) {
2633
+ const promptState = useControllerSelector(controller, (state) => {
2634
+ const activeSessionId = state.sessions.activeSessionId;
2635
+ const activeSession = activeSessionId ? state.sessions.byId[activeSessionId] || null : null;
2636
+ const outbox = activeSessionId && state.outbox?.bySessionId?.[activeSessionId]
2637
+ ? state.outbox.bySessionId[activeSessionId]
2638
+ : [];
2639
+ return {
2640
+ value: state.ui.prompt,
2641
+ cursor: state.ui.promptCursor,
2642
+ focused: state.ui.focusRegion === "prompt",
2643
+ modalOpen: Boolean(state.ui.modal),
2644
+ answerMode: Boolean(activeSession?.pendingQuestion?.question),
2645
+ canStopTurn: canStopSessionTurn(activeSession),
2646
+ hasOutbox: outbox.length > 0,
2647
+ hasPendingOutbox: outbox.some((item) => item?.phase === "pending"),
2648
+ pendingCount: outbox.filter((item) => item?.phase === "pending").length,
2649
+ editingPending: state.ui.promptEdit?.sessionId === activeSessionId,
2650
+ selectedOutboxPhase: state.ui.promptEdit?.sessionId === activeSessionId
2651
+ ? state.ui.promptEdit?.phase || null
2652
+ : null,
2653
+ };
2654
+ }, shallowEqualObject);
2655
+ const inputRef = React.useRef(null);
2656
+
2657
+ React.useEffect(() => {
2658
+ const inputNode = inputRef.current;
2659
+ if (!active || promptState.modalOpen || !promptState.focused || !inputNode) return;
2660
+ if (document.activeElement !== inputNode) {
2661
+ try {
2662
+ inputNode.focus({ preventScroll: true });
2663
+ } catch {
2664
+ inputNode.focus();
2665
+ }
2666
+ }
2667
+ inputNode.setSelectionRange(promptState.cursor, promptState.cursor);
2668
+ }, [active, promptState.cursor, promptState.focused, promptState.modalOpen]);
2669
+
2670
+ const sendPrompt = React.useCallback(() => {
2671
+ controller.handleCommand(UI_COMMANDS.SEND_PROMPT)
2672
+ .catch(() => {})
2673
+ .finally(() => {
2674
+ onAfterSend?.();
2675
+ });
2676
+ }, [controller, onAfterSend]);
2677
+
2678
+ const [stoppingTurn, setStoppingTurn] = React.useState(false);
2679
+ const stopTurn = React.useCallback(() => {
2680
+ setStoppingTurn(true);
2681
+ controller.handleCommand(UI_COMMANDS.STOP_TURN)
2682
+ .catch(() => {})
2683
+ .finally(() => setStoppingTurn(false));
2684
+ }, [controller]);
2685
+
2686
+ const cancelPending = React.useCallback(() => {
2687
+ if (controller.getState().ui.promptEdit) {
2688
+ if (typeof controller.cancelSelectedOutboxPrompt === "function") {
2689
+ controller.cancelSelectedOutboxPrompt().catch(() => {});
2690
+ } else {
2691
+ controller.cancelSelectedPendingPrompt();
2692
+ }
2693
+ return;
2694
+ }
2695
+ if (typeof controller.cancelLatestQueuedOutbox === "function") {
2696
+ controller.cancelLatestQueuedOutbox().catch(() => {});
2697
+ }
2698
+ }, [controller]);
2699
+
2700
+ const sendLabel = promptState.editingPending || (promptState.hasPendingOutbox && !promptState.value.trim())
2701
+ ? "⇪"
2702
+ : promptState.hasOutbox
2703
+ ? "+"
2704
+ : "❯";
2705
+ const selectedQueued = promptState.selectedOutboxPhase === "queued";
2706
+ const selectedCancelling = promptState.selectedOutboxPhase === "cancelling";
2707
+ const selectedReadOnly = selectedQueued || selectedCancelling;
2708
+
2709
+ return React.createElement("div", {
2710
+ className: `ps-prompt-shell${mobile ? " is-mobile" : ""}`,
2711
+ },
2712
+ React.createElement("label", { className: "ps-prompt-label" }, promptState.answerMode ? "answer" : selectedCancelling ? "cancelling" : selectedQueued ? "queued" : promptState.editingPending ? "pending" : "you"),
2713
+ React.createElement("textarea", {
2714
+ ref: inputRef,
2715
+ className: "ps-prompt-input",
2716
+ rows: mobile ? 2 : Math.max(2, getPromptInputRows(promptState.value)),
2717
+ value: promptState.value,
2718
+ readOnly: selectedReadOnly,
2719
+ placeholder: promptState.answerMode
2720
+ ? "Type an answer and press Enter"
2721
+ : promptState.editingPending
2722
+ ? selectedCancelling
2723
+ ? "Cancellation requested"
2724
+ : selectedQueued
2725
+ ? "Queued message selected"
2726
+ : "Edit the pending message, then send or cancel it"
2727
+ : promptState.hasOutbox
2728
+ ? "Type a message and press Enter to queue it behind the pending batch"
2729
+ : "Type a message and press Enter",
2730
+ enterKeyHint: "send",
2731
+ onFocus: () => controller.setFocus("prompt"),
2732
+ onSelect: (event) => controller.setPromptCursor(event.currentTarget.selectionStart || 0),
2733
+ onChange: (event) => controller.setPrompt(event.currentTarget.value, event.currentTarget.selectionStart || event.currentTarget.value.length),
2734
+ onKeyDown: (event) => {
2735
+ if (event.key === "Tab" && !event.shiftKey && controller.acceptPromptReferenceAutocomplete()) {
2736
+ event.preventDefault();
2737
+ return;
2738
+ }
2739
+ if (event.key === "ArrowUp" && !event.shiftKey && !event.metaKey && !event.altKey) {
2740
+ event.preventDefault();
2741
+ controller.movePromptCursorVertical(-1);
2742
+ return;
2743
+ }
2744
+ if (event.key === "ArrowDown" && !event.shiftKey && !event.metaKey && !event.altKey) {
2745
+ event.preventDefault();
2746
+ controller.movePromptCursorVertical(1);
2747
+ return;
2748
+ }
2749
+ if (event.key === "Escape" && promptState.editingPending) {
2750
+ event.preventDefault();
2751
+ if (selectedReadOnly) {
2752
+ controller.exitPendingPromptEdit({ restoreDraft: true });
2753
+ return;
2754
+ }
2755
+ cancelPending();
2756
+ return;
2757
+ }
2758
+ if (event.key === "Enter" && !event.shiftKey && !event.metaKey && !mobile) {
2759
+ event.preventDefault();
2760
+ sendPrompt();
2761
+ }
2762
+ },
2763
+ }),
2764
+ React.createElement("div", { className: "ps-prompt-actions" },
2765
+ promptState.editingPending && !selectedCancelling
2766
+ ? React.createElement("button", {
2767
+ type: "button",
2768
+ className: "ps-mini-button",
2769
+ title: selectedQueued ? "Delete selected queued prompt" : "Cancel selected pending prompt",
2770
+ "aria-label": selectedQueued ? "Delete selected queued prompt" : "Cancel selected pending prompt",
2771
+ onClick: cancelPending,
2772
+ }, selectedQueued ? "Delete" : "Cancel")
2773
+ : null,
2774
+ promptState.canStopTurn || stoppingTurn
2775
+ ? React.createElement("button", {
2776
+ type: "button",
2777
+ className: `ps-stop-button${stoppingTurn ? " is-stopping" : ""}`,
2778
+ title: "Stop the current turn (the session stays alive and returns to idle)",
2779
+ "aria-label": "Stop the current turn",
2780
+ disabled: stoppingTurn,
2781
+ onClick: stopTurn,
2782
+ }, "■")
2783
+ : null,
2784
+ React.createElement("button", {
2785
+ type: "button",
2786
+ className: `ps-send-button${mobile ? " is-inline" : ""}`,
2787
+ title: promptState.editingPending || (promptState.hasPendingOutbox && !promptState.value.trim())
2788
+ ? "Send all queued prompts"
2789
+ : promptState.hasOutbox
2790
+ ? "Queue prompt behind the pending batch"
2791
+ : "Send prompt",
2792
+ "aria-label": promptState.editingPending || (promptState.hasPendingOutbox && !promptState.value.trim())
2793
+ ? "Send queued prompts"
2794
+ : promptState.hasOutbox
2795
+ ? "Queue prompt"
2796
+ : "Send prompt",
2797
+ onClick: sendPrompt,
2798
+ }, sendLabel),
2799
+ ),
2800
+ );
2801
+ }
2802
+
2803
+ function StatusStrip({ controller }) {
2804
+ const status = useControllerSelector(controller, (state) => selectStatusBar(state), shallowEqualObject);
2805
+ return React.createElement("div", { className: "ps-status-strip" },
2806
+ React.createElement("div", { className: "ps-status-left" }, status.left),
2807
+ React.createElement("div", { className: "ps-status-right" }, status.right),
2808
+ );
2809
+ }
2810
+
2811
+ function Toolbar({ controller, mobile, chatFocusMode = false, onToggleChatFocus = null, chatFocusDisabled = false }) {
2812
+ const adminVisible = useControllerSelector(controller, (state) => Boolean(state.admin?.visible));
2813
+ const chatView = useControllerSelector(controller, (state) => ({
2814
+ mode: state.ui.chatViewMode || "transcript",
2815
+ activeSessionIsGroup: Boolean(state.sessions.activeSessionId && state.sessions.byId[state.sessions.activeSessionId]?.isGroup),
2816
+ hasActiveSession: Boolean(state.sessions.activeSessionId && state.sessions.byId[state.sessions.activeSessionId]),
2817
+ }), shallowEqualObject);
2818
+ const switchModelDisabled = !chatView.hasActiveSession || chatView.activeSessionIsGroup;
2819
+
2820
+ const buttonDefs = [
2821
+ {
2822
+ key: "new",
2823
+ label: "New",
2824
+ onClick: () => controller.handleCommand(UI_COMMANDS.NEW_SESSION).catch(() => {}),
2825
+ },
2826
+ {
2827
+ key: "model",
2828
+ label: mobile ? "Model" : "New + Model",
2829
+ onClick: () => controller.handleCommand(UI_COMMANDS.OPEN_MODEL_PICKER).catch(() => {}),
2830
+ },
2831
+ {
2832
+ key: "switch-model",
2833
+ label: mobile ? "Switch" : "Switch Model",
2834
+ onClick: () => controller.openSwitchModelPicker().catch((err) => {
2835
+ controller.dispatch({ type: "ui/status", text: err?.message || String(err) || "Failed to switch model" });
2836
+ }),
2837
+ disabled: switchModelDisabled,
2838
+ title: switchModelDisabled ? "Select a session to switch its model" : "Switch the selected session model at the next turn boundary",
2839
+ },
2840
+ {
2841
+ key: "filter",
2842
+ label: "Filter",
2843
+ onClick: () => controller.handleCommand(UI_COMMANDS.OPEN_SESSION_FILTER).catch(() => {}),
2844
+ },
2845
+ {
2846
+ key: "theme",
2847
+ label: "Theme",
2848
+ onClick: () => controller.handleCommand(UI_COMMANDS.OPEN_THEME_PICKER).catch(() => {}),
2849
+ },
2850
+ {
2851
+ key: "summary",
2852
+ label: chatView.mode === "summary" ? "Chat" : "Summary",
2853
+ onClick: () => controller.setChatViewMode(chatView.mode === "summary" ? "transcript" : "summary"),
2854
+ disabled: chatView.activeSessionIsGroup,
2855
+ title: chatView.activeSessionIsGroup ? "Groups show group details" : "Toggle Chat / Summary view",
2856
+ active: chatView.mode === "summary",
2857
+ },
2858
+ ...(onToggleChatFocus ? [{
2859
+ key: "focus",
2860
+ label: mobile
2861
+ ? (chatFocusMode ? "Exit Focus" : "Focus")
2862
+ : (chatFocusMode ? "Exit Focus" : "Chat Focus"),
2863
+ onClick: onToggleChatFocus,
2864
+ disabled: chatFocusDisabled,
2865
+ active: chatFocusMode,
2866
+ }] : []),
2867
+ {
2868
+ key: "admin",
2869
+ label: adminVisible ? "Close Admin" : "Admin",
2870
+ onClick: () => controller.handleCommand(adminVisible ? UI_COMMANDS.CLOSE_ADMIN_CONSOLE : UI_COMMANDS.OPEN_ADMIN_CONSOLE).catch(() => {}),
2871
+ active: adminVisible,
2872
+ },
2873
+ ];
2874
+
2875
+ const renderButton = (def) => React.createElement("button", {
2876
+ key: def.key,
2877
+ type: "button",
2878
+ className: `ps-toolbar-button${def.active ? " is-active" : ""}`,
2879
+ onClick: def.onClick,
2880
+ disabled: Boolean(def.disabled),
2881
+ title: def.title,
2882
+ }, def.label);
2883
+
2884
+ if (mobile) {
2885
+ const firstRowButtons = buttonDefs.slice(0, 4);
2886
+ const secondRowButtons = buttonDefs.slice(4);
2887
+
2888
+ return React.createElement("div", { className: "ps-toolbar is-mobile" },
2889
+ React.createElement("div", { className: "ps-toolbar-row ps-toolbar-row-primary" },
2890
+ firstRowButtons.map(renderButton)),
2891
+ React.createElement("div", { className: "ps-toolbar-row ps-toolbar-row-secondary" },
2892
+ React.createElement("div", { className: "ps-toolbar-row-actions" }, secondRowButtons.map(renderButton))),
2893
+ );
2894
+ }
2895
+
2896
+ return React.createElement("div", { className: "ps-toolbar" },
2897
+ React.createElement("div", { className: "ps-toolbar-actions" }, buttonDefs.map(renderButton)),
2898
+ );
2899
+ }
2900
+
2901
+ function ColumnResizeHandle({ controller, paneAdjust = 0 }) {
2902
+ const dragStateRef = React.useRef(null);
2903
+ const [dragging, setDragging] = React.useState(false);
2904
+
2905
+ React.useEffect(() => {
2906
+ if (!dragging) return undefined;
2907
+
2908
+ const stopDragging = () => {
2909
+ dragStateRef.current = null;
2910
+ setDragging(false);
2911
+ document.body.classList.remove("is-resizing-pane-x");
2912
+ };
2913
+
2914
+ const onPointerMove = (event) => {
2915
+ const dragState = dragStateRef.current;
2916
+ if (!dragState) return;
2917
+ const deltaCells = Math.round((event.clientX - dragState.startX) / GRID_CELL_WIDTH);
2918
+ const deltaIncrement = deltaCells - dragState.appliedCells;
2919
+ if (!deltaIncrement) return;
2920
+ controller.adjustPaneSplit(deltaIncrement);
2921
+ dragState.appliedCells = deltaCells;
2922
+ };
2923
+
2924
+ window.addEventListener("pointermove", onPointerMove);
2925
+ window.addEventListener("pointerup", stopDragging);
2926
+ window.addEventListener("pointercancel", stopDragging);
2927
+
2928
+ return () => {
2929
+ window.removeEventListener("pointermove", onPointerMove);
2930
+ window.removeEventListener("pointerup", stopDragging);
2931
+ window.removeEventListener("pointercancel", stopDragging);
2932
+ document.body.classList.remove("is-resizing-pane-x");
2933
+ };
2934
+ }, [controller, dragging]);
2935
+
2936
+ return React.createElement("button", {
2937
+ type: "button",
2938
+ className: `ps-column-resizer${dragging ? " is-dragging" : ""}`,
2939
+ title: "Drag to resize the inspector column. Double-click to reset.",
2940
+ "aria-label": "Resize inspector column",
2941
+ onPointerDown: (event) => {
2942
+ if (event.button !== 0) return;
2943
+ event.preventDefault();
2944
+ dragStateRef.current = {
2945
+ startX: event.clientX,
2946
+ appliedCells: 0,
2947
+ };
2948
+ setDragging(true);
2949
+ document.body.classList.add("is-resizing-pane-x");
2950
+ },
2951
+ onDoubleClick: () => {
2952
+ if (!paneAdjust) return;
2953
+ controller.adjustPaneSplit(-paneAdjust);
2954
+ },
2955
+ onKeyDown: (event) => {
2956
+ if (event.key === "ArrowLeft") {
2957
+ event.preventDefault();
2958
+ controller.handleCommand(UI_COMMANDS.GROW_RIGHT_PANE).catch(() => {});
2959
+ return;
2960
+ }
2961
+ if (event.key === "ArrowRight") {
2962
+ event.preventDefault();
2963
+ controller.handleCommand(UI_COMMANDS.GROW_LEFT_PANE).catch(() => {});
2964
+ }
2965
+ },
2966
+ },
2967
+ React.createElement("span", { className: "ps-column-resizer-handle", "aria-hidden": "true" },
2968
+ React.createElement("span", { className: "ps-column-resizer-dot" }),
2969
+ React.createElement("span", { className: "ps-column-resizer-dot" }),
2970
+ React.createElement("span", { className: "ps-column-resizer-dot" })));
2971
+ }
2972
+
2973
+ function RowResizeHandle({ controller, sessionPaneAdjust = 0 }) {
2974
+ const dragStateRef = React.useRef(null);
2975
+ const [dragging, setDragging] = React.useState(false);
2976
+
2977
+ React.useEffect(() => {
2978
+ if (!dragging) return undefined;
2979
+
2980
+ const stopDragging = () => {
2981
+ dragStateRef.current = null;
2982
+ setDragging(false);
2983
+ document.body.classList.remove("is-resizing-pane-y");
2984
+ };
2985
+
2986
+ const onPointerMove = (event) => {
2987
+ const dragState = dragStateRef.current;
2988
+ if (!dragState) return;
2989
+ const deltaCells = Math.round((event.clientY - dragState.startY) / GRID_CELL_HEIGHT);
2990
+ const deltaIncrement = deltaCells - dragState.appliedCells;
2991
+ if (!deltaIncrement) return;
2992
+ controller.adjustSessionPaneSplit(deltaIncrement);
2993
+ dragState.appliedCells = deltaCells;
2994
+ };
2995
+
2996
+ window.addEventListener("pointermove", onPointerMove);
2997
+ window.addEventListener("pointerup", stopDragging);
2998
+ window.addEventListener("pointercancel", stopDragging);
2999
+
3000
+ return () => {
3001
+ window.removeEventListener("pointermove", onPointerMove);
3002
+ window.removeEventListener("pointerup", stopDragging);
3003
+ window.removeEventListener("pointercancel", stopDragging);
3004
+ document.body.classList.remove("is-resizing-pane-y");
3005
+ };
3006
+ }, [controller, dragging]);
3007
+
3008
+ return React.createElement("button", {
3009
+ type: "button",
3010
+ className: `ps-row-resizer${dragging ? " is-dragging" : ""}`,
3011
+ title: "Drag to resize the sessions and chat panes. Double-click to reset.",
3012
+ "aria-label": "Resize sessions and chat panes",
3013
+ onPointerDown: (event) => {
3014
+ if (event.button !== 0) return;
3015
+ event.preventDefault();
3016
+ dragStateRef.current = {
3017
+ startY: event.clientY,
3018
+ appliedCells: 0,
3019
+ };
3020
+ setDragging(true);
3021
+ document.body.classList.add("is-resizing-pane-y");
3022
+ },
3023
+ onDoubleClick: () => {
3024
+ if (!sessionPaneAdjust) return;
3025
+ controller.adjustSessionPaneSplit(-sessionPaneAdjust);
3026
+ },
3027
+ onKeyDown: (event) => {
3028
+ if (event.key === "ArrowUp") {
3029
+ event.preventDefault();
3030
+ controller.adjustSessionPaneSplit(-1);
3031
+ return;
3032
+ }
3033
+ if (event.key === "ArrowDown") {
3034
+ event.preventDefault();
3035
+ controller.adjustSessionPaneSplit(1);
3036
+ }
3037
+ },
3038
+ },
3039
+ React.createElement("span", { className: "ps-row-resizer-handle", "aria-hidden": "true" },
3040
+ React.createElement("span", { className: "ps-row-resizer-dot" }),
3041
+ React.createElement("span", { className: "ps-row-resizer-dot" }),
3042
+ React.createElement("span", { className: "ps-row-resizer-dot" })));
3043
+ }
3044
+
3045
+ function SessionColumnResizeHandle({ controller, portalSessionColumnAdjust = 0 }) {
3046
+ const dragStateRef = React.useRef(null);
3047
+ const [dragging, setDragging] = React.useState(false);
3048
+
3049
+ React.useEffect(() => {
3050
+ if (!dragging) return undefined;
3051
+
3052
+ const stopDragging = () => {
3053
+ dragStateRef.current = null;
3054
+ setDragging(false);
3055
+ document.body.classList.remove("is-resizing-pane-x");
3056
+ };
3057
+
3058
+ const onPointerMove = (event) => {
3059
+ const dragState = dragStateRef.current;
3060
+ if (!dragState) return;
3061
+ const nextAdjust = clampNumber(
3062
+ dragState.startAdjust + (event.clientX - dragState.startX),
3063
+ dragState.minAdjust,
3064
+ dragState.maxAdjust,
3065
+ );
3066
+ controller.dispatch({
3067
+ type: "ui/portalSessionColumnAdjust",
3068
+ portalSessionColumnAdjust: nextAdjust,
3069
+ });
3070
+ };
3071
+
3072
+ window.addEventListener("pointermove", onPointerMove);
3073
+ window.addEventListener("pointerup", stopDragging);
3074
+ window.addEventListener("pointercancel", stopDragging);
3075
+
3076
+ return () => {
3077
+ window.removeEventListener("pointermove", onPointerMove);
3078
+ window.removeEventListener("pointerup", stopDragging);
3079
+ window.removeEventListener("pointercancel", stopDragging);
3080
+ document.body.classList.remove("is-resizing-pane-x");
3081
+ };
3082
+ }, [controller, dragging]);
3083
+
3084
+ return React.createElement("button", {
3085
+ type: "button",
3086
+ className: `ps-column-resizer${dragging ? " is-dragging" : ""}`,
3087
+ title: "Drag to resize the sessions and chat columns. Double-click to reset.",
3088
+ "aria-label": "Resize sessions and chat columns",
3089
+ onPointerDown: (event) => {
3090
+ if (event.button !== 0) return;
3091
+ event.preventDefault();
3092
+ const gridNode = event.currentTarget.closest(".ps-workspace-main-grid");
3093
+ const bounds = portalSessionColumnBounds(gridNode?.getBoundingClientRect?.().width);
3094
+ dragStateRef.current = {
3095
+ startX: event.clientX,
3096
+ startAdjust: Number(portalSessionColumnAdjust) || 0,
3097
+ minAdjust: bounds.minAdjust,
3098
+ maxAdjust: bounds.maxAdjust,
3099
+ };
3100
+ setDragging(true);
3101
+ document.body.classList.add("is-resizing-pane-x");
3102
+ },
3103
+ onDoubleClick: () => {
3104
+ controller.dispatch({ type: "ui/portalSessionColumnAdjust", portalSessionColumnAdjust: 0 });
3105
+ },
3106
+ onKeyDown: (event) => {
3107
+ const gridNode = event.currentTarget.closest(".ps-workspace-main-grid");
3108
+ const bounds = portalSessionColumnBounds(gridNode?.getBoundingClientRect?.().width);
3109
+ const adjustBy = (delta) => {
3110
+ const nextAdjust = clampNumber(
3111
+ (Number(portalSessionColumnAdjust) || 0) + delta,
3112
+ bounds.minAdjust,
3113
+ bounds.maxAdjust,
3114
+ );
3115
+ controller.dispatch({
3116
+ type: "ui/portalSessionColumnAdjust",
3117
+ portalSessionColumnAdjust: nextAdjust,
3118
+ });
3119
+ };
3120
+ if (event.key === "ArrowLeft") {
3121
+ event.preventDefault();
3122
+ adjustBy(-24);
3123
+ return;
3124
+ }
3125
+ if (event.key === "ArrowRight") {
3126
+ event.preventDefault();
3127
+ adjustBy(24);
3128
+ }
3129
+ },
3130
+ },
3131
+ React.createElement("span", { className: "ps-column-resizer-handle", "aria-hidden": "true" },
3132
+ React.createElement("span", { className: "ps-column-resizer-dot" }),
3133
+ React.createElement("span", { className: "ps-column-resizer-dot" }),
3134
+ React.createElement("span", { className: "ps-column-resizer-dot" })));
3135
+ }
3136
+
3137
+ function ActivityRowResizeHandle({ controller, activityPaneAdjust = 0 }) {
3138
+ const dragStateRef = React.useRef(null);
3139
+ const [dragging, setDragging] = React.useState(false);
3140
+
3141
+ React.useEffect(() => {
3142
+ if (!dragging) return undefined;
3143
+
3144
+ const stopDragging = () => {
3145
+ dragStateRef.current = null;
3146
+ setDragging(false);
3147
+ document.body.classList.remove("is-resizing-pane-y");
3148
+ };
3149
+
3150
+ const onPointerMove = (event) => {
3151
+ const dragState = dragStateRef.current;
3152
+ if (!dragState) return;
3153
+ const deltaCells = Math.round((event.clientY - dragState.startY) / GRID_CELL_HEIGHT);
3154
+ const deltaIncrement = deltaCells - dragState.appliedCells;
3155
+ if (!deltaIncrement) return;
3156
+ controller.adjustActivityPaneSplit(-deltaIncrement);
3157
+ dragState.appliedCells = deltaCells;
3158
+ };
3159
+
3160
+ window.addEventListener("pointermove", onPointerMove);
3161
+ window.addEventListener("pointerup", stopDragging);
3162
+ window.addEventListener("pointercancel", stopDragging);
3163
+
3164
+ return () => {
3165
+ window.removeEventListener("pointermove", onPointerMove);
3166
+ window.removeEventListener("pointerup", stopDragging);
3167
+ window.removeEventListener("pointercancel", stopDragging);
3168
+ document.body.classList.remove("is-resizing-pane-y");
3169
+ };
3170
+ }, [controller, dragging]);
3171
+
3172
+ return React.createElement("button", {
3173
+ type: "button",
3174
+ className: `ps-row-resizer${dragging ? " is-dragging" : ""}`,
3175
+ title: "Drag to resize the activity pane. Double-click to reset.",
3176
+ "aria-label": "Resize activity pane",
3177
+ onPointerDown: (event) => {
3178
+ if (event.button !== 0) return;
3179
+ event.preventDefault();
3180
+ dragStateRef.current = {
3181
+ startY: event.clientY,
3182
+ appliedCells: 0,
3183
+ };
3184
+ setDragging(true);
3185
+ document.body.classList.add("is-resizing-pane-y");
3186
+ },
3187
+ onDoubleClick: () => {
3188
+ if (!activityPaneAdjust) return;
3189
+ controller.adjustActivityPaneSplit(-activityPaneAdjust);
3190
+ },
3191
+ onKeyDown: (event) => {
3192
+ if (event.key === "ArrowUp") {
3193
+ event.preventDefault();
3194
+ controller.adjustActivityPaneSplit(1);
3195
+ return;
3196
+ }
3197
+ if (event.key === "ArrowDown") {
3198
+ event.preventDefault();
3199
+ controller.adjustActivityPaneSplit(-1);
3200
+ }
3201
+ },
3202
+ },
3203
+ React.createElement("span", { className: "ps-row-resizer-handle", "aria-hidden": "true" },
3204
+ React.createElement("span", { className: "ps-row-resizer-dot" }),
3205
+ React.createElement("span", { className: "ps-row-resizer-dot" }),
3206
+ React.createElement("span", { className: "ps-row-resizer-dot" })));
3207
+ }
3208
+
3209
+ function MobileNav({ activePane, setActivePane, controller }) {
3210
+ const tabs = [
3211
+ { id: "workspace", label: "Main", focus: "chat" },
3212
+ { id: "inspector", label: "Inspector", focus: "inspector" },
3213
+ { id: "activity", label: "Activity", focus: "activity" },
3214
+ ];
3215
+ return React.createElement("div", { className: "ps-mobile-nav" },
3216
+ tabs.map((tab) => React.createElement("button", {
3217
+ key: tab.id,
3218
+ type: "button",
3219
+ className: `ps-mobile-nav-button${activePane === tab.id ? " is-active" : ""}`,
3220
+ onClick: () => {
3221
+ setActivePane(tab.id);
3222
+ controller.setFocus(tab.focus);
3223
+ },
3224
+ }, tab.label)));
3225
+ }
3226
+
3227
+ function ModalLayer({ controller }) {
3228
+ const themeId = useControllerSelector(controller, (state) => state.ui.themeId);
3229
+ const theme = getTheme(themeId);
3230
+ const modalState = useControllerSelector(controller, (state) => ({
3231
+ rawModal: state.ui.modal,
3232
+ themePicker: selectThemePickerModal(state),
3233
+ modelPicker: selectModelPickerModal(state),
3234
+ reasoningEffortPicker: selectReasoningEffortPickerModal(state),
3235
+ sessionAgentPicker: selectSessionAgentPickerModal(state),
3236
+ sessionGroupPicker: selectSessionGroupPickerModal(state),
3237
+ sessionGroupName: selectSessionGroupNameModal(state),
3238
+ artifactPicker: selectArtifactPickerModal(state),
3239
+ logFilter: selectLogFilterModal(state),
3240
+ filesFilter: selectFilesFilterModal(state),
3241
+ historyFormat: selectHistoryFormatModal(state),
3242
+ renameSession: selectRenameSessionModal(state),
3243
+ sessionOwnerFilter: selectSessionOwnerFilterModal(state),
3244
+ artifactUpload: selectArtifactUploadModal(state),
3245
+ confirm: selectConfirmModal(state),
3246
+ logsFilter: state.logs.filter,
3247
+ filesFilterState: state.files.filter,
3248
+ historyFormatState: state.executionHistory?.format || "pretty",
3249
+ }), shallowEqualObject);
3250
+ const modal = modalState.rawModal;
3251
+ const renameInputRef = React.useRef(null);
3252
+ const groupNameInputRef = React.useRef(null);
3253
+ const listModalRef = React.useRef(null);
3254
+
3255
+ React.useEffect(() => {
3256
+ if (modal?.type !== "renameSession" || !modalState.renameSession) return;
3257
+ const inputNode = renameInputRef.current;
3258
+ if (!inputNode) return;
3259
+ if (document.activeElement !== inputNode) {
3260
+ try {
3261
+ inputNode.focus({ preventScroll: true });
3262
+ } catch {
3263
+ inputNode.focus();
3264
+ }
3265
+ }
3266
+ inputNode.setSelectionRange(modalState.renameSession.cursorIndex, modalState.renameSession.cursorIndex);
3267
+ }, [modal?.type, modalState.renameSession?.cursorIndex, modalState.renameSession?.value]);
3268
+
3269
+ React.useEffect(() => {
3270
+ if (modal?.type !== "sessionGroupName" || !modalState.sessionGroupName) return;
3271
+ const inputNode = groupNameInputRef.current;
3272
+ if (!inputNode) return;
3273
+ if (document.activeElement !== inputNode) {
3274
+ try {
3275
+ inputNode.focus({ preventScroll: true });
3276
+ } catch {
3277
+ inputNode.focus();
3278
+ }
3279
+ }
3280
+ inputNode.setSelectionRange(modalState.sessionGroupName.cursorIndex, modalState.sessionGroupName.cursorIndex);
3281
+ }, [modal?.type, modalState.sessionGroupName?.cursorIndex, modalState.sessionGroupName?.value]);
3282
+
3283
+ React.useEffect(() => {
3284
+ if (!modal) return;
3285
+ if (![
3286
+ "themePicker",
3287
+ "modelPicker",
3288
+ "reasoningEffortPicker",
3289
+ "sessionAgentPicker",
3290
+ "sessionGroupPicker",
3291
+ "artifactPicker",
3292
+ "sessionOwnerFilter",
3293
+ "logFilter",
3294
+ "filesFilter",
3295
+ "historyFormat",
3296
+ ].includes(modal.type)) {
3297
+ return;
3298
+ }
3299
+
3300
+ const listNode = listModalRef.current;
3301
+ if (!listNode) return;
3302
+ const selected = listNode.querySelector(".ps-list-button.is-selected");
3303
+ if (selected && typeof selected.scrollIntoView === "function") {
3304
+ selected.scrollIntoView({ block: "nearest" });
3305
+ }
3306
+ }, [
3307
+ modal?.type,
3308
+ modal?.selectedIndex,
3309
+ modalState.themePicker?.selectedRowIndex,
3310
+ modalState.modelPicker?.selectedRowIndex,
3311
+ modalState.reasoningEffortPicker?.selectedRowIndex,
3312
+ modalState.sessionAgentPicker?.selectedRowIndex,
3313
+ modalState.sessionGroupPicker?.selectedRowIndex,
3314
+ modalState.artifactPicker?.selectedRowIndex,
3315
+ modalState.sessionOwnerFilter?.selectedRowIndex,
3316
+ modalState.logFilter?.selectedRowIndex,
3317
+ modalState.filesFilter?.selectedRowIndex,
3318
+ modalState.historyFormat?.selectedRowIndex,
3319
+ ]);
3320
+
3321
+ if (!modal) return null;
3322
+
3323
+ const close = () => controller.handleCommand(UI_COMMANDS.CLOSE_MODAL).catch(() => {});
3324
+
3325
+ const renderListModal = (presentation, confirmLabel = "Apply") => {
3326
+ const rows = Array.isArray(presentation.rows) ? presentation.rows : [];
3327
+ const rowItemIndexes = Array.isArray(presentation.rowItemIndexes) ? presentation.rowItemIndexes : null;
3328
+ const usesHangingIndent = modal.type === "modelPicker" || modal.type === "reasoningEffortPicker" || modal.type === "sessionAgentPicker";
3329
+ const renderedList = rowItemIndexes && rowItemIndexes.length === rows.length
3330
+ ? rows.map((row, rowIndex) => {
3331
+ const itemIndex = rowItemIndexes[rowIndex];
3332
+ const runs = Array.isArray(row)
3333
+ ? row
3334
+ : normalizeLines([row])[0]?.runs || [{ text: row?.text || "", color: row?.color }];
3335
+ if (itemIndex == null || itemIndex < 0) {
3336
+ return React.createElement("div", {
3337
+ key: `row:${rowIndex}`,
3338
+ className: "ps-line ps-modal-list-heading-line",
3339
+ }, React.createElement(Runs, { runs, theme }));
3340
+ }
3341
+ const item = modal.items?.[itemIndex];
3342
+ return React.createElement("button", {
3343
+ key: item?.id || `row:${rowIndex}`,
3344
+ type: "button",
3345
+ className: `ps-list-button ps-modal-list-button${itemIndex === modal.selectedIndex ? " is-selected" : ""}${usesHangingIndent ? " is-hanging" : ""}`,
3346
+ onClick: () => controller.dispatch({ type: "ui/modalSelection", index: itemIndex }),
3347
+ },
3348
+ React.createElement("div", { className: "ps-line ps-modal-list-line" },
3349
+ React.createElement(Runs, { runs, theme })));
3350
+ })
3351
+ : (modal.items || []).map((item, index) => React.createElement("button", {
3352
+ key: item.id || index,
3353
+ type: "button",
3354
+ className: `ps-list-button ps-modal-list-button${index === modal.selectedIndex ? " is-selected" : ""}${usesHangingIndent ? " is-hanging" : ""}`,
3355
+ onClick: () => controller.dispatch({ type: "ui/modalSelection", index }),
3356
+ },
3357
+ React.createElement("div", { className: "ps-line ps-modal-list-line" },
3358
+ React.createElement(Runs, {
3359
+ runs: Array.isArray(rows?.[index])
3360
+ ? rows[index]
3361
+ : normalizeLines([rows?.[index]])[0]?.runs || [{ text: rows?.[index]?.text || "", color: rows?.[index]?.color }],
3362
+ theme,
3363
+ }))));
3364
+
3365
+ return React.createElement("div", { className: "ps-modal-backdrop", onClick: close },
3366
+ React.createElement("div", { className: `ps-modal${modal.type === "themePicker" ? " is-theme-picker" : ""}`, onClick: (event) => event.stopPropagation() },
3367
+ React.createElement("div", { className: "ps-modal-header" },
3368
+ React.createElement("div", { className: "ps-modal-title" }, presentation.title),
3369
+ React.createElement("button", { type: "button", className: "ps-modal-close", onClick: close }, "Close"),
3370
+ ),
3371
+ React.createElement("div", { className: "ps-modal-grid" },
3372
+ React.createElement("div", { ref: listModalRef, className: "ps-modal-list" },
3373
+ renderedList,
3374
+ ),
3375
+ React.createElement("div", { className: "ps-modal-details" },
3376
+ React.createElement("div", { className: "ps-modal-details-title" }, presentation.detailsTitle || "Details"),
3377
+ normalizeLines(presentation.detailsLines || []).map((line, index) => React.createElement(Line, { key: `detail:${index}`, line, theme, className: "ps-modal-detail-line" })),
3378
+ ),
3379
+ ),
3380
+ React.createElement("div", { className: "ps-modal-footer" },
3381
+ React.createElement("button", { type: "button", className: "ps-modal-button", onClick: close }, "Cancel"),
3382
+ React.createElement("button", {
3383
+ type: "button",
3384
+ className: "ps-modal-button is-primary",
3385
+ onClick: () => controller.handleCommand(UI_COMMANDS.MODAL_CONFIRM).catch(() => {}),
3386
+ }, confirmLabel)),
3387
+ ));
3388
+ };
3389
+
3390
+ if (modal.type === "confirm" && modalState.confirm) {
3391
+ const isAlert = Boolean(modal.alert);
3392
+ const isDestructive = !isAlert && modal.action === "deleteSession";
3393
+ return React.createElement("div", { className: "ps-modal-backdrop", onClick: close },
3394
+ React.createElement("div", { className: "ps-modal is-narrow", onClick: (event) => event.stopPropagation() },
3395
+ React.createElement("div", { className: "ps-modal-header" },
3396
+ React.createElement("div", { className: "ps-modal-title" }, modalState.confirm.title),
3397
+ React.createElement("button", { type: "button", className: "ps-modal-close", onClick: close }, "Close"),
3398
+ ),
3399
+ React.createElement("div", { className: "ps-modal-body", style: { padding: "16px 20px" } },
3400
+ React.createElement("p", { style: { color: "#94a3b8", margin: 0 } }, modalState.confirm.message),
3401
+ ),
3402
+ React.createElement("div", { className: "ps-modal-footer" },
3403
+ isAlert ? null : React.createElement("button", { type: "button", className: "ps-modal-button", onClick: close }, "Cancel"),
3404
+ React.createElement("button", {
3405
+ type: "button",
3406
+ className: `ps-modal-button ${isDestructive ? "is-danger" : "is-primary"}`,
3407
+ onClick: () => controller.handleCommand(UI_COMMANDS.MODAL_CONFIRM).catch(() => {}),
3408
+ }, modalState.confirm.confirmLabel)),
3409
+ ));
3410
+ }
3411
+ if (modal.type === "themePicker" && modalState.themePicker) {
3412
+ return renderListModal(modalState.themePicker, "Apply Theme");
3413
+ }
3414
+ // In switch-model mode the model/reasoning pickers retarget an existing
3415
+ // session, so the confirm action is "Switch Model", not "Create Session".
3416
+ const pickerConfirmLabel = modal.sessionOptions?.mode === "switchModel" ? "Switch Model" : "Create Session";
3417
+ if (modal.type === "modelPicker" && modalState.modelPicker) {
3418
+ return renderListModal(modalState.modelPicker, pickerConfirmLabel);
3419
+ }
3420
+ if (modal.type === "reasoningEffortPicker" && modalState.reasoningEffortPicker) {
3421
+ return renderListModal(modalState.reasoningEffortPicker, pickerConfirmLabel);
3422
+ }
3423
+ if (modal.type === "sessionAgentPicker" && modalState.sessionAgentPicker) {
3424
+ return renderListModal(modalState.sessionAgentPicker, "Create Session");
3425
+ }
3426
+ if (modal.type === "sessionGroupPicker" && modalState.sessionGroupPicker) {
3427
+ return renderListModal(modalState.sessionGroupPicker, "Move");
3428
+ }
3429
+ if (modal.type === "artifactPicker" && modalState.artifactPicker) {
3430
+ return renderListModal(modalState.artifactPicker, modalState.artifactPicker.confirmLabel || "Open / Download");
3431
+ }
3432
+ if (modal.type === "sessionOwnerFilter" && modalState.sessionOwnerFilter) {
3433
+ const presentation = modalState.sessionOwnerFilter;
3434
+ const rows = Array.isArray(presentation.rows) ? presentation.rows : [];
3435
+ return React.createElement("div", { className: "ps-modal-backdrop", onClick: close },
3436
+ React.createElement("div", { className: "ps-modal", onClick: (event) => event.stopPropagation() },
3437
+ React.createElement("div", { className: "ps-modal-header" },
3438
+ React.createElement("div", { className: "ps-modal-title" }, presentation.title),
3439
+ React.createElement("button", { type: "button", className: "ps-modal-close", onClick: close }, "Close"),
3440
+ ),
3441
+ React.createElement("div", { className: "ps-modal-grid" },
3442
+ React.createElement("div", { ref: listModalRef, className: "ps-modal-list" },
3443
+ (modal.items || []).map((item, index) => React.createElement("button", {
3444
+ key: item.id || index,
3445
+ type: "button",
3446
+ className: `ps-list-button ps-modal-list-button${index === modal.selectedIndex ? " is-selected" : ""}`,
3447
+ onClick: () => controller.toggleSessionOwnerFilter(index),
3448
+ },
3449
+ React.createElement("div", { className: "ps-line ps-modal-list-line" },
3450
+ React.createElement(Runs, {
3451
+ runs: Array.isArray(rows?.[index])
3452
+ ? rows[index]
3453
+ : normalizeLines([rows?.[index]])[0]?.runs || [{ text: rows?.[index]?.text || "", color: rows?.[index]?.color }],
3454
+ theme,
3455
+ })))),
3456
+ ),
3457
+ React.createElement("div", { className: "ps-modal-details" },
3458
+ React.createElement("div", { className: "ps-modal-details-title" }, presentation.detailsTitle || "Details"),
3459
+ normalizeLines(presentation.detailsLines || []).map((line, index) => React.createElement(Line, { key: `detail:${index}`, line, theme, className: "ps-modal-detail-line" })),
3460
+ ),
3461
+ ),
3462
+ React.createElement("div", { className: "ps-modal-footer" },
3463
+ React.createElement("button", { type: "button", className: "ps-modal-button is-primary", onClick: close }, "Done")),
3464
+ ));
3465
+ }
3466
+ if (modal.type === "renameSession" && modalState.renameSession) {
3467
+ return React.createElement("div", { className: "ps-modal-backdrop", onClick: close },
3468
+ React.createElement("div", { className: "ps-modal is-narrow", onClick: (event) => event.stopPropagation() },
3469
+ React.createElement("div", { className: "ps-modal-header" },
3470
+ React.createElement("div", { className: "ps-modal-title" }, modalState.renameSession.title),
3471
+ React.createElement("button", { type: "button", className: "ps-modal-close", onClick: close }, "Close"),
3472
+ ),
3473
+ React.createElement("input", {
3474
+ ref: renameInputRef,
3475
+ className: "ps-modal-input",
3476
+ value: modalState.renameSession.value,
3477
+ placeholder: modalState.renameSession.placeholder,
3478
+ onChange: (event) => controller.setRenameSessionValue(event.currentTarget.value, event.currentTarget.selectionStart ?? event.currentTarget.value.length),
3479
+ onKeyDown: (event) => {
3480
+ if (event.key === "Enter") {
3481
+ event.preventDefault();
3482
+ controller.handleCommand(UI_COMMANDS.MODAL_CONFIRM).catch(() => {});
3483
+ }
3484
+ },
3485
+ autoFocus: true,
3486
+ }),
3487
+ React.createElement("div", { className: "ps-modal-details" },
3488
+ normalizeLines(modalState.renameSession.helpLines || []).map((line, index) => React.createElement(Line, { key: `help:${index}`, line, theme, className: "ps-modal-detail-line" })),
3489
+ ),
3490
+ React.createElement("div", { className: "ps-modal-footer" },
3491
+ React.createElement("button", { type: "button", className: "ps-modal-button", onClick: close }, "Cancel"),
3492
+ React.createElement("button", {
3493
+ type: "button",
3494
+ className: "ps-modal-button is-primary",
3495
+ onClick: () => controller.handleCommand(UI_COMMANDS.MODAL_CONFIRM).catch(() => {}),
3496
+ }, "Save")),
3497
+ ));
3498
+ }
3499
+ if (modal.type === "sessionGroupName" && modalState.sessionGroupName) {
3500
+ return React.createElement("div", { className: "ps-modal-backdrop", onClick: close },
3501
+ React.createElement("div", { className: "ps-modal is-narrow", onClick: (event) => event.stopPropagation() },
3502
+ React.createElement("div", { className: "ps-modal-header" },
3503
+ React.createElement("div", { className: "ps-modal-title" }, modalState.sessionGroupName.title),
3504
+ React.createElement("button", { type: "button", className: "ps-modal-close", onClick: close }, "Close"),
3505
+ ),
3506
+ React.createElement("input", {
3507
+ ref: groupNameInputRef,
3508
+ className: "ps-modal-input",
3509
+ value: modalState.sessionGroupName.value,
3510
+ placeholder: modalState.sessionGroupName.placeholder,
3511
+ onChange: (event) => controller.setSessionGroupNameValue(event.currentTarget.value, event.currentTarget.selectionStart ?? event.currentTarget.value.length),
3512
+ onKeyDown: (event) => {
3513
+ if (event.key === "Enter") {
3514
+ event.preventDefault();
3515
+ controller.handleCommand(UI_COMMANDS.MODAL_CONFIRM).catch(() => {});
3516
+ }
3517
+ },
3518
+ autoFocus: true,
3519
+ }),
3520
+ React.createElement("div", { className: "ps-modal-details" },
3521
+ normalizeLines(modalState.sessionGroupName.helpLines || []).map((line, index) => React.createElement(Line, { key: `help:${index}`, line, theme, className: "ps-modal-detail-line" })),
3522
+ ),
3523
+ React.createElement("div", { className: "ps-modal-footer" },
3524
+ React.createElement("button", { type: "button", className: "ps-modal-button", onClick: close }, "Cancel"),
3525
+ React.createElement("button", {
3526
+ type: "button",
3527
+ className: "ps-modal-button is-primary",
3528
+ onClick: () => controller.handleCommand(UI_COMMANDS.MODAL_CONFIRM).catch(() => {}),
3529
+ }, "Create and Move")),
3530
+ ));
3531
+ }
3532
+ if (modal.type === "terminatePicker") {
3533
+ const isBulk = Number(modal.bulkCount) > 1;
3534
+ const isSystemRestart = Boolean(modal.systemRestart);
3535
+ const sessionLabel = String(modal.sessionTitle || "").trim() || "this session";
3536
+ const bulkCount = Number(modal.bulkCount) || 0;
3537
+ const bodyText = isBulk
3538
+ ? `What should happen to the ${bulkCount} selected sessions? Choose an action; you'll be asked to confirm.`
3539
+ : isSystemRestart
3540
+ ? `How should "${sessionLabel}" be restarted? Choose a disposition; you'll be asked to confirm.`
3541
+ : `What should happen to "${sessionLabel}"? Choose an action; you'll be asked to confirm.`;
3542
+ const pick = (action) => () => {
3543
+ controller.pickTerminateAction(action).catch(() => {});
3544
+ };
3545
+ return React.createElement("div", { className: "ps-modal-backdrop", onClick: close },
3546
+ React.createElement("div", { className: "ps-modal is-narrow", onClick: (event) => event.stopPropagation() },
3547
+ React.createElement("div", { className: "ps-modal-header" },
3548
+ React.createElement("div", { className: "ps-modal-title" }, modal.title || "Terminate session"),
3549
+ React.createElement("button", { type: "button", className: "ps-modal-close", onClick: close }, "Close"),
3550
+ ),
3551
+ React.createElement("div", { className: "ps-modal-body", style: { padding: "12px 16px 4px" } },
3552
+ React.createElement("p", { style: { color: "#94a3b8", margin: 0 } }, bodyText)),
3553
+ React.createElement("div", {
3554
+ className: "ps-modal-body",
3555
+ style: { padding: "10px 16px 16px", display: "flex", flexDirection: "column", gap: 8 },
3556
+ },
3557
+ React.createElement("button", {
3558
+ type: "button",
3559
+ className: "ps-modal-button is-primary",
3560
+ style: { width: "100%", justifyContent: "flex-start" },
3561
+ onClick: pick("complete"),
3562
+ }, isBulk ? `Mark ${bulkCount} Completed` : isSystemRestart ? "Complete & Restart" : "Mark Completed"),
3563
+ React.createElement("button", {
3564
+ type: "button",
3565
+ className: "ps-modal-button",
3566
+ style: { width: "100%", justifyContent: "flex-start" },
3567
+ onClick: pick("cancel"),
3568
+ }, isBulk ? `Cancel ${bulkCount} Sessions` : isSystemRestart ? "Terminate & Restart" : "Cancel Session"),
3569
+ React.createElement("button", {
3570
+ type: "button",
3571
+ className: "ps-modal-button is-danger",
3572
+ style: { width: "100%", justifyContent: "flex-start" },
3573
+ onClick: pick("delete"),
3574
+ }, isBulk ? `Hard Delete ${bulkCount} Sessions` : isSystemRestart ? "Hard Delete & Restart" : "Delete Session"),
3575
+ ),
3576
+ React.createElement("div", { className: "ps-modal-footer" },
3577
+ React.createElement("button", { type: "button", className: "ps-modal-button", onClick: close }, "Close")),
3578
+ ));
3579
+ }
3580
+ if (modal.type === "artifactUpload" && modalState.artifactUpload) {
3581
+ return React.createElement("div", { className: "ps-modal-backdrop", onClick: close },
3582
+ React.createElement("div", { className: "ps-modal is-narrow", onClick: (event) => event.stopPropagation() },
3583
+ React.createElement("div", { className: "ps-modal-header" },
3584
+ React.createElement("div", { className: "ps-modal-title" }, modalState.artifactUpload.title),
3585
+ React.createElement("button", { type: "button", className: "ps-modal-close", onClick: close }, "Close"),
3586
+ ),
3587
+ React.createElement("input", {
3588
+ className: "ps-modal-input",
3589
+ value: modalState.artifactUpload.value,
3590
+ placeholder: modalState.artifactUpload.placeholder,
3591
+ onChange: (event) => controller.setArtifactUploadValue(event.currentTarget.value, event.currentTarget.selectionStart || event.currentTarget.value.length),
3592
+ autoFocus: true,
3593
+ }),
3594
+ React.createElement("div", { className: "ps-modal-details" },
3595
+ normalizeLines(modalState.artifactUpload.helpLines || []).map((line, index) => React.createElement(Line, { key: `help:${index}`, line, theme, className: "ps-modal-detail-line" })),
3596
+ ),
3597
+ React.createElement("div", { className: "ps-modal-footer" },
3598
+ React.createElement("button", { type: "button", className: "ps-modal-button", onClick: close }, "Cancel"),
3599
+ React.createElement("button", {
3600
+ type: "button",
3601
+ className: "ps-modal-button is-primary",
3602
+ onClick: () => controller.handleCommand(UI_COMMANDS.MODAL_CONFIRM).catch(() => {}),
3603
+ }, "Attach")),
3604
+ ));
3605
+ }
3606
+
3607
+ const filterPresentation = modal.type === "logFilter"
3608
+ ? modalState.logFilter
3609
+ : modal.type === "filesFilter"
3610
+ ? modalState.filesFilter
3611
+ : modal.type === "historyFormat"
3612
+ ? modalState.historyFormat
3613
+ : null;
3614
+ if (filterPresentation) {
3615
+ return React.createElement("div", { className: "ps-modal-backdrop", onClick: close },
3616
+ React.createElement("div", { className: "ps-modal is-wide", onClick: (event) => event.stopPropagation() },
3617
+ React.createElement("div", { className: "ps-modal-header" },
3618
+ React.createElement("div", { className: "ps-modal-title" }, filterPresentation.title),
3619
+ React.createElement("button", { type: "button", className: "ps-modal-close", onClick: close }, "Close"),
3620
+ ),
3621
+ React.createElement("div", { className: "ps-filter-grid" },
3622
+ (modal.items || []).map((item, itemIndex) => {
3623
+ const currentValue = modal.type === "filesFilter"
3624
+ ? modalState.filesFilterState?.[item.id] || item.options?.[0]?.id
3625
+ : modal.type === "historyFormat"
3626
+ ? modalState.historyFormatState
3627
+ : modalState.logsFilter?.[item.id] || item.options?.[0]?.id;
3628
+ return React.createElement("div", { key: item.id || itemIndex, className: "ps-filter-column" },
3629
+ React.createElement("div", { className: "ps-filter-title" }, item.label),
3630
+ (item.options || []).map((option) => React.createElement("button", {
3631
+ key: option.id,
3632
+ type: "button",
3633
+ className: `ps-filter-option${option.id === currentValue ? " is-selected" : ""}`,
3634
+ onClick: () => {
3635
+ controller.dispatch({ type: "ui/modalSelection", index: itemIndex });
3636
+ if (modal.type === "historyFormat") {
3637
+ controller.dispatch({ type: "executionHistory/format", format: option.id });
3638
+ } else if (modal.type === "filesFilter") {
3639
+ controller.dispatch({ type: "files/filter", filter: { [item.id]: option.id } });
3640
+ controller.ensureFilesForScope(option.id).catch(() => {});
3641
+ } else {
3642
+ controller.dispatch({ type: "logs/filter", filter: { [item.id]: option.id } });
3643
+ }
3644
+ },
3645
+ }, option.label)),
3646
+ );
3647
+ }),
3648
+ ),
3649
+ React.createElement("div", { className: "ps-modal-footer" },
3650
+ React.createElement("button", { type: "button", className: "ps-modal-button is-primary", onClick: close }, "Done")),
3651
+ ));
3652
+ }
3653
+
3654
+ return null;
3655
+ }
3656
+
3657
+ function useKeyboardShortcuts(controller, mobile) {
3658
+ React.useEffect(() => {
3659
+ const handler = (event) => {
3660
+ const target = event.target;
3661
+ const editable = target instanceof HTMLElement
3662
+ && (target.tagName === "TEXTAREA" || target.tagName === "INPUT" || target.isContentEditable);
3663
+ const modal = controller.getState().ui.modal;
3664
+ const visibleInspectorTabs = getVisibleInspectorTabs(controller);
3665
+ const currentInspectorTab = controller.getState().ui.inspectorTab;
3666
+ const focusRegion = controller.getState().ui.focusRegion;
3667
+ const isPlainShortcut = !event.metaKey && !event.ctrlKey && !event.altKey;
3668
+ const isShiftTheme = !event.metaKey && !event.ctrlKey && !event.altKey && event.key === "T" && event.shiftKey;
3669
+ const isShiftModel = !event.metaKey && !event.ctrlKey && !event.altKey && event.key === "N" && event.shiftKey;
3670
+ const selectVisibleInspectorTab = (delta) => {
3671
+ const nextTab = cycleTabs(visibleInspectorTabs, currentInspectorTab, delta);
3672
+ controller.selectInspectorTab(nextTab).catch(() => {});
3673
+ };
3674
+
3675
+ if (!editable && isShiftTheme) {
3676
+ event.preventDefault();
3677
+ controller.handleCommand(UI_COMMANDS.OPEN_THEME_PICKER).catch(() => {});
3678
+ return;
3679
+ }
3680
+ if (!editable && isShiftModel) {
3681
+ event.preventDefault();
3682
+ controller.handleCommand(UI_COMMANDS.OPEN_MODEL_PICKER).catch(() => {});
3683
+ return;
3684
+ }
3685
+
3686
+ if (modal && !editable) {
3687
+ if (event.key === "Escape" || (modal.type === "confirm" && event.key === "n")) {
3688
+ event.preventDefault();
3689
+ controller.handleCommand(UI_COMMANDS.CLOSE_MODAL).catch(() => {});
3690
+ return;
3691
+ }
3692
+ if (event.key === "Enter" || (modal.type === "confirm" && event.key === "y")) {
3693
+ event.preventDefault();
3694
+ controller.handleCommand(UI_COMMANDS.MODAL_CONFIRM).catch(() => {});
3695
+ return;
3696
+ }
3697
+ if (event.key === "Tab" && event.shiftKey) {
3698
+ event.preventDefault();
3699
+ controller.handleCommand(UI_COMMANDS.MODAL_PANE_PREV).catch(() => {});
3700
+ return;
3701
+ }
3702
+ if (event.key === "Tab") {
3703
+ event.preventDefault();
3704
+ controller.handleCommand(UI_COMMANDS.MODAL_PANE_NEXT).catch(() => {});
3705
+ return;
3706
+ }
3707
+ if (event.key === "ArrowUp" || event.key === "k") {
3708
+ event.preventDefault();
3709
+ controller.handleCommand(UI_COMMANDS.MODAL_PREV).catch(() => {});
3710
+ return;
3711
+ }
3712
+ if (event.key === "ArrowDown" || event.key === "j") {
3713
+ event.preventDefault();
3714
+ controller.handleCommand(UI_COMMANDS.MODAL_NEXT).catch(() => {});
3715
+ }
3716
+ return;
3717
+ }
3718
+
3719
+ if (editable) {
3720
+ return;
3721
+ }
3722
+
3723
+ if (event.key === "r" && isPlainShortcut && focusRegion !== "prompt") {
3724
+ event.preventDefault();
3725
+ controller.handleCommand(UI_COMMANDS.REFRESH).catch(() => {});
3726
+ return;
3727
+ }
3728
+ if (focusRegion === "chat" && event.key === "s" && isPlainShortcut) {
3729
+ event.preventDefault();
3730
+ controller.handleCommand(UI_COMMANDS.TOGGLE_CHAT_VIEW).catch(() => {});
3731
+ return;
3732
+ }
3733
+ if (event.key === "n" && isPlainShortcut) {
3734
+ event.preventDefault();
3735
+ controller.handleCommand(UI_COMMANDS.NEW_SESSION).catch(() => {});
3736
+ return;
3737
+ }
3738
+ if (
3739
+ focusRegion === "inspector"
3740
+ && currentInspectorTab === "files"
3741
+ && (
3742
+ (event.key === "u" && isPlainShortcut)
3743
+ || ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "a")
3744
+ )
3745
+ ) {
3746
+ event.preventDefault();
3747
+ controller.handleCommand(UI_COMMANDS.OPEN_ARTIFACT_UPLOAD).catch(() => {});
3748
+ return;
3749
+ }
3750
+ if (focusRegion === "inspector" && currentInspectorTab === "files" && event.key === "a" && isPlainShortcut) {
3751
+ event.preventDefault();
3752
+ controller.handleCommand(UI_COMMANDS.DOWNLOAD_SELECTED_FILE).catch(() => {});
3753
+ return;
3754
+ }
3755
+ if (focusRegion === "inspector" && currentInspectorTab === "files" && event.key === "x" && isPlainShortcut) {
3756
+ event.preventDefault();
3757
+ controller.handleCommand(UI_COMMANDS.DELETE_SELECTED_FILE).catch(() => {});
3758
+ return;
3759
+ }
3760
+ if (focusRegion === "inspector" && currentInspectorTab === "files" && event.key === "o" && isPlainShortcut) {
3761
+ event.preventDefault();
3762
+ controller.handleCommand(UI_COMMANDS.OPEN_SELECTED_FILE).catch(() => {});
3763
+ return;
3764
+ }
3765
+ if (focusRegion === "inspector" && currentInspectorTab === "files" && event.key === "f" && isPlainShortcut) {
3766
+ event.preventDefault();
3767
+ controller.handleCommand(UI_COMMANDS.OPEN_FILES_FILTER).catch(() => {});
3768
+ return;
3769
+ }
3770
+ if (focusRegion === "inspector" && currentInspectorTab === "stats" && event.key === "f" && isPlainShortcut) {
3771
+ event.preventDefault();
3772
+ controller.handleCommand(UI_COMMANDS.TOGGLE_STATS_VIEW).catch(() => {});
3773
+ return;
3774
+ }
3775
+ if (focusRegion === "inspector" && currentInspectorTab === "files" && event.key === "v" && isPlainShortcut) {
3776
+ event.preventDefault();
3777
+ controller.handleCommand(UI_COMMANDS.TOGGLE_FILE_PREVIEW_FULLSCREEN).catch(() => {});
3778
+ return;
3779
+ }
3780
+ if (focusRegion === "inspector" && currentInspectorTab === "logs" && event.key === "t" && isPlainShortcut) {
3781
+ event.preventDefault();
3782
+ controller.handleCommand(UI_COMMANDS.TOGGLE_LOG_TAIL).catch(() => {});
3783
+ return;
3784
+ }
3785
+ if (focusRegion === "inspector" && currentInspectorTab === "logs" && event.key === "f" && isPlainShortcut) {
3786
+ event.preventDefault();
3787
+ controller.handleCommand(UI_COMMANDS.OPEN_LOG_FILTER).catch(() => {});
3788
+ return;
3789
+ }
3790
+ if (focusRegion === "inspector" && currentInspectorTab === "history" && event.key === "f" && isPlainShortcut) {
3791
+ event.preventDefault();
3792
+ controller.handleCommand(UI_COMMANDS.OPEN_HISTORY_FORMAT).catch(() => {});
3793
+ return;
3794
+ }
3795
+ if (focusRegion === "inspector" && currentInspectorTab === "history" && event.key === "r" && isPlainShortcut) {
3796
+ event.preventDefault();
3797
+ controller.handleCommand(UI_COMMANDS.REFRESH_EXECUTION_HISTORY).catch(() => {});
3798
+ return;
3799
+ }
3800
+ if (focusRegion === "inspector" && currentInspectorTab === "history" && event.key === "a" && isPlainShortcut) {
3801
+ event.preventDefault();
3802
+ controller.handleCommand(UI_COMMANDS.EXPORT_EXECUTION_HISTORY).catch(() => {});
3803
+ return;
3804
+ }
3805
+ if (event.key === "a" && isPlainShortcut) {
3806
+ event.preventDefault();
3807
+ controller.handleCommand(UI_COMMANDS.OPEN_ARTIFACT_PICKER).catch(() => {});
3808
+ return;
3809
+ }
3810
+ if (event.key === "p" && isPlainShortcut) {
3811
+ event.preventDefault();
3812
+ controller.handleCommand(UI_COMMANDS.FOCUS_PROMPT).catch(() => {});
3813
+ return;
3814
+ }
3815
+ if (event.key === "c" && isPlainShortcut) {
3816
+ event.preventDefault();
3817
+ controller.handleCommand(UI_COMMANDS.CANCEL_SESSION).catch(() => {});
3818
+ return;
3819
+ }
3820
+ if (event.key === "d" && isPlainShortcut) {
3821
+ event.preventDefault();
3822
+ controller.handleCommand(UI_COMMANDS.DONE_SESSION).catch(() => {});
3823
+ return;
3824
+ }
3825
+ if (event.key === "D" && event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey) {
3826
+ event.preventDefault();
3827
+ controller.handleCommand(UI_COMMANDS.DELETE_SESSION).catch(() => {});
3828
+ return;
3829
+ }
3830
+ if (event.key === "m" && isPlainShortcut && focusRegion === "inspector") {
3831
+ event.preventDefault();
3832
+ selectVisibleInspectorTab(1);
3833
+ return;
3834
+ }
3835
+ if (event.key === "[" && isPlainShortcut) {
3836
+ event.preventDefault();
3837
+ controller.handleCommand(UI_COMMANDS.GROW_LEFT_PANE).catch(() => {});
3838
+ return;
3839
+ }
3840
+ if (event.key === "]" && isPlainShortcut) {
3841
+ event.preventDefault();
3842
+ controller.handleCommand(UI_COMMANDS.GROW_RIGHT_PANE).catch(() => {});
3843
+ return;
3844
+ }
3845
+ if (event.key === "{" && isPlainShortcut) {
3846
+ event.preventDefault();
3847
+ controller.handleCommand(UI_COMMANDS.SHRINK_SESSION_PANE).catch(() => {});
3848
+ return;
3849
+ }
3850
+ if (event.key === "}" && isPlainShortcut) {
3851
+ event.preventDefault();
3852
+ controller.handleCommand(UI_COMMANDS.GROW_SESSION_PANE).catch(() => {});
3853
+ return;
3854
+ }
3855
+ if (focusRegion === "inspector" && event.key === "ArrowLeft") {
3856
+ event.preventDefault();
3857
+ selectVisibleInspectorTab(-1);
3858
+ return;
3859
+ }
3860
+ if (focusRegion === "inspector" && event.key === "ArrowRight") {
3861
+ event.preventDefault();
3862
+ selectVisibleInspectorTab(1);
3863
+ return;
3864
+ }
3865
+ if (focusRegion === "sessions" && (event.key === "ArrowUp" || event.key === "k")) {
3866
+ event.preventDefault();
3867
+ controller.handleCommand(UI_COMMANDS.MOVE_SESSION_UP).catch(() => {});
3868
+ return;
3869
+ }
3870
+ if (focusRegion === "sessions" && (event.key === "ArrowDown" || event.key === "j")) {
3871
+ event.preventDefault();
3872
+ controller.handleCommand(UI_COMMANDS.MOVE_SESSION_DOWN).catch(() => {});
3873
+ return;
3874
+ }
3875
+ if (focusRegion === "sessions" && event.ctrlKey && event.key.toLowerCase() === "g" && !event.metaKey && !event.altKey) {
3876
+ event.preventDefault();
3877
+ controller.handleCommand(UI_COMMANDS.OPEN_MOVE_TO_GROUP).catch(() => {});
3878
+ return;
3879
+ }
3880
+ if (focusRegion === "sessions" && (event.key === "PageUp" || (event.ctrlKey && event.key.toLowerCase() === "u"))) {
3881
+ event.preventDefault();
3882
+ controller.handleCommand(UI_COMMANDS.PAGE_UP).catch(() => {});
3883
+ return;
3884
+ }
3885
+ if (focusRegion === "sessions" && (event.key === "PageDown" || (event.ctrlKey && event.key.toLowerCase() === "d"))) {
3886
+ event.preventDefault();
3887
+ controller.handleCommand(UI_COMMANDS.PAGE_DOWN).catch(() => {});
3888
+ return;
3889
+ }
3890
+ if (focusRegion === "sessions" && event.key === "t" && isPlainShortcut) {
3891
+ event.preventDefault();
3892
+ controller.handleCommand(UI_COMMANDS.OPEN_RENAME_SESSION).catch(() => {});
3893
+ return;
3894
+ }
3895
+ if (!mobile && (event.key === "PageUp" || (event.ctrlKey && event.key.toLowerCase() === "u"))) {
3896
+ event.preventDefault();
3897
+ controller.handleCommand(UI_COMMANDS.PAGE_UP).catch(() => {});
3898
+ return;
3899
+ }
3900
+ if (!mobile && (event.key === "PageDown" || (event.ctrlKey && event.key.toLowerCase() === "d"))) {
3901
+ event.preventDefault();
3902
+ controller.handleCommand(UI_COMMANDS.PAGE_DOWN).catch(() => {});
3903
+ return;
3904
+ }
3905
+ if (!mobile && event.key === "g" && isPlainShortcut) {
3906
+ event.preventDefault();
3907
+ controller.handleCommand(UI_COMMANDS.SCROLL_TOP).catch(() => {});
3908
+ return;
3909
+ }
3910
+ if (!mobile && event.key === "G" && event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey) {
3911
+ event.preventDefault();
3912
+ controller.handleCommand(UI_COMMANDS.SCROLL_BOTTOM).catch(() => {});
3913
+ return;
3914
+ }
3915
+ if (focusRegion !== "prompt" && event.key === "h" && isPlainShortcut) {
3916
+ event.preventDefault();
3917
+ controller.handleCommand(UI_COMMANDS.FOCUS_LEFT).catch(() => {});
3918
+ return;
3919
+ }
3920
+ if (focusRegion !== "prompt" && event.key === "l" && isPlainShortcut) {
3921
+ event.preventDefault();
3922
+ controller.handleCommand(UI_COMMANDS.FOCUS_RIGHT).catch(() => {});
3923
+ return;
3924
+ }
3925
+ if (!mobile && (event.key === "ArrowUp" || event.key === "k")) {
3926
+ event.preventDefault();
3927
+ controller.handleCommand(UI_COMMANDS.SCROLL_UP).catch(() => {});
3928
+ return;
3929
+ }
3930
+ if (!mobile && (event.key === "ArrowDown" || event.key === "j")) {
3931
+ event.preventDefault();
3932
+ controller.handleCommand(UI_COMMANDS.SCROLL_DOWN).catch(() => {});
3933
+ return;
3934
+ }
3935
+ if (!mobile && event.key === "Escape") {
3936
+ event.preventDefault();
3937
+ controller.handleCommand(UI_COMMANDS.FOCUS_SESSIONS).catch(() => {});
3938
+ }
3939
+ };
3940
+
3941
+ window.addEventListener("keydown", handler);
3942
+ return () => window.removeEventListener("keydown", handler);
3943
+ }, [controller, mobile]);
3944
+ }
3945
+
3946
+ function formatAdminPrincipalLabel(principal) {
3947
+ if (!principal) return "Unknown user";
3948
+ const name = String(principal.displayName || "").trim();
3949
+ const email = String(principal.email || "").trim();
3950
+ if (name && email && name.toLowerCase() !== email.toLowerCase()) return `${name} <${email}>`;
3951
+ if (name) return name;
3952
+ if (email) return email;
3953
+ const provider = String(principal.provider || "").trim();
3954
+ const subject = String(principal.subject || "").trim();
3955
+ return [provider, subject].filter(Boolean).join(":") || "user";
3956
+ }
3957
+
3958
+ function AdminConsolePanel({ controller }) {
3959
+ const view = useControllerSelector(controller, selectAdminConsole, shallowEqualObject);
3960
+ const draftRef = React.useRef(null);
3961
+
3962
+ React.useEffect(() => {
3963
+ if (view.ghcpKey.editing) {
3964
+ // Defer focus to next tick so the input has mounted.
3965
+ const handle = window.requestAnimationFrame(() => {
3966
+ if (draftRef.current) draftRef.current.focus();
3967
+ });
3968
+ return () => window.cancelAnimationFrame(handle);
3969
+ }
3970
+ return undefined;
3971
+ }, [view.ghcpKey.editing]);
3972
+
3973
+ const onClose = React.useCallback(() => {
3974
+ controller.closeAdminConsole();
3975
+ }, [controller]);
3976
+ const onRefresh = React.useCallback(() => {
3977
+ controller.refreshAdminProfile().catch(() => {});
3978
+ }, [controller]);
3979
+ const onBeginEdit = React.useCallback(() => {
3980
+ controller.beginAdminEditGhcpKey();
3981
+ }, [controller]);
3982
+ const onCancelEdit = React.useCallback(() => {
3983
+ controller.cancelAdminEditGhcpKey();
3984
+ }, [controller]);
3985
+ const onClear = React.useCallback(() => {
3986
+ controller.clearAdminGhcpKey().catch(() => {});
3987
+ }, [controller]);
3988
+ const onSubmit = React.useCallback((event) => {
3989
+ event.preventDefault();
3990
+ controller.saveAdminGhcpKey().catch(() => {});
3991
+ }, [controller]);
3992
+ const onDraftChange = React.useCallback((event) => {
3993
+ controller.setAdminGhcpKeyDraft(event.target.value);
3994
+ }, [controller]);
3995
+
3996
+ const principalLabel = formatAdminPrincipalLabel(view.principal);
3997
+
3998
+ return React.createElement("div", { className: "ps-admin-console" },
3999
+ React.createElement("header", { className: "ps-admin-console__header" },
4000
+ React.createElement("h2", null, "Admin Console"),
4001
+ React.createElement("button", {
4002
+ type: "button",
4003
+ className: "ps-mini-button",
4004
+ onClick: onClose,
4005
+ }, "Close")),
4006
+ React.createElement("section", { className: "ps-admin-console__identity" },
4007
+ React.createElement("dl", null,
4008
+ React.createElement("dt", null, "Signed in as"),
4009
+ React.createElement("dd", null, principalLabel))),
4010
+ view.loadError
4011
+ ? React.createElement("div", { className: "ps-admin-console__error", role: "alert" }, view.loadError)
4012
+ : null,
4013
+ React.createElement("section", { className: "ps-admin-console__section" },
4014
+ React.createElement("h3", null, "GitHub Copilot key"),
4015
+ React.createElement("p", { className: "ps-admin-console__hint" },
4016
+ "Per-user override for the GitHub Copilot model provider token. ",
4017
+ "When set, this key is used instead of the worker's env-supplied ",
4018
+ "GITHUB_TOKEN for sessions you own. Clearing the key reverts to ",
4019
+ "the worker default."),
4020
+ React.createElement("p", { className: `ps-admin-console__status${view.ghcpKey.error ? " is-error" : ""}` },
4021
+ view.ghcpKey.error || view.ghcpKey.statusText),
4022
+ view.ghcpKey.editing
4023
+ ? React.createElement("form", { className: "ps-admin-console__form", onSubmit },
4024
+ React.createElement("input", {
4025
+ ref: draftRef,
4026
+ type: "password",
4027
+ autoComplete: "off",
4028
+ spellCheck: false,
4029
+ value: view.ghcpKey.draft,
4030
+ disabled: view.ghcpKey.saving,
4031
+ placeholder: "Paste GitHub Copilot key",
4032
+ onChange: onDraftChange,
4033
+ }),
4034
+ React.createElement("div", { className: "ps-admin-console__actions" },
4035
+ React.createElement("button", {
4036
+ type: "submit",
4037
+ className: "ps-primary-button",
4038
+ disabled: view.ghcpKey.saving,
4039
+ }, view.ghcpKey.saving ? "Saving..." : "Save"),
4040
+ React.createElement("button", {
4041
+ type: "button",
4042
+ className: "ps-mini-button",
4043
+ onClick: onCancelEdit,
4044
+ disabled: view.ghcpKey.saving,
4045
+ }, "Cancel")))
4046
+ : React.createElement("div", { className: "ps-admin-console__actions" },
4047
+ React.createElement("button", {
4048
+ type: "button",
4049
+ className: "ps-primary-button",
4050
+ onClick: onBeginEdit,
4051
+ }, view.ghcpKey.configured ? "Replace key" : "Set key"),
4052
+ view.ghcpKey.configured
4053
+ ? React.createElement("button", {
4054
+ type: "button",
4055
+ className: "ps-mini-button",
4056
+ onClick: onClear,
4057
+ }, "Clear key")
4058
+ : null,
4059
+ React.createElement("button", {
4060
+ type: "button",
4061
+ className: "ps-mini-button",
4062
+ onClick: onRefresh,
4063
+ }, view.loading ? "Refreshing..." : "Refresh"))));
4064
+ }
4065
+
4066
+ export function createWebPilotSwarmController({ transport, mode = "remote", branding = null } = {}) {
4067
+ clearBrowserPreferenceCache();
4068
+ const store = createStore(appReducer, createInitialState({
4069
+ mode,
4070
+ branding,
4071
+ }));
4072
+ return new PilotSwarmUiController({ store, transport });
4073
+ }
4074
+
4075
+ export function PilotSwarmWebApp({ controller }) {
4076
+ const viewportRef = React.useRef(null);
4077
+ const mainGridRef = React.useRef(null);
4078
+ const viewport = useMeasuredViewport(viewportRef);
4079
+ const mainGridViewport = useMeasuredViewport(mainGridRef);
4080
+ const gridViewport = computeGridViewport(viewport);
4081
+ const [chatFocusMode, setChatFocusMode] = React.useState(false);
4082
+ const [chatFocusPane, setChatFocusPane] = React.useState(null);
4083
+ const state = useControllerSelector(controller, (rootState) => ({
4084
+ themeId: rootState.ui.themeId,
4085
+ ownerFilter: rootState.sessions.ownerFilter,
4086
+ pinnedIds: rootState.sessions.pinnedIds,
4087
+ // Pass the live Set reference here so shallow-equal sees the same
4088
+ // identity across renders (a fresh array would break memoization).
4089
+ // Conversion to a sorted array happens inside `normalizeProfileSettings`.
4090
+ collapsedSessionIds: rootState.sessions.collapsedIds,
4091
+ activeSessionId: rootState.sessions.activeSessionId || null,
4092
+ promptRows: getStatePromptRows(rootState),
4093
+ chatViewMode: rootState.ui.chatViewMode || "transcript",
4094
+ activeSessionIsGroup: Boolean(rootState.sessions.activeSessionId && rootState.sessions.byId[rootState.sessions.activeSessionId]?.isGroup),
4095
+ paneAdjust: rootState.ui.layout?.paneAdjust ?? 0,
4096
+ sessionPaneAdjust: rootState.ui.layout?.sessionPaneAdjust ?? 0,
4097
+ portalSessionColumnAdjust: rootState.ui.layout?.portalSessionColumnAdjust ?? 0,
4098
+ activityPaneAdjust: rootState.ui.layout?.activityPaneAdjust ?? 0,
4099
+ focusRegion: rootState.ui.focusRegion,
4100
+ inspectorTab: rootState.ui.inspectorTab,
4101
+ filesFullscreen: Boolean(rootState.files.fullscreen),
4102
+ adminVisible: Boolean(rootState.admin?.visible),
4103
+ }), shallowEqualObject);
4104
+ const profileSettingsHydratedRef = React.useRef(false);
4105
+ const lastProfileSettingsJsonRef = React.useRef(null);
4106
+ const profileSettingsSaveTimerRef = React.useRef(null);
4107
+ const profileSettingsPollTimerRef = React.useRef(null);
4108
+ const profileSettingsPollInFlightRef = React.useRef(false);
4109
+ const profileSettingsSaveInFlightRef = React.useRef(false);
4110
+ const appliedProfileSettingsJsonRef = React.useRef(null);
4111
+ const defaultProfileSettingsRef = React.useRef(null);
4112
+ const [mobilePane, setMobilePane] = React.useState("workspace");
4113
+ const [mobileSessionsCollapsed, setMobileSessionsCollapsed] = React.useState(false);
4114
+ const mobile = (viewport.width || window.innerWidth || 0) < MOBILE_BREAKPOINT;
4115
+ const readOnlyChatPane = state.activeSessionIsGroup || state.chatViewMode === "summary";
4116
+ const effectivePromptRows = readOnlyChatPane ? 0 : state.promptRows;
4117
+
4118
+ useKeyboardShortcuts(controller, mobile);
4119
+
4120
+ React.useEffect(() => {
4121
+ controller.setViewport(gridViewport);
4122
+ }, [controller, gridViewport.height, gridViewport.width]);
4123
+
4124
+ React.useEffect(() => {
4125
+ let active = true;
4126
+ profileSettingsHydratedRef.current = false;
4127
+ lastProfileSettingsJsonRef.current = null;
4128
+ appliedProfileSettingsJsonRef.current = null;
4129
+ defaultProfileSettingsRef.current = buildDefaultProfileSettingsFromState(controller.getState());
4130
+ if (profileSettingsSaveTimerRef.current) {
4131
+ clearTimeout(profileSettingsSaveTimerRef.current);
4132
+ profileSettingsSaveTimerRef.current = null;
4133
+ }
4134
+ if (profileSettingsPollTimerRef.current) {
4135
+ clearInterval(profileSettingsPollTimerRef.current);
4136
+ profileSettingsPollTimerRef.current = null;
4137
+ }
4138
+ profileSettingsPollInFlightRef.current = false;
4139
+ profileSettingsSaveInFlightRef.current = false;
4140
+
4141
+ const transport = controller.transport;
4142
+ if (typeof transport?.getCurrentUserProfile !== "function"
4143
+ || typeof transport?.setCurrentUserProfileSettings !== "function") {
4144
+ profileSettingsHydratedRef.current = true;
4145
+ return () => {
4146
+ active = false;
4147
+ };
4148
+ }
4149
+
4150
+ const pollProfileSettings = async () => {
4151
+ if (!active || profileSettingsPollInFlightRef.current) return;
4152
+ profileSettingsPollInFlightRef.current = true;
4153
+ try {
4154
+ const profile = await transport.getCurrentUserProfile();
4155
+ if (!active) return;
4156
+ const settings = materializeProfileSettings(
4157
+ profile?.profileSettings,
4158
+ defaultProfileSettingsRef.current,
4159
+ );
4160
+ const settingsJson = JSON.stringify(settings);
4161
+ const currentSettingsBeforeApply = profileSettingsFromViewState(controller.getState());
4162
+ const currentSettingsBeforeApplyJson = JSON.stringify(currentSettingsBeforeApply);
4163
+ const hasUnpersistedLocalChange = profileSettingsHydratedRef.current
4164
+ && lastProfileSettingsJsonRef.current != null
4165
+ && currentSettingsBeforeApplyJson !== lastProfileSettingsJsonRef.current;
4166
+ const hasPendingLocalWrite = Boolean(profileSettingsSaveTimerRef.current)
4167
+ || profileSettingsSaveInFlightRef.current
4168
+ || hasUnpersistedLocalChange;
4169
+ if (!hasPendingLocalWrite && appliedProfileSettingsJsonRef.current !== settingsJson) {
4170
+ controller.dispatch({ type: "profileSettings/apply", settings });
4171
+ appliedProfileSettingsJsonRef.current = settingsJson;
4172
+ }
4173
+
4174
+ const currentSettings = profileSettingsFromViewState(controller.getState());
4175
+ lastProfileSettingsJsonRef.current = JSON.stringify(currentSettings);
4176
+ profileSettingsHydratedRef.current = true;
4177
+ } catch {
4178
+ if (!active) return;
4179
+ profileSettingsHydratedRef.current = true;
4180
+ } finally {
4181
+ profileSettingsPollInFlightRef.current = false;
4182
+ }
4183
+ };
4184
+
4185
+ pollProfileSettings().catch(() => {});
4186
+ profileSettingsPollTimerRef.current = setInterval(() => {
4187
+ pollProfileSettings().catch(() => {});
4188
+ }, PROFILE_SETTINGS_POLL_MS);
4189
+
4190
+ return () => {
4191
+ active = false;
4192
+ if (profileSettingsSaveTimerRef.current) {
4193
+ clearTimeout(profileSettingsSaveTimerRef.current);
4194
+ profileSettingsSaveTimerRef.current = null;
4195
+ }
4196
+ if (profileSettingsPollTimerRef.current) {
4197
+ clearInterval(profileSettingsPollTimerRef.current);
4198
+ profileSettingsPollTimerRef.current = null;
4199
+ }
4200
+ profileSettingsPollInFlightRef.current = false;
4201
+ };
4202
+ }, [controller]);
4203
+
4204
+ React.useEffect(() => {
4205
+ if (!profileSettingsHydratedRef.current) return undefined;
4206
+ const settings = profileSettingsFromViewState(state);
4207
+ const settingsJson = JSON.stringify(settings);
4208
+ if (lastProfileSettingsJsonRef.current === settingsJson) return undefined;
4209
+ lastProfileSettingsJsonRef.current = settingsJson;
4210
+ if (profileSettingsSaveTimerRef.current) {
4211
+ clearTimeout(profileSettingsSaveTimerRef.current);
4212
+ }
4213
+ profileSettingsSaveTimerRef.current = setTimeout(() => {
4214
+ profileSettingsSaveTimerRef.current = null;
4215
+ profileSettingsSaveInFlightRef.current = true;
4216
+ saveProfileSettings(controller, settings)
4217
+ .catch(() => {})
4218
+ .finally(() => {
4219
+ profileSettingsSaveInFlightRef.current = false;
4220
+ });
4221
+ }, 400);
4222
+ return undefined;
4223
+ }, [controller, state.activeSessionId, state.activityPaneAdjust, state.chatViewMode, state.collapsedSessionIds, state.ownerFilter, state.paneAdjust, state.pinnedIds, state.portalSessionColumnAdjust, state.sessionPaneAdjust, state.themeId]);
4224
+
4225
+ React.useEffect(() => {
4226
+ applyDocumentTheme(state.themeId);
4227
+ }, [state.themeId]);
4228
+
4229
+ React.useEffect(() => {
4230
+ if (mobile && state.focusRegion !== "prompt") {
4231
+ setMobilePane(state.focusRegion === "activity"
4232
+ ? "activity"
4233
+ : state.focusRegion === "inspector"
4234
+ ? "inspector"
4235
+ : "workspace");
4236
+ }
4237
+ }, [mobile, state.focusRegion]);
4238
+
4239
+ React.useEffect(() => {
4240
+ const visibleTabs = getVisibleInspectorTabs(controller);
4241
+ if (!visibleTabs.includes(state.inspectorTab) && visibleTabs.length > 0) {
4242
+ controller.selectInspectorTab(visibleTabs[0]).catch(() => {});
4243
+ }
4244
+ }, [controller, state.inspectorTab]);
4245
+
4246
+ const layout = React.useMemo(
4247
+ () => computeLegacyLayout(gridViewport, state.paneAdjust, effectivePromptRows, state.sessionPaneAdjust, state.activityPaneAdjust),
4248
+ [gridViewport, state.activityPaneAdjust, state.paneAdjust, effectivePromptRows, state.sessionPaneAdjust],
4249
+ );
4250
+ const filesFullscreenActive = state.filesFullscreen && state.inspectorTab === "files";
4251
+
4252
+ React.useEffect(() => {
4253
+ if (!filesFullscreenActive || !chatFocusMode) return;
4254
+ setChatFocusMode(false);
4255
+ setChatFocusPane(null);
4256
+ }, [chatFocusMode, filesFullscreenActive]);
4257
+
4258
+ const toggleChatFocusMode = React.useCallback(() => {
4259
+ setChatFocusMode((current) => {
4260
+ const next = !current;
4261
+ if (!next) {
4262
+ setChatFocusPane(null);
4263
+ } else {
4264
+ controller.setFocus("chat");
4265
+ }
4266
+ return next;
4267
+ });
4268
+ }, [controller]);
4269
+
4270
+ const toggleChatFocusPane = React.useCallback((paneId) => {
4271
+ setChatFocusPane((current) => {
4272
+ const next = current === paneId ? null : paneId;
4273
+ controller.setFocus(next || "chat");
4274
+ return next;
4275
+ });
4276
+ }, [controller]);
4277
+
4278
+ const estimatedMainGridWidth = Math.max(0, (viewport.width || 0) * 0.68);
4279
+ const measuredMainGridWidth = mainGridViewport.width || estimatedMainGridWidth;
4280
+ const sessionColumnWidth = portalSessionColumnWidth(measuredMainGridWidth, state.portalSessionColumnAdjust);
4281
+ const sessionColumnMode = portalSessionColumnMode(sessionColumnWidth);
4282
+ const sessionColumnTrack = sessionColumnMode === "hidden"
4283
+ ? "0px"
4284
+ : `clamp(0px, calc(${PORTAL_SESSION_COLUMN_RATIO * 100}% + ${Number(state.portalSessionColumnAdjust) || 0}px), max(0px, calc(100% - 14rem - 32px)))`;
4285
+
4286
+ const desktopWorkspace = React.createElement("div", {
4287
+ className: "ps-workspace-grid",
4288
+ style: {
4289
+ gridTemplateColumns: `minmax(0, ${layout.leftWidth}fr) 16px minmax(0, ${layout.rightWidth}fr)`,
4290
+ },
4291
+ },
4292
+ React.createElement("div", {
4293
+ ref: mainGridRef,
4294
+ className: `ps-workspace-main-grid is-session-${sessionColumnMode}`,
4295
+ style: {
4296
+ gridTemplateColumns: `${sessionColumnTrack} 16px minmax(14rem, 1fr)`,
4297
+ },
4298
+ },
4299
+ sessionColumnMode !== "hidden" ? React.createElement("div", {
4300
+ className: "ps-workspace-pane-slot",
4301
+ style: { gridColumn: "1" },
4302
+ },
4303
+ React.createElement(SessionPane, { controller, structuredRows: true })) : null,
4304
+ React.createElement("div", {
4305
+ style: {
4306
+ gridColumn: "2",
4307
+ minWidth: 0,
4308
+ display: "flex",
4309
+ },
4310
+ },
4311
+ React.createElement(SessionColumnResizeHandle, { controller, portalSessionColumnAdjust: state.portalSessionColumnAdjust })),
4312
+ React.createElement("div", {
4313
+ className: "ps-workspace-pane-slot",
4314
+ style: { gridColumn: "3" },
4315
+ },
4316
+ React.createElement(ChatPane, { controller }))),
4317
+ React.createElement(ColumnResizeHandle, { controller, paneAdjust: state.paneAdjust }),
4318
+ React.createElement("div", {
4319
+ className: "ps-workspace-column",
4320
+ style: { gridTemplateRows: `${layout.inspectorPaneHeight}fr 16px ${layout.activityPaneHeight}fr` },
4321
+ },
4322
+ React.createElement("div", {
4323
+ className: "ps-workspace-pane-slot",
4324
+ style: { gridRow: "1" },
4325
+ },
4326
+ !layout.inspectorHidden ? React.createElement(InspectorPane, { controller, mobile: false }) : null),
4327
+ React.createElement("div", {
4328
+ style: {
4329
+ gridRow: "2",
4330
+ minHeight: 0,
4331
+ display: "flex",
4332
+ flexDirection: "column",
4333
+ },
4334
+ },
4335
+ React.createElement(ActivityRowResizeHandle, { controller, activityPaneAdjust: state.activityPaneAdjust })),
4336
+ React.createElement("div", {
4337
+ className: "ps-workspace-pane-slot",
4338
+ style: { gridRow: "3" },
4339
+ },
4340
+ !layout.activityHidden ? React.createElement(ActivityPane, { controller }) : null)));
4341
+ const chatFocusWorkspace = React.createElement(ChatFocusWorkspace, {
4342
+ controller,
4343
+ openPane: chatFocusPane,
4344
+ onTogglePane: toggleChatFocusPane,
4345
+ onExitFocus: toggleChatFocusMode,
4346
+ mobile,
4347
+ });
4348
+ const fullscreenWorkspace = React.createElement("div", { className: "ps-workspace-full" },
4349
+ React.createElement(InspectorPane, { controller, mobile: false }));
4350
+
4351
+ let mobileContent = null;
4352
+ if (filesFullscreenActive) mobileContent = React.createElement("div", { className: "ps-mobile-pane-fill" },
4353
+ React.createElement(InspectorPane, { controller, mobile: true }));
4354
+ else if (mobilePane === "inspector") mobileContent = React.createElement("div", { className: "ps-mobile-pane-fill" },
4355
+ React.createElement(InspectorPane, { controller, mobile: true }));
4356
+ else if (mobilePane === "activity") mobileContent = React.createElement("div", { className: "ps-mobile-pane-fill" },
4357
+ React.createElement(ActivityPane, { controller }));
4358
+ else mobileContent = React.createElement(MobileWorkspace, {
4359
+ controller,
4360
+ sessionsCollapsed: mobileSessionsCollapsed,
4361
+ setSessionsCollapsed: setMobileSessionsCollapsed,
4362
+ });
4363
+
4364
+ return React.createElement("div", { ref: viewportRef, className: "ps-web-shell" },
4365
+ // Hide the top toolbar on mobile inspector/activity panes (and in
4366
+ // chat-focus mode). Those panes are pure read-only surfaces;
4367
+ // session/model/theme actions stay reachable from the Main pane,
4368
+ // and dropping the toolbar buys ~3 lines of vertical real estate
4369
+ // on a phone, which matters for the fleet-skills card and the
4370
+ // logs/sequence inspectors.
4371
+ (mobile && (chatFocusMode || mobilePane === "inspector" || mobilePane === "activity"))
4372
+ ? null
4373
+ : React.createElement(Toolbar, {
4374
+ controller,
4375
+ mobile,
4376
+ chatFocusMode,
4377
+ onToggleChatFocus: toggleChatFocusMode,
4378
+ chatFocusDisabled: filesFullscreenActive,
4379
+ }),
4380
+ React.createElement("div", { className: "ps-workspace" },
4381
+ state.adminVisible
4382
+ ? React.createElement(AdminConsolePanel, { controller })
4383
+ : (filesFullscreenActive
4384
+ ? fullscreenWorkspace
4385
+ : (chatFocusMode
4386
+ ? chatFocusWorkspace
4387
+ : (mobile ? mobileContent : desktopWorkspace)))),
4388
+ mobile && !chatFocusMode ? React.createElement(MobileNav, { activePane: mobilePane, setActivePane: setMobilePane, controller }) : null,
4389
+ React.createElement(ModalLayer, { controller }));
4390
+ }