pilotswarm 0.0.1 → 0.4.1

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