pilotswarm-web 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +144 -0
  2. package/auth/authz/engine.js +139 -0
  3. package/auth/config.js +110 -0
  4. package/auth/index.js +153 -0
  5. package/auth/normalize/entra.js +22 -0
  6. package/auth/providers/entra.js +76 -0
  7. package/auth/providers/none.js +24 -0
  8. package/auth.js +10 -0
  9. package/bin/serve.js +53 -0
  10. package/config.js +20 -0
  11. package/dist/app.js +469 -0
  12. package/dist/assets/index-BSVg-lGb.css +1 -0
  13. package/dist/assets/index-BXD5YP7A.js +24 -0
  14. package/dist/assets/msal-CytV9RFv.js +7 -0
  15. package/dist/assets/pilotswarm-WX3NED6m.js +40 -0
  16. package/dist/assets/react-jg0oazEi.js +1 -0
  17. package/dist/index.html +16 -0
  18. package/node_modules/pilotswarm-ui-core/README.md +6 -0
  19. package/node_modules/pilotswarm-ui-core/package.json +32 -0
  20. package/node_modules/pilotswarm-ui-core/src/commands.js +72 -0
  21. package/node_modules/pilotswarm-ui-core/src/context-usage.js +212 -0
  22. package/node_modules/pilotswarm-ui-core/src/controller.js +3613 -0
  23. package/node_modules/pilotswarm-ui-core/src/formatting.js +872 -0
  24. package/node_modules/pilotswarm-ui-core/src/history.js +571 -0
  25. package/node_modules/pilotswarm-ui-core/src/index.js +13 -0
  26. package/node_modules/pilotswarm-ui-core/src/layout.js +196 -0
  27. package/node_modules/pilotswarm-ui-core/src/reducer.js +1027 -0
  28. package/node_modules/pilotswarm-ui-core/src/selectors.js +2786 -0
  29. package/node_modules/pilotswarm-ui-core/src/session-tree.js +109 -0
  30. package/node_modules/pilotswarm-ui-core/src/state.js +80 -0
  31. package/node_modules/pilotswarm-ui-core/src/store.js +23 -0
  32. package/node_modules/pilotswarm-ui-core/src/system-titles.js +24 -0
  33. package/node_modules/pilotswarm-ui-core/src/themes/catppuccin-mocha.js +56 -0
  34. package/node_modules/pilotswarm-ui-core/src/themes/cobalt2.js +56 -0
  35. package/node_modules/pilotswarm-ui-core/src/themes/dark-high-contrast.js +56 -0
  36. package/node_modules/pilotswarm-ui-core/src/themes/dracula.js +56 -0
  37. package/node_modules/pilotswarm-ui-core/src/themes/github-dark.js +56 -0
  38. package/node_modules/pilotswarm-ui-core/src/themes/gruvbox-dark.js +56 -0
  39. package/node_modules/pilotswarm-ui-core/src/themes/hacker-x-matrix.js +56 -0
  40. package/node_modules/pilotswarm-ui-core/src/themes/hacker-x-orion-prime.js +56 -0
  41. package/node_modules/pilotswarm-ui-core/src/themes/helpers.js +77 -0
  42. package/node_modules/pilotswarm-ui-core/src/themes/index.js +42 -0
  43. package/node_modules/pilotswarm-ui-core/src/themes/noctis-viola.js +56 -0
  44. package/node_modules/pilotswarm-ui-core/src/themes/noctis.js +56 -0
  45. package/node_modules/pilotswarm-ui-core/src/themes/nord.js +56 -0
  46. package/node_modules/pilotswarm-ui-core/src/themes/solarized-dark.js +56 -0
  47. package/node_modules/pilotswarm-ui-core/src/themes/tokyo-night.js +56 -0
  48. package/node_modules/pilotswarm-ui-react/README.md +5 -0
  49. package/node_modules/pilotswarm-ui-react/package.json +36 -0
  50. package/node_modules/pilotswarm-ui-react/src/components.js +1316 -0
  51. package/node_modules/pilotswarm-ui-react/src/index.js +4 -0
  52. package/node_modules/pilotswarm-ui-react/src/platform.js +15 -0
  53. package/node_modules/pilotswarm-ui-react/src/use-controller-state.js +38 -0
  54. package/node_modules/pilotswarm-ui-react/src/web-app.js +2661 -0
  55. package/package.json +64 -0
  56. package/runtime.js +146 -0
  57. package/server.js +311 -0
@@ -0,0 +1,2661 @@
1
+ import React from "react";
2
+ import {
3
+ UI_COMMANDS,
4
+ INSPECTOR_TABS,
5
+ appReducer,
6
+ computeLegacyLayout,
7
+ createInitialState,
8
+ createStore,
9
+ getPromptInputRows,
10
+ getTheme,
11
+ parseTerminalMarkupRuns,
12
+ PilotSwarmUiController,
13
+ selectActivityPane,
14
+ selectArtifactPickerModal,
15
+ selectArtifactUploadModal,
16
+ selectChatLines,
17
+ selectChatPaneChrome,
18
+ selectFileBrowserItems,
19
+ selectFilesFilterModal,
20
+ selectFilesScope,
21
+ selectFilesView,
22
+ selectHistoryFormatModal,
23
+ selectInspector,
24
+ selectLogFilterModal,
25
+ selectModelPickerModal,
26
+ selectRenameSessionModal,
27
+ selectSessionAgentPickerModal,
28
+ selectSessionRows,
29
+ selectStatusBar,
30
+ selectThemePickerModal,
31
+ } from "pilotswarm-ui-core";
32
+ import { useControllerSelector } from "./use-controller-state.js";
33
+
34
+ const MOBILE_BREAKPOINT = 920;
35
+ const GRID_CELL_WIDTH = 7;
36
+ const GRID_CELL_HEIGHT = 19;
37
+ const SCROLL_ROW_HEIGHT = 16;
38
+ const THEME_STORAGE_KEY = "pilotswarm.theme";
39
+ const THEME_COOKIE_NAME = "pilotswarm_theme";
40
+ const CHAT_FOCUS_MODE_STORAGE_KEY = "pilotswarm.chatFocus";
41
+ const INSPECTOR_TAB_LABELS = {
42
+ sequence: "Sequence",
43
+ logs: "Logs",
44
+ nodes: "Node Map",
45
+ history: "History",
46
+ files: "Files",
47
+ };
48
+
49
+ function cycleTabs(tabs, current, delta) {
50
+ const values = Array.isArray(tabs) ? tabs.filter(Boolean) : [];
51
+ if (values.length === 0) return current;
52
+ const index = values.indexOf(current);
53
+ const safeIndex = index === -1 ? 0 : index;
54
+ const nextIndex = (safeIndex + delta + values.length) % values.length;
55
+ return values[nextIndex];
56
+ }
57
+
58
+ function supportsBrowserFileUploads(controller) {
59
+ return typeof controller?.transport?.uploadArtifactFromFile === "function";
60
+ }
61
+
62
+ function supportsPathArtifactUploads(controller) {
63
+ return typeof controller?.transport?.uploadArtifactFromPath === "function";
64
+ }
65
+
66
+ function supportsArtifactBrowser(controller) {
67
+ return typeof controller?.transport?.listArtifacts === "function";
68
+ }
69
+
70
+ function supportsLocalFileOpen(controller) {
71
+ return typeof controller?.transport?.openPathInDefaultApp === "function";
72
+ }
73
+
74
+ function readStoredChatFocusMode() {
75
+ if (typeof window === "undefined") return false;
76
+ try {
77
+ return window.localStorage.getItem(CHAT_FOCUS_MODE_STORAGE_KEY) === "1";
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+
83
+ function writeStoredChatFocusMode(enabled) {
84
+ if (typeof window === "undefined") return;
85
+ try {
86
+ window.localStorage.setItem(CHAT_FOCUS_MODE_STORAGE_KEY, enabled ? "1" : "0");
87
+ } catch {
88
+ // Ignore localStorage failures in private or constrained environments.
89
+ }
90
+ }
91
+
92
+ function getVisibleInspectorTabs(controller) {
93
+ return supportsArtifactBrowser(controller)
94
+ ? INSPECTOR_TABS
95
+ : INSPECTOR_TABS.filter((tab) => tab !== "files");
96
+ }
97
+
98
+ function shallowEqualObject(left, right) {
99
+ if (Object.is(left, right)) return true;
100
+ if (!left || !right || typeof left !== "object" || typeof right !== "object") return false;
101
+ const leftKeys = Object.keys(left);
102
+ const rightKeys = Object.keys(right);
103
+ if (leftKeys.length !== rightKeys.length) return false;
104
+ for (const key of leftKeys) {
105
+ if (!Object.prototype.hasOwnProperty.call(right, key)) return false;
106
+ if (!Object.is(left[key], right[key])) return false;
107
+ }
108
+ return true;
109
+ }
110
+
111
+ function getStatePromptRows(state) {
112
+ const promptRows = Number(state?.ui?.promptRows);
113
+ return Number.isFinite(promptRows) && promptRows > 0
114
+ ? promptRows
115
+ : getPromptInputRows(state?.ui?.prompt || "");
116
+ }
117
+
118
+ function computeStateLayout(state) {
119
+ return computeLegacyLayout({
120
+ width: state.ui.layout?.viewportWidth ?? 120,
121
+ height: state.ui.layout?.viewportHeight ?? 40,
122
+ }, state.ui.layout?.paneAdjust ?? 0, getStatePromptRows(state), state.ui.layout?.sessionPaneAdjust ?? 0);
123
+ }
124
+
125
+ function normalizeLines(lines) {
126
+ const normalized = [];
127
+ for (const line of lines || []) {
128
+ if (line?.kind === "markup") {
129
+ for (const parsedLine of parseTerminalMarkupRuns(line.value || "")) {
130
+ normalized.push({ kind: "runs", runs: parsedLine });
131
+ }
132
+ continue;
133
+ }
134
+ if (Array.isArray(line)) {
135
+ normalized.push({ kind: "runs", runs: line });
136
+ continue;
137
+ }
138
+ normalized.push({ kind: "text", ...line });
139
+ }
140
+ return normalized;
141
+ }
142
+
143
+ function resolveColor(theme, token) {
144
+ if (!token) return undefined;
145
+ return theme?.tui?.[token] || theme?.terminal?.[token] || theme?.page?.[token] || token;
146
+ }
147
+
148
+ function runsToText(runs = []) {
149
+ return runs.map((run) => String(run?.text || "")).join("");
150
+ }
151
+
152
+ function flattenTitleText(title) {
153
+ if (Array.isArray(title)) return runsToText(title);
154
+ return String(title || "");
155
+ }
156
+
157
+ function compactTitleRuns(title, maxWidth = 40) {
158
+ if (!Array.isArray(title)) {
159
+ const text = String(title || "");
160
+ return [{ text: text.length > maxWidth ? `${text.slice(0, maxWidth - 1)}…` : text, color: "white", bold: true }];
161
+ }
162
+ const compactRuns = [];
163
+ let remaining = Math.max(8, maxWidth);
164
+ for (const run of title) {
165
+ if (remaining <= 0) break;
166
+ const color = run?.color;
167
+ if (color === "gray" && compactRuns.length > 0) continue;
168
+ const text = String(run?.text || "");
169
+ if (!text) continue;
170
+ const chunk = text.length > remaining && remaining > 1
171
+ ? `${text.slice(0, remaining - 1)}…`
172
+ : text.slice(0, remaining);
173
+ if (!chunk) continue;
174
+ compactRuns.push({ ...run, text: chunk });
175
+ remaining -= chunk.length;
176
+ }
177
+ return compactRuns.length > 0 ? compactRuns : title;
178
+ }
179
+
180
+ function applyDocumentTheme(themeId) {
181
+ const theme = getTheme(themeId);
182
+ if (!theme || typeof document === "undefined") return;
183
+ const root = document.documentElement;
184
+ root.style.setProperty("--ps-page-background", theme.page.background);
185
+ root.style.setProperty("--ps-page-foreground", theme.page.foreground);
186
+ root.style.setProperty("--ps-surface", theme.tui.surface);
187
+ root.style.setProperty("--ps-background", theme.tui.background);
188
+ root.style.setProperty("--ps-foreground", theme.tui.foreground);
189
+ root.style.setProperty("--ps-muted", theme.tui.gray);
190
+ root.style.setProperty("--ps-border", theme.tui.gray);
191
+ root.style.setProperty("--ps-selection-background", theme.tui.selectionBackground);
192
+ root.style.setProperty("--ps-selection-foreground", theme.tui.selectionForeground);
193
+ root.style.setProperty("--ps-highlight-background", theme.tui.activeHighlightBackground);
194
+ root.style.setProperty("--ps-highlight-foreground", theme.tui.activeHighlightForeground);
195
+ root.style.setProperty("--ps-modal-backdrop", theme.page.modalBackdrop);
196
+ root.style.setProperty("--ps-modal-background", theme.page.modalBackground);
197
+ root.style.setProperty("--ps-modal-border", theme.page.modalBorder);
198
+ root.style.setProperty("--ps-modal-foreground", theme.page.modalForeground);
199
+ root.style.setProperty("--ps-modal-muted", theme.page.modalMuted);
200
+ root.style.setProperty("--ps-modal-selected-background", theme.page.modalSelectedBackground);
201
+ root.style.setProperty("--ps-modal-selected-border", theme.page.modalSelectedBorder);
202
+ root.style.setProperty("--ps-modal-selected-foreground", theme.page.modalSelectedForeground);
203
+ }
204
+
205
+ function readStoredThemeId() {
206
+ try {
207
+ const cookieMatch = document.cookie.match(new RegExp(`(?:^|; )${THEME_COOKIE_NAME}=([^;]+)`));
208
+ if (cookieMatch?.[1]) {
209
+ return decodeURIComponent(cookieMatch[1]);
210
+ }
211
+ } catch {}
212
+ try {
213
+ return window.localStorage.getItem(THEME_STORAGE_KEY);
214
+ } catch {
215
+ return null;
216
+ }
217
+ }
218
+
219
+ function writeStoredThemeId(themeId) {
220
+ try {
221
+ document.cookie = `${THEME_COOKIE_NAME}=${encodeURIComponent(themeId)}; Path=/; Max-Age=31536000; SameSite=Lax`;
222
+ } catch {}
223
+ try {
224
+ window.localStorage.setItem(THEME_STORAGE_KEY, themeId);
225
+ } catch {}
226
+ }
227
+
228
+ function useMeasuredViewport(ref) {
229
+ const [viewport, setViewport] = React.useState({ width: 0, height: 0 });
230
+
231
+ React.useLayoutEffect(() => {
232
+ const element = ref.current;
233
+ if (!element) return undefined;
234
+
235
+ const update = () => {
236
+ setViewport({
237
+ width: element.clientWidth,
238
+ height: element.clientHeight,
239
+ });
240
+ };
241
+
242
+ update();
243
+ const observer = new ResizeObserver(update);
244
+ observer.observe(element);
245
+ window.addEventListener("resize", update);
246
+ return () => {
247
+ observer.disconnect();
248
+ window.removeEventListener("resize", update);
249
+ };
250
+ }, [ref]);
251
+
252
+ return viewport;
253
+ }
254
+
255
+ function computeGridViewport(viewport) {
256
+ const width = Math.max(320, viewport.width || window.innerWidth || 1280);
257
+ const height = Math.max(320, viewport.height || window.innerHeight || 800);
258
+ return {
259
+ width: Math.max(40, Math.floor(width / GRID_CELL_WIDTH)),
260
+ height: Math.max(18, Math.floor(height / GRID_CELL_HEIGHT)),
261
+ };
262
+ }
263
+
264
+ function useScrollSync(ref, lines, scrollOffset, scrollMode, paneKey, controller) {
265
+ const normalizedLines = React.useMemo(() => normalizeLines(lines), [lines]);
266
+
267
+ React.useLayoutEffect(() => {
268
+ const node = ref.current;
269
+ if (!node) return;
270
+ const maxScroll = Math.max(0, node.scrollHeight - node.clientHeight);
271
+ const offsetPixels = Math.max(0, Number(scrollOffset) || 0) * SCROLL_ROW_HEIGHT;
272
+ const nextScrollTop = scrollMode === "bottom"
273
+ ? Math.max(0, maxScroll - offsetPixels)
274
+ : Math.min(maxScroll, offsetPixels);
275
+ if (Math.abs(node.scrollTop - nextScrollTop) > 2) {
276
+ node.scrollTop = nextScrollTop;
277
+ }
278
+ }, [normalizedLines, ref, scrollMode, scrollOffset]);
279
+
280
+ const onScroll = React.useCallback(() => {
281
+ const node = ref.current;
282
+ if (!node || !paneKey) return;
283
+ const maxScroll = Math.max(0, node.scrollHeight - node.clientHeight);
284
+ const pixels = scrollMode === "bottom"
285
+ ? Math.max(0, maxScroll - node.scrollTop)
286
+ : Math.max(0, node.scrollTop);
287
+ controller.dispatch({
288
+ type: "ui/scroll",
289
+ pane: paneKey,
290
+ offset: pixels / SCROLL_ROW_HEIGHT,
291
+ });
292
+ }, [controller, paneKey, ref, scrollMode]);
293
+
294
+ return { normalizedLines, onScroll };
295
+ }
296
+
297
+ function Runs({ runs, theme }) {
298
+ return React.createElement(React.Fragment, null,
299
+ (runs || []).map((run, index) => React.createElement("span", {
300
+ key: `${index}:${run.text || ""}`,
301
+ style: {
302
+ color: resolveColor(theme, run.color),
303
+ backgroundColor: resolveColor(theme, run.backgroundColor),
304
+ fontWeight: run.bold ? 700 : 400,
305
+ textDecoration: run.underline ? "underline" : "none",
306
+ },
307
+ }, run.text || "")),
308
+ );
309
+ }
310
+
311
+ function SystemNoticeLine({ line, theme }) {
312
+ const body = String(line?.body || "").trim();
313
+ return React.createElement("details", { className: "ps-system-notice" },
314
+ React.createElement("summary", {
315
+ className: "ps-system-notice-summary",
316
+ style: { color: resolveColor(theme, line?.color) || "var(--ps-muted)" },
317
+ },
318
+ React.createElement("span", { className: "ps-system-notice-summary-text" }, line?.text || "System notice")),
319
+ body
320
+ ? React.createElement("div", { className: "ps-system-notice-body" },
321
+ React.createElement(MarkdownPreviewContent, { content: body, theme }))
322
+ : null);
323
+ }
324
+
325
+ function Line({ line, theme }) {
326
+ if (!line) {
327
+ return React.createElement("div", { className: "ps-line" }, " ");
328
+ }
329
+ if (line.kind === "systemNotice") {
330
+ return React.createElement(SystemNoticeLine, { line, theme });
331
+ }
332
+ if (line.kind === "runs") {
333
+ return React.createElement("div", { className: "ps-line" },
334
+ React.createElement(Runs, { runs: line.runs, theme }));
335
+ }
336
+ return React.createElement("div", {
337
+ className: "ps-line",
338
+ style: {
339
+ color: resolveColor(theme, line.color),
340
+ backgroundColor: resolveColor(theme, line.backgroundColor),
341
+ fontWeight: line.bold ? 700 : 400,
342
+ textDecoration: line.underline ? "underline" : "none",
343
+ },
344
+ }, line.text || " ");
345
+ }
346
+
347
+ function lineText(line) {
348
+ if (!line) return "";
349
+ if (line.kind === "runs") return runsToText(line.runs);
350
+ return String(line.text || "");
351
+ }
352
+
353
+ function usePanePixelScroll(ref, scrollOffset, paneKey, controller) {
354
+ React.useLayoutEffect(() => {
355
+ const node = ref.current;
356
+ if (!node) return;
357
+ const maxScroll = Math.max(0, node.scrollHeight - node.clientHeight);
358
+ const nextScrollTop = Math.min(maxScroll, Math.max(0, Number(scrollOffset) || 0) * SCROLL_ROW_HEIGHT);
359
+ if (Math.abs(node.scrollTop - nextScrollTop) > 2) {
360
+ node.scrollTop = nextScrollTop;
361
+ }
362
+ }, [ref, scrollOffset]);
363
+
364
+ return React.useCallback(() => {
365
+ const node = ref.current;
366
+ if (!node || !paneKey) return;
367
+ controller.dispatch({
368
+ type: "ui/scroll",
369
+ pane: paneKey,
370
+ offset: Math.max(0, node.scrollTop) / SCROLL_ROW_HEIGHT,
371
+ });
372
+ }, [controller, paneKey, ref]);
373
+ }
374
+
375
+ function tokenizeInlineMarkdown(source = "") {
376
+ const tokens = [];
377
+ const text = String(source || "");
378
+ const pattern = /(`[^`]+`)|(\[([^\]]+)\]\(([^)]+)\))|(\*\*([^*]+)\*\*)|(__(.+?)__)|(\*([^*]+)\*)|(_([^_]+)_)/g;
379
+ let lastIndex = 0;
380
+ let match;
381
+ while ((match = pattern.exec(text)) !== null) {
382
+ if (match.index > lastIndex) {
383
+ tokens.push({ type: "text", text: text.slice(lastIndex, match.index) });
384
+ }
385
+ if (match[1]) {
386
+ tokens.push({ type: "code", text: match[1].slice(1, -1) });
387
+ } else if (match[2]) {
388
+ tokens.push({ type: "link", text: match[3], href: match[4] });
389
+ } else if (match[5]) {
390
+ tokens.push({ type: "strong", text: match[6] });
391
+ } else if (match[7]) {
392
+ tokens.push({ type: "strong", text: match[8] });
393
+ } else if (match[9]) {
394
+ tokens.push({ type: "em", text: match[10] });
395
+ } else if (match[11]) {
396
+ tokens.push({ type: "em", text: match[12] });
397
+ }
398
+ lastIndex = pattern.lastIndex;
399
+ }
400
+ if (lastIndex < text.length) {
401
+ tokens.push({ type: "text", text: text.slice(lastIndex) });
402
+ }
403
+ return tokens;
404
+ }
405
+
406
+ function renderInlineMarkdown(source, theme, keyPrefix = "md") {
407
+ return tokenizeInlineMarkdown(source).map((token, index) => {
408
+ const key = `${keyPrefix}:${index}`;
409
+ if (token.type === "code") {
410
+ return React.createElement("code", { key, className: "ps-md-inline-code" }, token.text);
411
+ }
412
+ if (token.type === "strong") {
413
+ return React.createElement("strong", { key, className: "ps-md-strong" }, renderInlineMarkdown(token.text, theme, `${key}:strong`));
414
+ }
415
+ if (token.type === "em") {
416
+ return React.createElement("em", { key, className: "ps-md-em" }, renderInlineMarkdown(token.text, theme, `${key}:em`));
417
+ }
418
+ if (token.type === "link") {
419
+ return React.createElement("a", {
420
+ key,
421
+ className: "ps-md-link",
422
+ href: token.href,
423
+ target: "_blank",
424
+ rel: "noreferrer",
425
+ style: { color: resolveColor(theme, "cyan") },
426
+ }, token.text);
427
+ }
428
+ return React.createElement(React.Fragment, { key }, token.text);
429
+ });
430
+ }
431
+
432
+ function isMarkdownSpecialLine(line = "", nextLine = "") {
433
+ const value = String(line || "");
434
+ return /^\s*#{1,6}\s+/.test(value)
435
+ || /^\s*>/.test(value)
436
+ || /^\s*([-*]|\d+\.)\s+/.test(value)
437
+ || /^\s*```/.test(value)
438
+ || (value.includes("|") && /^\s*\|?[\s:-]+(?:\|[\s:-]+)+\|?\s*$/.test(String(nextLine || "")));
439
+ }
440
+
441
+ function splitMarkdownTableRow(line = "") {
442
+ const trimmed = String(line || "").trim().replace(/^\|/, "").replace(/\|$/, "");
443
+ return trimmed.split("|").map((cell) => cell.trim());
444
+ }
445
+
446
+ function parseMarkdownBlocks(source = "") {
447
+ const text = String(source || "").replace(/\r\n?/g, "\n");
448
+ const lines = text.split("\n");
449
+ const blocks = [];
450
+
451
+ for (let index = 0; index < lines.length;) {
452
+ const line = lines[index];
453
+ const trimmed = line.trim();
454
+
455
+ if (!trimmed) {
456
+ index += 1;
457
+ continue;
458
+ }
459
+
460
+ const fenceMatch = /^```(\S*)\s*$/.exec(trimmed);
461
+ if (fenceMatch) {
462
+ const language = fenceMatch[1] || "";
463
+ const codeLines = [];
464
+ index += 1;
465
+ while (index < lines.length && !/^```/.test(lines[index].trim())) {
466
+ codeLines.push(lines[index]);
467
+ index += 1;
468
+ }
469
+ if (index < lines.length) index += 1;
470
+ blocks.push({ type: "code", language, content: codeLines.join("\n") });
471
+ continue;
472
+ }
473
+
474
+ const headingMatch = /^(#{1,6})\s+(.*)$/.exec(line);
475
+ if (headingMatch) {
476
+ blocks.push({ type: "heading", level: headingMatch[1].length, text: headingMatch[2].trim() });
477
+ index += 1;
478
+ continue;
479
+ }
480
+
481
+ if (line.includes("|") && index + 1 < lines.length && /^\s*\|?[\s:-]+(?:\|[\s:-]+)+\|?\s*$/.test(lines[index + 1].trim())) {
482
+ const header = splitMarkdownTableRow(line);
483
+ index += 2;
484
+ const rows = [];
485
+ while (index < lines.length && lines[index].includes("|") && lines[index].trim()) {
486
+ rows.push(splitMarkdownTableRow(lines[index]));
487
+ index += 1;
488
+ }
489
+ blocks.push({ type: "table", header, rows });
490
+ continue;
491
+ }
492
+
493
+ if (/^\s*>/.test(line)) {
494
+ const quoteLines = [];
495
+ while (index < lines.length && /^\s*>/.test(lines[index])) {
496
+ quoteLines.push(lines[index].replace(/^\s*>\s?/, ""));
497
+ index += 1;
498
+ }
499
+ blocks.push({ type: "blockquote", text: quoteLines.join("\n").trim() });
500
+ continue;
501
+ }
502
+
503
+ const listMatch = /^(\s*)([-*]|\d+\.)\s+(.*)$/.exec(line);
504
+ if (listMatch) {
505
+ const ordered = /\d+\./.test(listMatch[2]);
506
+ const items = [];
507
+ while (index < lines.length) {
508
+ const current = lines[index];
509
+ const itemMatch = /^(\s*)([-*]|\d+\.)\s+(.*)$/.exec(current);
510
+ if (!itemMatch || /\d+\./.test(itemMatch[2]) !== ordered) break;
511
+ const itemLines = [itemMatch[3].trim()];
512
+ index += 1;
513
+ while (
514
+ index < lines.length
515
+ && lines[index].trim()
516
+ && !/^(\s*)([-*]|\d+\.)\s+/.test(lines[index])
517
+ && !isMarkdownSpecialLine(lines[index], lines[index + 1] || "")
518
+ ) {
519
+ itemLines.push(lines[index].trim());
520
+ index += 1;
521
+ }
522
+ items.push(itemLines.join(" "));
523
+ if (!lines[index]?.trim()) break;
524
+ }
525
+ blocks.push({ type: "list", ordered, items });
526
+ continue;
527
+ }
528
+
529
+ const paragraphLines = [line.trim()];
530
+ index += 1;
531
+ while (
532
+ index < lines.length
533
+ && lines[index].trim()
534
+ && !isMarkdownSpecialLine(lines[index], lines[index + 1] || "")
535
+ ) {
536
+ paragraphLines.push(lines[index].trim());
537
+ index += 1;
538
+ }
539
+ blocks.push({ type: "paragraph", text: paragraphLines.join(" ") });
540
+ }
541
+
542
+ return blocks;
543
+ }
544
+
545
+ function MarkdownPreviewContent({ content, theme }) {
546
+ const blocks = React.useMemo(() => parseMarkdownBlocks(content), [content]);
547
+ if (blocks.length === 0) {
548
+ return React.createElement("div", { className: "ps-empty-state" }, "No preview content.");
549
+ }
550
+ return React.createElement("div", { className: "ps-markdown-preview" },
551
+ blocks.map((block, index) => {
552
+ if (block.type === "heading") {
553
+ return React.createElement("div", {
554
+ key: `block:${index}`,
555
+ className: `ps-md-heading is-h${block.level}`,
556
+ }, renderInlineMarkdown(block.text, theme, `heading:${index}`));
557
+ }
558
+ if (block.type === "code") {
559
+ return React.createElement("section", { key: `block:${index}`, className: "ps-md-code-block" },
560
+ React.createElement("div", { className: "ps-md-code-header" }, block.language || "text"),
561
+ React.createElement("pre", { className: "ps-md-code-pre" },
562
+ React.createElement("code", null, block.content)));
563
+ }
564
+ if (block.type === "blockquote") {
565
+ return React.createElement("blockquote", { key: `block:${index}`, className: "ps-md-quote" },
566
+ renderInlineMarkdown(block.text, theme, `quote:${index}`));
567
+ }
568
+ if (block.type === "list") {
569
+ const ListTag = block.ordered ? "ol" : "ul";
570
+ return React.createElement(ListTag, {
571
+ key: `block:${index}`,
572
+ className: `ps-md-list${block.ordered ? " is-ordered" : ""}`,
573
+ }, block.items.map((item, itemIndex) => React.createElement("li", {
574
+ key: `item:${itemIndex}`,
575
+ className: "ps-md-list-item",
576
+ }, renderInlineMarkdown(item, theme, `list:${index}:${itemIndex}`))));
577
+ }
578
+ if (block.type === "table") {
579
+ return React.createElement("div", { key: `block:${index}`, className: "ps-md-table-wrap" },
580
+ React.createElement("table", { className: "ps-md-table" },
581
+ React.createElement("thead", null,
582
+ React.createElement("tr", null,
583
+ block.header.map((cell, cellIndex) => React.createElement("th", { key: `head:${cellIndex}` },
584
+ renderInlineMarkdown(cell, theme, `table:${index}:head:${cellIndex}`))))),
585
+ React.createElement("tbody", null,
586
+ block.rows.map((row, rowIndex) => React.createElement("tr", { key: `row:${rowIndex}` },
587
+ row.map((cell, cellIndex) => React.createElement("td", { key: `cell:${rowIndex}:${cellIndex}` },
588
+ renderInlineMarkdown(cell, theme, `table:${index}:${rowIndex}:${cellIndex}`))))))));
589
+ }
590
+ return React.createElement("p", { key: `block:${index}`, className: "ps-md-paragraph" },
591
+ renderInlineMarkdown(block.text, theme, `para:${index}`));
592
+ }));
593
+ }
594
+
595
+ function MarkdownPreviewPanel({ controller, title, color, focused, scrollOffset = 0, paneKey, theme, content }) {
596
+ const ref = React.useRef(null);
597
+ const onScroll = usePanePixelScroll(ref, scrollOffset, paneKey, controller);
598
+
599
+ return React.createElement(Panel, { title, color, focused, theme },
600
+ React.createElement("div", {
601
+ ref,
602
+ className: "ps-scroll-panel ps-markdown-scroll",
603
+ onScroll,
604
+ }, React.createElement(MarkdownPreviewContent, { content, theme })));
605
+ }
606
+
607
+ function isBoxTopLine(text) {
608
+ const value = String(text || "").trim();
609
+ return value.startsWith("┌") && value.endsWith("┐");
610
+ }
611
+
612
+ function isBoxBottomLine(text) {
613
+ const value = String(text || "").trim();
614
+ return value.startsWith("└") && value.endsWith("┘");
615
+ }
616
+
617
+ function isBoxDividerLine(text) {
618
+ const value = String(text || "").trim();
619
+ return value.startsWith("├") && value.endsWith("┤");
620
+ }
621
+
622
+ function isBoxContentLine(text) {
623
+ const value = String(text || "").trim();
624
+ return value.startsWith("│") && value.endsWith("│");
625
+ }
626
+
627
+ function extractCodeFenceLanguage(line) {
628
+ const value = String(lineText(line) || "").trim();
629
+ if (!isBoxTopLine(value)) return "";
630
+ return value
631
+ .slice(1, -1)
632
+ .replace(/^─+/u, "")
633
+ .replace(/─+$/u, "")
634
+ .trim();
635
+ }
636
+
637
+ function extractCodeFenceLine(line) {
638
+ if (line?.kind === "runs" && Array.isArray(line.runs) && line.runs.length >= 3) {
639
+ return String(line.runs[1]?.text || "").replace(/\s+$/u, "");
640
+ }
641
+ const value = lineText(line);
642
+ if (!isBoxContentLine(value)) return String(value || "");
643
+ return String(value)
644
+ .slice(1, -1)
645
+ .replace(/\s+$/u, "");
646
+ }
647
+
648
+ function trimRunsEdgeWhitespace(runs = []) {
649
+ const nextRuns = (runs || []).map((run) => ({ ...run }));
650
+ if (nextRuns.length === 0) return nextRuns;
651
+ nextRuns[0].text = String(nextRuns[0].text || "").replace(/^\s+/, "");
652
+ nextRuns[nextRuns.length - 1].text = String(nextRuns[nextRuns.length - 1].text || "").replace(/\s+$/, "");
653
+ return nextRuns.filter((run, index) => String(run.text || "").length > 0 || nextRuns.length === 1 || index === 0);
654
+ }
655
+
656
+ function extractFramedRuns(line, { fallbackColor = null } = {}) {
657
+ if (line?.kind === "runs" && Array.isArray(line.runs) && line.runs.length >= 3) {
658
+ return trimRunsEdgeWhitespace(line.runs.slice(1, -1));
659
+ }
660
+ const text = lineText(line)
661
+ .replace(/^\s*[┌│]\s?/, "")
662
+ .replace(/\s?[┐│]\s*$/, "")
663
+ .replace(/^─+/, "")
664
+ .replace(/─+$/, "")
665
+ .trim();
666
+ return [{ text, color: fallbackColor }];
667
+ }
668
+
669
+ function splitBoxTableCells(text) {
670
+ const value = String(text || "").trim();
671
+ if (!isBoxContentLine(value)) return [];
672
+ return value
673
+ .slice(1, -1)
674
+ .split("│")
675
+ .map((cell) => cell.trim());
676
+ }
677
+
678
+ function mergeBoxTableRowGroup(rowGroup = []) {
679
+ const columnCount = Math.max(0, ...rowGroup.map((row) => row.length));
680
+ return Array.from({ length: columnCount }, (_, columnIndex) => rowGroup
681
+ .map((row) => String(row[columnIndex] || "").trim())
682
+ .filter(Boolean)
683
+ .join(" "));
684
+ }
685
+
686
+ function parseStructuredChatBlocks(lines = []) {
687
+ const blocks = [];
688
+
689
+ for (let index = 0; index < lines.length;) {
690
+ const currentLine = lines[index];
691
+ const currentText = lineText(currentLine);
692
+
693
+ if (isBoxTopLine(currentText) && currentText.includes("┬")) {
694
+ const headerRows = [];
695
+ const bodyRows = [];
696
+ let currentRowGroup = [];
697
+ let inHeader = true;
698
+ index += 1;
699
+
700
+ while (index < lines.length) {
701
+ const nextLine = lines[index];
702
+ const nextText = lineText(nextLine);
703
+ if (isBoxBottomLine(nextText)) {
704
+ if (currentRowGroup.length > 0) {
705
+ const mergedRow = mergeBoxTableRowGroup(currentRowGroup);
706
+ if (mergedRow.length > 0) {
707
+ if (inHeader) headerRows.push(mergedRow);
708
+ else bodyRows.push(mergedRow);
709
+ }
710
+ }
711
+ index += 1;
712
+ break;
713
+ }
714
+ if (isBoxDividerLine(nextText)) {
715
+ if (currentRowGroup.length > 0) {
716
+ const mergedRow = mergeBoxTableRowGroup(currentRowGroup);
717
+ if (mergedRow.length > 0) {
718
+ if (inHeader) headerRows.push(mergedRow);
719
+ else bodyRows.push(mergedRow);
720
+ }
721
+ }
722
+ currentRowGroup = [];
723
+ inHeader = false;
724
+ index += 1;
725
+ continue;
726
+ }
727
+ if (isBoxContentLine(nextText)) {
728
+ const cells = splitBoxTableCells(nextText);
729
+ if (cells.length > 0) {
730
+ currentRowGroup.push(cells);
731
+ }
732
+ }
733
+ index += 1;
734
+ }
735
+
736
+ blocks.push({ type: "table", headerRows, bodyRows });
737
+ continue;
738
+ }
739
+
740
+ if (
741
+ currentLine?.kind === "runs"
742
+ && Array.isArray(currentLine.runs)
743
+ && currentLine.runs.length === 1
744
+ && isBoxTopLine(currentText)
745
+ ) {
746
+ const language = extractCodeFenceLanguage(currentLine);
747
+ const codeLines = [];
748
+ index += 1;
749
+
750
+ while (index < lines.length) {
751
+ const nextLine = lines[index];
752
+ const nextText = lineText(nextLine);
753
+ if (isBoxBottomLine(nextText)) {
754
+ index += 1;
755
+ break;
756
+ }
757
+ if (isBoxContentLine(nextText)) {
758
+ codeLines.push(extractCodeFenceLine(nextLine));
759
+ } else {
760
+ codeLines.push(lineText(nextLine));
761
+ }
762
+ index += 1;
763
+ }
764
+
765
+ if (index < lines.length && lineText(lines[index]).trim().length === 0) {
766
+ index += 1;
767
+ }
768
+
769
+ blocks.push({
770
+ type: "code",
771
+ language: language || "text",
772
+ content: codeLines.join("\n"),
773
+ });
774
+ continue;
775
+ }
776
+
777
+ if (
778
+ currentLine?.kind === "runs"
779
+ && Array.isArray(currentLine.runs)
780
+ && currentLine.runs.length > 2
781
+ && isBoxTopLine(currentText)
782
+ ) {
783
+ const headerRuns = extractFramedRuns(currentLine);
784
+ const borderColor = currentLine.runs[0]?.color || "gray";
785
+ const bodyLines = [];
786
+ index += 1;
787
+
788
+ while (index < lines.length) {
789
+ const nextLine = lines[index];
790
+ const nextText = lineText(nextLine);
791
+ if (isBoxBottomLine(nextText)) {
792
+ index += 1;
793
+ break;
794
+ }
795
+ if (isBoxContentLine(nextText)) {
796
+ bodyLines.push(extractFramedRuns(nextLine));
797
+ } else {
798
+ bodyLines.push(nextLine?.kind === "runs"
799
+ ? nextLine.runs
800
+ : [{ text: lineText(nextLine), color: nextLine?.color || null }]);
801
+ }
802
+ index += 1;
803
+ }
804
+
805
+ if (index < lines.length && lineText(lines[index]).trim().length === 0) {
806
+ index += 1;
807
+ }
808
+
809
+ blocks.push({ type: "card", headerRuns, bodyLines, borderColor });
810
+ continue;
811
+ }
812
+
813
+ blocks.push({ type: "line", line: currentLine });
814
+ index += 1;
815
+ }
816
+
817
+ return blocks;
818
+ }
819
+
820
+ function StructuredChatBlocks({ lines, theme }) {
821
+ const blocks = React.useMemo(() => parseStructuredChatBlocks(lines), [lines]);
822
+
823
+ return React.createElement(React.Fragment, null,
824
+ blocks.map((block, index) => {
825
+ if (block.type === "code") {
826
+ return React.createElement("section", { key: `code:${index}`, className: "ps-md-code-block ps-chat-code-block" },
827
+ React.createElement("div", { className: "ps-md-code-header" }, block.language || "text"),
828
+ React.createElement("pre", { className: "ps-md-code-pre" },
829
+ React.createElement("code", null, block.content || "")));
830
+ }
831
+
832
+ if (block.type === "card") {
833
+ return React.createElement("section", {
834
+ key: `card:${index}`,
835
+ className: "ps-chat-card",
836
+ style: { "--ps-chat-card-accent": resolveColor(theme, block.borderColor) || "var(--ps-border)" },
837
+ },
838
+ React.createElement("header", { className: "ps-chat-card-header" },
839
+ React.createElement(Runs, { runs: block.headerRuns, theme })),
840
+ React.createElement("div", { className: "ps-chat-card-body" },
841
+ (block.bodyLines || []).map((bodyRuns, bodyIndex) => React.createElement("div", {
842
+ key: `card:${index}:line:${bodyIndex}`,
843
+ className: "ps-chat-card-line",
844
+ }, React.createElement(Runs, { runs: bodyRuns, theme })) )));
845
+ }
846
+
847
+ if (block.type === "table") {
848
+ const headerRows = block.headerRows || [];
849
+ const bodyRows = block.bodyRows || [];
850
+ const columnCount = Math.max(
851
+ 1,
852
+ ...headerRows.map((row) => row.length),
853
+ ...bodyRows.map((row) => row.length),
854
+ );
855
+ return React.createElement("div", { key: `table:${index}`, className: "ps-chat-table-wrap" },
856
+ React.createElement("table", { className: "ps-chat-table" },
857
+ headerRows.length > 0
858
+ ? React.createElement("thead", null,
859
+ headerRows.map((row, rowIndex) => React.createElement("tr", { key: `thead:${rowIndex}` },
860
+ Array.from({ length: columnCount }, (_, cellIndex) => React.createElement("th", { key: `th:${rowIndex}:${cellIndex}` }, row[cellIndex] || "")))))
861
+ : null,
862
+ React.createElement("tbody", null,
863
+ bodyRows.map((row, rowIndex) => React.createElement("tr", { key: `tbody:${rowIndex}` },
864
+ Array.from({ length: columnCount }, (_, cellIndex) => React.createElement("td", { key: `td:${rowIndex}:${cellIndex}` }, row[cellIndex] || "")))))));
865
+ }
866
+
867
+ return React.createElement(Line, { key: `line:${index}`, line: block.line, theme });
868
+ }));
869
+ }
870
+
871
+ function Panel({ title, color = "gray", focused = false, actions = null, children, theme, className = "" }) {
872
+ const accent = resolveColor(theme, color);
873
+ return React.createElement("section", {
874
+ className: `ps-panel${focused ? " is-focused" : ""}${className ? ` ${className}` : ""}`,
875
+ style: { "--ps-panel-accent": accent || "var(--ps-border)" },
876
+ },
877
+ React.createElement("header", { className: "ps-panel-header" },
878
+ React.createElement("div", { className: "ps-panel-title" },
879
+ Array.isArray(title)
880
+ ? React.createElement(Runs, { runs: title, theme })
881
+ : flattenTitleText(title)),
882
+ actions ? React.createElement("div", { className: "ps-panel-actions" }, actions) : null,
883
+ ),
884
+ React.createElement("div", { className: "ps-panel-body" }, children));
885
+ }
886
+
887
+ function ScrollLinesPanel({ title, color, focused, actions, lines, stickyLines = [], scrollOffset = 0, scrollMode = "top", paneKey, controller, className = "", panelClassName = "", topContent = null, structuredBlocks = false }) {
888
+ const themeId = useControllerSelector(controller, (state) => state.ui.themeId);
889
+ const theme = getTheme(themeId);
890
+ const ref = React.useRef(null);
891
+ const stickyRef = React.useRef(null);
892
+ const syncingHorizontalRef = React.useRef(false);
893
+ const { normalizedLines, onScroll } = useScrollSync(ref, lines, scrollOffset, scrollMode, paneKey, controller);
894
+ const normalizedSticky = React.useMemo(() => normalizeLines(stickyLines), [stickyLines]);
895
+ const preserveHorizontalScroll = className.includes("is-preserve") && panelClassName.includes("has-preserved-sticky");
896
+
897
+ const syncScrollLeft = React.useCallback((source, target) => {
898
+ if (!source || !target) return;
899
+ if (Math.abs((target.scrollLeft || 0) - (source.scrollLeft || 0)) <= 1) return;
900
+ syncingHorizontalRef.current = true;
901
+ target.scrollLeft = source.scrollLeft;
902
+ window.requestAnimationFrame(() => {
903
+ syncingHorizontalRef.current = false;
904
+ });
905
+ }, []);
906
+
907
+ const handleBodyScroll = React.useCallback((event) => {
908
+ onScroll();
909
+ if (!preserveHorizontalScroll || syncingHorizontalRef.current) return;
910
+ syncScrollLeft(event.currentTarget, stickyRef.current);
911
+ }, [onScroll, preserveHorizontalScroll, syncScrollLeft]);
912
+
913
+ const handleStickyScroll = React.useCallback((event) => {
914
+ if (!preserveHorizontalScroll || syncingHorizontalRef.current) return;
915
+ syncScrollLeft(event.currentTarget, ref.current);
916
+ }, [preserveHorizontalScroll, syncScrollLeft]);
917
+
918
+ return React.createElement(Panel, { title, color, focused, actions, theme, className: panelClassName },
919
+ topContent,
920
+ normalizedSticky.length > 0
921
+ ? React.createElement("div", {
922
+ ref: stickyRef,
923
+ className: `ps-panel-sticky${preserveHorizontalScroll ? " is-scroll-sync" : ""}`,
924
+ onScroll: handleStickyScroll,
925
+ },
926
+ normalizedSticky.map((line, index) => React.createElement(Line, { key: `sticky:${index}`, line, theme })),
927
+ )
928
+ : null,
929
+ React.createElement("div", { ref, className: `ps-scroll-panel ${className}`.trim(), onScroll: handleBodyScroll },
930
+ structuredBlocks
931
+ ? React.createElement(StructuredChatBlocks, { lines: normalizedLines, theme })
932
+ : normalizedLines.map((line, index) => React.createElement(Line, { key: `line:${index}`, line, theme })),
933
+ ));
934
+ }
935
+
936
+ function SessionPane({ controller, actions = null, panelClassName = "" }) {
937
+ const themeId = useControllerSelector(controller, (state) => state.ui.themeId);
938
+ const theme = getTheme(themeId);
939
+ const sessionButtonRefs = React.useRef(new Map());
940
+ const viewState = useControllerSelector(controller, (state) => ({
941
+ activeSessionId: state.sessions.activeSessionId,
942
+ sessionsById: state.sessions.byId,
943
+ sessionsFlat: state.sessions.flat,
944
+ filterQuery: state.sessions.filterQuery || "",
945
+ connectionMode: state.connection?.mode || "local",
946
+ focused: state.ui.focusRegion === "sessions",
947
+ }), shallowEqualObject);
948
+ const rows = React.useMemo(() => selectSessionRows({
949
+ sessions: {
950
+ activeSessionId: viewState.activeSessionId,
951
+ byId: viewState.sessionsById,
952
+ flat: viewState.sessionsFlat,
953
+ filterQuery: viewState.filterQuery,
954
+ },
955
+ connection: {
956
+ mode: viewState.connectionMode,
957
+ },
958
+ }), [viewState.activeSessionId, viewState.connectionMode, viewState.filterQuery, viewState.sessionsById, viewState.sessionsFlat]);
959
+ const activeSession = viewState.activeSessionId
960
+ ? viewState.sessionsById[viewState.activeSessionId] || null
961
+ : null;
962
+ const canRenameActiveSession = Boolean(activeSession && !activeSession.isSystem);
963
+ const combinedPanelClassName = `ps-session-pane${panelClassName ? ` ${panelClassName}` : ""}`;
964
+ const setSessionButtonRef = React.useCallback((sessionId, node) => {
965
+ if (!sessionId) return;
966
+ if (node) {
967
+ sessionButtonRefs.current.set(sessionId, node);
968
+ } else {
969
+ sessionButtonRefs.current.delete(sessionId);
970
+ }
971
+ }, []);
972
+
973
+ React.useEffect(() => {
974
+ if (!viewState.focused || !viewState.activeSessionId) return;
975
+ const activeButton = sessionButtonRefs.current.get(viewState.activeSessionId);
976
+ if (!activeButton) return;
977
+
978
+ if (document.activeElement !== activeButton) {
979
+ activeButton.focus({ preventScroll: true });
980
+ }
981
+ activeButton.scrollIntoView({ block: "nearest" });
982
+ }, [rows, viewState.activeSessionId, viewState.focused]);
983
+
984
+ const panelActions = React.createElement(React.Fragment, null,
985
+ React.createElement("button", {
986
+ type: "button",
987
+ className: "ps-mini-button",
988
+ onClick: () => controller.handleCommand(UI_COMMANDS.OPEN_RENAME_SESSION).catch(() => {}),
989
+ disabled: !canRenameActiveSession,
990
+ }, "Rename"),
991
+ actions);
992
+
993
+ return React.createElement(Panel, {
994
+ title: [{ text: "Sessions", color: "yellow", bold: true }],
995
+ color: "yellow",
996
+ focused: viewState.focused,
997
+ theme,
998
+ actions: panelActions,
999
+ className: combinedPanelClassName,
1000
+ },
1001
+ React.createElement("div", { className: "ps-action-list ps-session-list" },
1002
+ rows.length === 0
1003
+ ? React.createElement("div", { className: "ps-empty-state" }, viewState.filterQuery
1004
+ ? `No sessions matched "@@${viewState.filterQuery}".`
1005
+ : "No sessions yet.")
1006
+ : rows.map((row) => React.createElement("button", {
1007
+ key: row.sessionId,
1008
+ type: "button",
1009
+ ref: (node) => setSessionButtonRef(row.sessionId, node),
1010
+ className: `ps-list-button ps-session-list-button${row.active ? " is-selected" : ""}`,
1011
+ tabIndex: row.active ? 0 : -1,
1012
+ "aria-selected": row.active ? "true" : "false",
1013
+ onClick: (event) => {
1014
+ const shouldToggleChildren = row.hasChildren && row.active && !event.metaKey && !event.ctrlKey;
1015
+ if (shouldToggleChildren) {
1016
+ controller.dispatch({
1017
+ type: row.collapsed ? "sessions/expand" : "sessions/collapse",
1018
+ sessionId: row.sessionId,
1019
+ });
1020
+ controller.setFocus("sessions");
1021
+ return;
1022
+ }
1023
+ controller.setFocus("sessions");
1024
+ if (!row.active) {
1025
+ controller.loadSession(row.sessionId).catch(() => {});
1026
+ }
1027
+ },
1028
+ },
1029
+ React.createElement("div", {
1030
+ className: "ps-line ps-session-row-content",
1031
+ style: { paddingInlineStart: `${Math.max(0, row.depth) * 18}px` },
1032
+ },
1033
+ Array.isArray(row.runs)
1034
+ ? React.createElement(Runs, { runs: row.runs, theme })
1035
+ : row.text),
1036
+ )),
1037
+ ));
1038
+ }
1039
+
1040
+ function ChatPane({ controller, mobile = false, fullWidth = false }) {
1041
+ const themeId = useControllerSelector(controller, (state) => state.ui.themeId);
1042
+ const theme = getTheme(themeId);
1043
+ const viewState = useControllerSelector(controller, (state) => {
1044
+ const activeSessionId = state.sessions.activeSessionId;
1045
+ const layout = computeStateLayout(state);
1046
+ const paneWidth = fullWidth || mobile
1047
+ ? layout.totalWidth
1048
+ : layout.leftWidth;
1049
+ const contentWidth = Math.max(20, paneWidth - 4);
1050
+ return {
1051
+ activeSessionId,
1052
+ activeHistory: activeSessionId ? state.history.bySessionId.get(activeSessionId) || null : null,
1053
+ branding: state.branding,
1054
+ connection: state.connection,
1055
+ sessionsById: state.sessions.byId,
1056
+ sessionsFlat: state.sessions.flat,
1057
+ inspectorTab: state.ui.inspectorTab,
1058
+ focused: state.ui.focusRegion === "chat",
1059
+ scroll: state.ui.scroll.chat,
1060
+ contentWidth,
1061
+ };
1062
+ }, shallowEqualObject);
1063
+ const selectorState = React.useMemo(() => ({
1064
+ branding: viewState.branding,
1065
+ connection: viewState.connection,
1066
+ sessions: {
1067
+ activeSessionId: viewState.activeSessionId,
1068
+ byId: viewState.sessionsById,
1069
+ flat: viewState.sessionsFlat,
1070
+ },
1071
+ history: {
1072
+ bySessionId: viewState.activeSessionId && viewState.activeHistory
1073
+ ? new Map([[viewState.activeSessionId, viewState.activeHistory]])
1074
+ : new Map(),
1075
+ },
1076
+ ui: {
1077
+ inspectorTab: viewState.inspectorTab,
1078
+ },
1079
+ }), [
1080
+ viewState.activeHistory,
1081
+ viewState.activeSessionId,
1082
+ viewState.branding,
1083
+ viewState.connection,
1084
+ viewState.inspectorTab,
1085
+ viewState.sessionsById,
1086
+ viewState.sessionsFlat,
1087
+ ]);
1088
+ const chrome = React.useMemo(() => selectChatPaneChrome(selectorState), [selectorState]);
1089
+ const lines = React.useMemo(
1090
+ () => selectChatLines(selectorState, viewState.contentWidth),
1091
+ [selectorState, viewState.contentWidth],
1092
+ );
1093
+
1094
+ return React.createElement(ScrollLinesPanel, {
1095
+ controller,
1096
+ title: mobile ? compactTitleRuns(chrome.title, 28) : chrome.title,
1097
+ color: chrome.color,
1098
+ focused: viewState.focused,
1099
+ lines,
1100
+ scrollOffset: viewState.scroll,
1101
+ scrollMode: "bottom",
1102
+ paneKey: "chat",
1103
+ className: "is-wrapped",
1104
+ panelClassName: "ps-chat-panel",
1105
+ structuredBlocks: true,
1106
+ });
1107
+ }
1108
+
1109
+ function MobileWorkspace({ controller, sessionsCollapsed, setSessionsCollapsed }) {
1110
+ const themeId = useControllerSelector(controller, (state) => state.ui.themeId);
1111
+ const theme = getTheme(themeId);
1112
+ const sessionToggle = React.createElement("button", {
1113
+ type: "button",
1114
+ className: "ps-mini-button",
1115
+ onClick: () => setSessionsCollapsed((current) => !current),
1116
+ }, sessionsCollapsed ? "Show" : "Hide");
1117
+
1118
+ return React.createElement("div", { className: "ps-mobile-workspace" },
1119
+ sessionsCollapsed
1120
+ ? React.createElement(Panel, {
1121
+ title: [{ text: "Sessions", color: "yellow", bold: true }],
1122
+ color: "yellow",
1123
+ focused: false,
1124
+ theme,
1125
+ actions: sessionToggle,
1126
+ className: "ps-mobile-session-collapsed",
1127
+ },
1128
+ React.createElement("div", { className: "ps-mobile-session-summary" }, "Session list collapsed."))
1129
+ : React.createElement(SessionPane, {
1130
+ controller,
1131
+ actions: sessionToggle,
1132
+ panelClassName: "ps-mobile-session-pane",
1133
+ }),
1134
+ React.createElement("div", { className: "ps-mobile-chat-pane" },
1135
+ React.createElement(ChatPane, { controller, mobile: true, fullWidth: true })));
1136
+ }
1137
+
1138
+ function InspectorTabs({ activeTab, controller }) {
1139
+ const visibleTabs = React.useMemo(() => getVisibleInspectorTabs(controller), [controller]);
1140
+ return React.createElement("div", { className: "ps-tab-row" },
1141
+ visibleTabs.map((tab) => React.createElement("button", {
1142
+ key: tab,
1143
+ type: "button",
1144
+ className: `ps-tab${activeTab === tab ? " is-active" : ""}`,
1145
+ title: `Switch to ${INSPECTOR_TAB_LABELS[tab] || tab}`,
1146
+ "aria-pressed": activeTab === tab,
1147
+ onClick: () => {
1148
+ controller.setFocus("inspector");
1149
+ controller.selectInspectorTab(tab).catch(() => {});
1150
+ },
1151
+ }, INSPECTOR_TAB_LABELS[tab] || tab)));
1152
+ }
1153
+
1154
+ function FilesPane({ controller, focused, mobile = false }) {
1155
+ const themeId = useControllerSelector(controller, (state) => state.ui.themeId);
1156
+ const theme = getTheme(themeId);
1157
+ const fileInputRef = React.useRef(null);
1158
+ const viewState = useControllerSelector(controller, (state) => {
1159
+ const activeSessionId = state.sessions.activeSessionId;
1160
+ const layout = computeStateLayout(state);
1161
+ const paneWidth = (mobile || state.files.fullscreen)
1162
+ ? (state.ui.layout?.viewportWidth ?? 120)
1163
+ : layout.rightWidth;
1164
+ return {
1165
+ activeSessionId,
1166
+ sessionsById: state.sessions.byId,
1167
+ sessionsFlat: state.sessions.flat,
1168
+ filesBySessionId: state.files.bySessionId,
1169
+ filesFilter: state.files.filter,
1170
+ selectedArtifactId: state.files.selectedArtifactId,
1171
+ focused,
1172
+ previewScroll: state.ui.scroll.filePreview,
1173
+ fullscreen: Boolean(state.files.fullscreen),
1174
+ contentWidth: Math.max(20, paneWidth - 4),
1175
+ canBrowserUpload: supportsBrowserFileUploads(controller),
1176
+ canPathUpload: supportsPathArtifactUploads(controller),
1177
+ canOpenLocally: supportsLocalFileOpen(controller),
1178
+ };
1179
+ }, shallowEqualObject);
1180
+ const selectorState = React.useMemo(() => ({
1181
+ sessions: {
1182
+ activeSessionId: viewState.activeSessionId,
1183
+ byId: viewState.sessionsById,
1184
+ flat: viewState.sessionsFlat,
1185
+ },
1186
+ files: {
1187
+ bySessionId: viewState.filesBySessionId,
1188
+ selectedArtifactId: viewState.selectedArtifactId,
1189
+ filter: viewState.filesFilter,
1190
+ fullscreen: viewState.fullscreen,
1191
+ },
1192
+ ui: {
1193
+ scroll: {
1194
+ filePreview: viewState.previewScroll,
1195
+ },
1196
+ },
1197
+ }), [
1198
+ viewState.activeSessionId,
1199
+ viewState.filesBySessionId,
1200
+ viewState.filesFilter,
1201
+ viewState.fullscreen,
1202
+ viewState.previewScroll,
1203
+ viewState.selectedArtifactId,
1204
+ viewState.sessionsById,
1205
+ viewState.sessionsFlat,
1206
+ ]);
1207
+ const filesView = React.useMemo(() => selectFilesView(selectorState, {
1208
+ listWidth: Math.max(18, viewState.contentWidth - 4),
1209
+ previewWidth: Math.max(18, viewState.contentWidth - 4),
1210
+ showHints: false,
1211
+ }), [selectorState, viewState.contentWidth]);
1212
+ const items = React.useMemo(() => selectFileBrowserItems(selectorState), [selectorState]);
1213
+ const hasSelection = items.length > 0;
1214
+
1215
+ const uploadFiles = React.useCallback((files) => {
1216
+ const nextFiles = Array.isArray(files) ? files.filter(Boolean) : [];
1217
+ if (nextFiles.length === 0) return;
1218
+ controller.uploadArtifactFiles(nextFiles).catch(() => {});
1219
+ }, [controller]);
1220
+
1221
+ const openUploadPicker = React.useCallback(() => {
1222
+ if (viewState.canBrowserUpload && fileInputRef.current) {
1223
+ fileInputRef.current.click();
1224
+ return;
1225
+ }
1226
+ if (viewState.canPathUpload) {
1227
+ controller.handleCommand(UI_COMMANDS.OPEN_ARTIFACT_UPLOAD).catch(() => {});
1228
+ }
1229
+ }, [controller, viewState.canBrowserUpload, viewState.canPathUpload]);
1230
+
1231
+ const panelActions = React.createElement(React.Fragment, null,
1232
+ React.createElement("input", {
1233
+ ref: fileInputRef,
1234
+ type: "file",
1235
+ className: "ps-hidden-file-input",
1236
+ multiple: true,
1237
+ tabIndex: -1,
1238
+ "aria-hidden": "true",
1239
+ onChange: (event) => {
1240
+ uploadFiles(Array.from(event.currentTarget.files || []));
1241
+ event.currentTarget.value = "";
1242
+ },
1243
+ }),
1244
+ React.createElement("button", {
1245
+ type: "button",
1246
+ className: "ps-mini-button",
1247
+ onClick: openUploadPicker,
1248
+ disabled: !viewState.canBrowserUpload && !viewState.canPathUpload,
1249
+ }, "Upload"),
1250
+ React.createElement("button", {
1251
+ type: "button",
1252
+ className: "ps-mini-button",
1253
+ onClick: () => controller.handleCommand(UI_COMMANDS.DOWNLOAD_SELECTED_FILE).catch(() => {}),
1254
+ disabled: !hasSelection,
1255
+ }, "Download"),
1256
+ viewState.canOpenLocally ? React.createElement("button", {
1257
+ type: "button",
1258
+ className: "ps-mini-button",
1259
+ onClick: () => controller.handleCommand(UI_COMMANDS.OPEN_SELECTED_FILE).catch(() => {}),
1260
+ disabled: !hasSelection,
1261
+ }, "Open") : null,
1262
+ React.createElement("button", {
1263
+ type: "button",
1264
+ className: "ps-mini-button",
1265
+ onClick: () => controller.handleCommand(UI_COMMANDS.OPEN_FILES_FILTER).catch(() => {}),
1266
+ }, "Filter"),
1267
+ React.createElement("button", {
1268
+ type: "button",
1269
+ className: "ps-mini-button",
1270
+ onClick: () => controller.handleCommand(UI_COMMANDS.TOGGLE_FILE_PREVIEW_FULLSCREEN).catch(() => {}),
1271
+ }, viewState.fullscreen ? "Close" : "Fullscreen"));
1272
+
1273
+ const listContent = items.length === 0
1274
+ ? normalizeLines(filesView.listBodyLines || []).map((line, index) => React.createElement(Line, {
1275
+ key: `empty:${index}`,
1276
+ line,
1277
+ theme,
1278
+ }))
1279
+ : items.map((item, index) => React.createElement("button", {
1280
+ key: item.id,
1281
+ type: "button",
1282
+ className: `ps-list-button${index === filesView.selectedIndex ? " is-selected" : ""}`,
1283
+ onClick: () => {
1284
+ controller.setFocus("inspector");
1285
+ controller.selectFileBrowserItem(item).catch(() => {});
1286
+ },
1287
+ }, React.createElement(Line, {
1288
+ line: normalizeLines([filesView.listBodyLines?.[index]])[0],
1289
+ theme,
1290
+ })));
1291
+
1292
+ const previewPane = filesView.previewRenderMode === "markdown"
1293
+ && !filesView.previewLoading
1294
+ && !filesView.previewError
1295
+ ? React.createElement(MarkdownPreviewPanel, {
1296
+ controller,
1297
+ title: filesView.previewTitle,
1298
+ color: "cyan",
1299
+ focused: false,
1300
+ scrollOffset: viewState.previewScroll,
1301
+ paneKey: "filePreview",
1302
+ theme,
1303
+ content: filesView.previewContent || "",
1304
+ })
1305
+ : React.createElement(ScrollLinesPanel, {
1306
+ controller,
1307
+ title: filesView.previewTitle,
1308
+ color: "cyan",
1309
+ focused: false,
1310
+ lines: filesView.previewLines,
1311
+ scrollOffset: viewState.previewScroll,
1312
+ scrollMode: "top",
1313
+ paneKey: "filePreview",
1314
+ className: "is-preview is-wrapped",
1315
+ });
1316
+ const view = viewState;
1317
+
1318
+ return React.createElement(Panel, {
1319
+ title: view.fullscreen ? filesView.fullscreenTitle : filesView.panelTitle,
1320
+ color: "magenta",
1321
+ focused: view.focused,
1322
+ actions: panelActions,
1323
+ theme,
1324
+ },
1325
+ React.createElement(InspectorTabs, { activeTab: "files", controller }),
1326
+ // Fullscreen files mode shows only the preview pane; the list stays hidden.
1327
+ view.fullscreen
1328
+ ? previewPane
1329
+ : React.createElement("div", { className: "ps-files-grid" },
1330
+ React.createElement(Panel, { title: filesView.listTitle, color: "cyan", theme },
1331
+ React.createElement("div", { className: "ps-action-list" }, listContent)),
1332
+ previewPane,
1333
+ ));
1334
+ }
1335
+
1336
+ function InspectorPane({ controller, mobile = false, panelClassName = "", extraActions = null }) {
1337
+ const viewState = useControllerSelector(controller, (state) => {
1338
+ const layout = computeStateLayout(state);
1339
+ const paneWidth = mobile
1340
+ ? (state.ui.layout?.viewportWidth ?? 120)
1341
+ : layout.rightWidth;
1342
+ return {
1343
+ inspectorTab: state.ui.inspectorTab,
1344
+ activeSessionId: state.sessions.activeSessionId,
1345
+ sessionsById: state.sessions.byId,
1346
+ sessionsFlat: state.sessions.flat,
1347
+ historyBySessionId: state.history.bySessionId,
1348
+ connection: state.connection,
1349
+ orchestrationBySessionId: state.orchestration.bySessionId,
1350
+ executionHistoryBySessionId: state.executionHistory?.bySessionId || {},
1351
+ executionHistoryFormat: state.executionHistory?.format || "pretty",
1352
+ logs: state.logs,
1353
+ files: state.files,
1354
+ focused: state.ui.focusRegion === "inspector",
1355
+ scroll: state.ui.scroll.inspector,
1356
+ logsTailing: state.logs.tailing,
1357
+ filesFullscreen: Boolean(state.files.fullscreen),
1358
+ contentWidth: Math.max(20, paneWidth - 4),
1359
+ };
1360
+ }, shallowEqualObject);
1361
+ const selectorState = React.useMemo(() => ({
1362
+ sessions: {
1363
+ activeSessionId: viewState.activeSessionId,
1364
+ byId: viewState.sessionsById,
1365
+ flat: viewState.sessionsFlat,
1366
+ },
1367
+ history: {
1368
+ bySessionId: viewState.historyBySessionId,
1369
+ },
1370
+ connection: viewState.connection,
1371
+ ui: {
1372
+ inspectorTab: viewState.inspectorTab,
1373
+ scroll: {
1374
+ inspector: viewState.scroll,
1375
+ },
1376
+ },
1377
+ logs: viewState.logs,
1378
+ files: viewState.files,
1379
+ orchestration: {
1380
+ bySessionId: viewState.orchestrationBySessionId,
1381
+ },
1382
+ executionHistory: {
1383
+ bySessionId: viewState.executionHistoryBySessionId,
1384
+ format: viewState.executionHistoryFormat,
1385
+ },
1386
+ }), [
1387
+ viewState.activeSessionId,
1388
+ viewState.connection,
1389
+ viewState.executionHistoryBySessionId,
1390
+ viewState.executionHistoryFormat,
1391
+ viewState.files,
1392
+ viewState.historyBySessionId,
1393
+ viewState.inspectorTab,
1394
+ viewState.logs,
1395
+ viewState.orchestrationBySessionId,
1396
+ viewState.scroll,
1397
+ viewState.sessionsById,
1398
+ viewState.sessionsFlat,
1399
+ ]);
1400
+ const inspector = React.useMemo(() => selectInspector(selectorState, {
1401
+ width: viewState.contentWidth,
1402
+ allowWideColumns: mobile,
1403
+ }), [mobile, selectorState, viewState.contentWidth]);
1404
+
1405
+ if (viewState.inspectorTab === "files") {
1406
+ return React.createElement(FilesPane, { controller, focused: viewState.focused, mobile });
1407
+ }
1408
+
1409
+ const actions = [];
1410
+ if (viewState.inspectorTab === "logs") {
1411
+ actions.push(React.createElement("button", {
1412
+ key: "tail",
1413
+ type: "button",
1414
+ className: "ps-mini-button",
1415
+ onClick: () => controller.handleCommand(UI_COMMANDS.TOGGLE_LOG_TAIL).catch(() => {}),
1416
+ }, viewState.logsTailing ? "Stop Tail" : "Tail"));
1417
+ actions.push(React.createElement("button", {
1418
+ key: "filter",
1419
+ type: "button",
1420
+ className: "ps-mini-button",
1421
+ onClick: () => controller.handleCommand(UI_COMMANDS.OPEN_LOG_FILTER).catch(() => {}),
1422
+ }, "Filter"));
1423
+ } else if (viewState.inspectorTab === "history") {
1424
+ actions.push(React.createElement("button", {
1425
+ key: "refresh",
1426
+ type: "button",
1427
+ className: "ps-mini-button",
1428
+ onClick: () => controller.handleCommand(UI_COMMANDS.REFRESH_EXECUTION_HISTORY).catch(() => {}),
1429
+ }, "Refresh"));
1430
+ actions.push(React.createElement("button", {
1431
+ key: "save",
1432
+ type: "button",
1433
+ className: "ps-mini-button",
1434
+ onClick: () => controller.handleCommand(UI_COMMANDS.EXPORT_EXECUTION_HISTORY).catch(() => {}),
1435
+ }, "Artifact"));
1436
+ }
1437
+
1438
+ const panelActions = extraActions
1439
+ ? actions.concat(extraActions)
1440
+ : actions;
1441
+
1442
+ return React.createElement(ScrollLinesPanel, {
1443
+ controller,
1444
+ title: inspector.title,
1445
+ color: "magenta",
1446
+ focused: viewState.focused,
1447
+ actions: panelActions,
1448
+ topContent: React.createElement(InspectorTabs, { activeTab: inspector.activeTab, controller }),
1449
+ stickyLines: inspector.stickyLines || [],
1450
+ lines: inspector.lines,
1451
+ scrollOffset: viewState.scroll,
1452
+ scrollMode: inspector.activeTab === "logs"
1453
+ || inspector.activeTab === "sequence"
1454
+ ? "bottom"
1455
+ : "top",
1456
+ paneKey: "inspector",
1457
+ className: inspector.activeTab === "history" ? "is-wrapped" : "is-preserve",
1458
+ panelClassName: `${inspector.activeTab === "sequence" ? "has-preserved-sticky" : ""}${panelClassName ? ` ${panelClassName}` : ""}`.trim(),
1459
+ });
1460
+ }
1461
+
1462
+ function ActivityPane({ controller, panelClassName = "", extraActions = null }) {
1463
+ const viewState = useControllerSelector(controller, (state) => {
1464
+ const activeSessionId = state.sessions.activeSessionId;
1465
+ const layout = computeStateLayout(state);
1466
+ const maxLines = Math.max(3, layout.activityPaneHeight - 2);
1467
+ return {
1468
+ activeSessionId,
1469
+ activeSession: activeSessionId ? state.sessions.byId[activeSessionId] || null : null,
1470
+ activeHistory: activeSessionId ? state.history.bySessionId.get(activeSessionId) || null : null,
1471
+ focused: state.ui.focusRegion === "activity",
1472
+ scroll: state.ui.scroll.activity,
1473
+ maxLines,
1474
+ };
1475
+ }, shallowEqualObject);
1476
+ const selectorState = React.useMemo(() => ({
1477
+ sessions: {
1478
+ activeSessionId: viewState.activeSessionId,
1479
+ byId: viewState.activeSessionId && viewState.activeSession
1480
+ ? { [viewState.activeSessionId]: viewState.activeSession }
1481
+ : {},
1482
+ },
1483
+ history: {
1484
+ bySessionId: viewState.activeSessionId && viewState.activeHistory
1485
+ ? new Map([[viewState.activeSessionId, viewState.activeHistory]])
1486
+ : new Map(),
1487
+ },
1488
+ }), [viewState.activeHistory, viewState.activeSession, viewState.activeSessionId]);
1489
+ const activity = React.useMemo(
1490
+ () => selectActivityPane(selectorState, viewState.maxLines),
1491
+ [selectorState, viewState.maxLines],
1492
+ );
1493
+
1494
+ return React.createElement(ScrollLinesPanel, {
1495
+ controller,
1496
+ title: activity.title,
1497
+ color: "gray",
1498
+ focused: viewState.focused,
1499
+ actions: extraActions,
1500
+ lines: activity.lines,
1501
+ scrollOffset: viewState.scroll,
1502
+ scrollMode: "bottom",
1503
+ paneKey: "activity",
1504
+ className: "is-preserve",
1505
+ panelClassName,
1506
+ });
1507
+ }
1508
+
1509
+ const CHAT_FOCUS_PANES = [
1510
+ { id: "sessions", label: "Sessions", side: "left" },
1511
+ { id: "inspector", label: "Inspector", side: "right" },
1512
+ { id: "activity", label: "Activity", side: "right" },
1513
+ ];
1514
+
1515
+ function ChatFocusOverlay({ controller, pane, onClose }) {
1516
+ if (!pane) return null;
1517
+
1518
+ let content = null;
1519
+ if (pane === "sessions") {
1520
+ content = React.createElement(SessionPane, {
1521
+ controller,
1522
+ panelClassName: "ps-chat-focus-pane",
1523
+ actions: React.createElement("button", {
1524
+ type: "button",
1525
+ className: "ps-mini-button",
1526
+ onClick: onClose,
1527
+ }, "Close"),
1528
+ });
1529
+ } else if (pane === "inspector") {
1530
+ content = React.createElement(InspectorPane, {
1531
+ controller,
1532
+ mobile: false,
1533
+ panelClassName: "ps-chat-focus-pane",
1534
+ extraActions: React.createElement("button", {
1535
+ type: "button",
1536
+ className: "ps-mini-button",
1537
+ onClick: onClose,
1538
+ }, "Close"),
1539
+ });
1540
+ } else if (pane === "activity") {
1541
+ content = React.createElement(ActivityPane, {
1542
+ controller,
1543
+ panelClassName: "ps-chat-focus-pane",
1544
+ extraActions: React.createElement("button", {
1545
+ type: "button",
1546
+ className: "ps-mini-button",
1547
+ onClick: onClose,
1548
+ }, "Close"),
1549
+ });
1550
+ }
1551
+
1552
+ const paneMeta = CHAT_FOCUS_PANES.find((entry) => entry.id === pane);
1553
+ return React.createElement("div", {
1554
+ className: `ps-chat-focus-overlay${paneMeta?.side === "left" ? " is-left" : " is-right"}`,
1555
+ }, content);
1556
+ }
1557
+
1558
+ function ChatFocusWorkspace({ controller, openPane, onTogglePane, mobile = false }) {
1559
+ const focusRegion = useControllerSelector(controller, (state) => state.ui.focusRegion);
1560
+
1561
+ return React.createElement("div", { className: "ps-chat-focus-shell" },
1562
+ React.createElement("div", { className: "ps-chat-focus-rail" },
1563
+ CHAT_FOCUS_PANES.map((pane) => React.createElement("button", {
1564
+ key: pane.id,
1565
+ type: "button",
1566
+ className: `ps-mini-button ps-chat-focus-button${openPane === pane.id ? " is-active" : ""}`,
1567
+ "aria-pressed": openPane === pane.id ? "true" : "false",
1568
+ onClick: () => onTogglePane(pane.id),
1569
+ }, pane.label)),
1570
+ React.createElement("div", { className: "ps-chat-focus-status" },
1571
+ openPane
1572
+ ? `Focused: ${CHAT_FOCUS_PANES.find((pane) => pane.id === openPane)?.label || openPane}`
1573
+ : `Focused: ${focusRegion === "prompt" ? "Prompt" : "Chat"}`)),
1574
+ React.createElement("div", { className: "ps-chat-focus-body" },
1575
+ React.createElement(ChatPane, { controller, mobile, fullWidth: true }),
1576
+ React.createElement(ChatFocusOverlay, {
1577
+ controller,
1578
+ pane: openPane,
1579
+ onClose: () => onTogglePane(openPane),
1580
+ })));
1581
+ }
1582
+
1583
+ function PromptComposer({ controller, mobile, active = true, onAfterSend = null }) {
1584
+ const promptState = useControllerSelector(controller, (state) => {
1585
+ const activeSessionId = state.sessions.activeSessionId;
1586
+ const activeSession = activeSessionId ? state.sessions.byId[activeSessionId] || null : null;
1587
+ return {
1588
+ value: state.ui.prompt,
1589
+ cursor: state.ui.promptCursor,
1590
+ focused: state.ui.focusRegion === "prompt",
1591
+ answerMode: Boolean(activeSession?.pendingQuestion?.question),
1592
+ };
1593
+ }, shallowEqualObject);
1594
+ const inputRef = React.useRef(null);
1595
+
1596
+ React.useEffect(() => {
1597
+ const inputNode = inputRef.current;
1598
+ if (!active || !promptState.focused || !inputNode) return;
1599
+ if (document.activeElement !== inputNode) {
1600
+ try {
1601
+ inputNode.focus({ preventScroll: true });
1602
+ } catch {
1603
+ inputNode.focus();
1604
+ }
1605
+ }
1606
+ inputNode.setSelectionRange(promptState.cursor, promptState.cursor);
1607
+ }, [active, promptState.cursor, promptState.focused]);
1608
+
1609
+ const sendPrompt = React.useCallback(() => {
1610
+ controller.handleCommand(UI_COMMANDS.SEND_PROMPT)
1611
+ .catch(() => {})
1612
+ .finally(() => {
1613
+ onAfterSend?.();
1614
+ });
1615
+ }, [controller, onAfterSend]);
1616
+
1617
+ return React.createElement("div", {
1618
+ className: `ps-prompt-shell${mobile ? " is-mobile" : ""}`,
1619
+ },
1620
+ React.createElement("label", { className: "ps-prompt-label" }, promptState.answerMode ? "answer" : "you"),
1621
+ React.createElement("textarea", {
1622
+ ref: inputRef,
1623
+ className: "ps-prompt-input",
1624
+ rows: mobile ? 2 : Math.max(2, getPromptInputRows(promptState.value)),
1625
+ value: promptState.value,
1626
+ placeholder: promptState.answerMode
1627
+ ? "Type an answer and press Enter"
1628
+ : "Type a message and press Enter",
1629
+ enterKeyHint: "send",
1630
+ onFocus: () => controller.setFocus("prompt"),
1631
+ onSelect: (event) => controller.setPrompt(
1632
+ event.currentTarget.value,
1633
+ event.currentTarget.selectionStart || 0,
1634
+ ),
1635
+ onChange: (event) => controller.setPrompt(event.currentTarget.value, event.currentTarget.selectionStart || event.currentTarget.value.length),
1636
+ onKeyDown: (event) => {
1637
+ if (event.key === "Tab" && !event.shiftKey && controller.acceptPromptReferenceAutocomplete()) {
1638
+ event.preventDefault();
1639
+ return;
1640
+ }
1641
+ if (event.key === "Enter" && !event.shiftKey && !event.metaKey && !mobile) {
1642
+ event.preventDefault();
1643
+ sendPrompt();
1644
+ }
1645
+ },
1646
+ }),
1647
+ React.createElement("button", {
1648
+ type: "button",
1649
+ className: `ps-send-button${mobile ? " is-inline" : ""}`,
1650
+ title: "Send prompt",
1651
+ "aria-label": "Send prompt",
1652
+ onClick: sendPrompt,
1653
+ }, mobile ? "↩" : "Send"),
1654
+ );
1655
+ }
1656
+
1657
+ function StatusStrip({ controller }) {
1658
+ const status = useControllerSelector(controller, (state) => selectStatusBar(state), shallowEqualObject);
1659
+ return React.createElement("div", { className: "ps-status-strip" },
1660
+ React.createElement("div", { className: "ps-status-left" }, status.left),
1661
+ React.createElement("div", { className: "ps-status-right" }, status.right),
1662
+ );
1663
+ }
1664
+
1665
+ function buildPortalKeybindingSections({ canUpload, canOpenLocally }) {
1666
+ return [
1667
+ {
1668
+ title: "Global",
1669
+ items: [
1670
+ ["n", "New session"],
1671
+ ["Shift+N", "New session with model"],
1672
+ ["T", "Theme picker"],
1673
+ ["Tab / Shift+Tab", "Cycle focus"],
1674
+ ["[ / ]", "Resize side panes"],
1675
+ ["{ / }", "Resize session list vertically"],
1676
+ ["?", "Toggle this legend"],
1677
+ ],
1678
+ },
1679
+ {
1680
+ title: "Navigation",
1681
+ items: [
1682
+ ["j / k", "Move or scroll the focused pane"],
1683
+ ["Ctrl+U / Ctrl+D", "Page up/down"],
1684
+ ["g / G", "Jump top/bottom"],
1685
+ ["m", "Cycle inspector tabs"],
1686
+ ["p", "Focus prompt"],
1687
+ ["Esc", "Focus sessions"],
1688
+ ],
1689
+ },
1690
+ {
1691
+ title: "Prompt",
1692
+ items: [
1693
+ ["Enter", "Send prompt"],
1694
+ ["Tab", "Accept @ / @@ autocomplete"],
1695
+ ["@", "Browse this session's artifacts and attach the selection"],
1696
+ ["@@", "Filter sessions and insert a durable session reference"],
1697
+ ],
1698
+ },
1699
+ {
1700
+ title: "Files",
1701
+ items: [
1702
+ [canUpload ? "u / Ctrl+A" : "u", "Upload artifact to the active session"],
1703
+ ["a", "Download selected artifact"],
1704
+ ...(canOpenLocally ? [["o", "Open downloaded file locally"]] : []),
1705
+ ["f", "Filter the artifact browser"],
1706
+ ["v", "Toggle fullscreen preview"],
1707
+ ],
1708
+ },
1709
+ ];
1710
+ }
1711
+
1712
+ function KeybindingLegend({ open, onClose, canUpload, canOpenLocally }) {
1713
+ if (!open) return null;
1714
+ const sections = buildPortalKeybindingSections({ canUpload, canOpenLocally });
1715
+ return React.createElement("div", { className: "ps-modal-backdrop", onClick: onClose },
1716
+ React.createElement("div", {
1717
+ className: "ps-modal is-wide ps-keybinding-modal",
1718
+ role: "dialog",
1719
+ "aria-modal": "true",
1720
+ "aria-label": "Keyboard shortcuts",
1721
+ onClick: (event) => event.stopPropagation(),
1722
+ },
1723
+ React.createElement("div", { className: "ps-modal-header" },
1724
+ React.createElement("div", { className: "ps-modal-title" }, "Keyboard Shortcuts"),
1725
+ React.createElement("button", { type: "button", className: "ps-modal-close", onClick: onClose }, "Close")),
1726
+ React.createElement("div", { className: "ps-keybinding-grid" },
1727
+ sections.map((section) => React.createElement("section", { key: section.title, className: "ps-keybinding-section" },
1728
+ React.createElement("h3", { className: "ps-keybinding-title" }, section.title),
1729
+ React.createElement("div", { className: "ps-keybinding-list" },
1730
+ section.items.map(([binding, description]) => React.createElement("div", { key: `${section.title}:${binding}`, className: "ps-keybinding-row" },
1731
+ React.createElement("kbd", { className: "ps-keybinding-kbd" }, binding),
1732
+ React.createElement("span", { className: "ps-keybinding-description" }, description)))))),
1733
+ ),
1734
+ React.createElement("div", { className: "ps-modal-footer" },
1735
+ React.createElement("button", { type: "button", className: "ps-modal-button is-primary", onClick: onClose }, "Done"))));
1736
+ }
1737
+
1738
+ function PromptOverlay({ controller, open, onClose }) {
1739
+ if (!open) return null;
1740
+ return React.createElement("div", {
1741
+ className: "ps-modal-backdrop ps-compose-backdrop",
1742
+ onClick: onClose,
1743
+ },
1744
+ React.createElement("div", {
1745
+ className: "ps-compose-card",
1746
+ role: "dialog",
1747
+ "aria-modal": "true",
1748
+ "aria-label": "Compose prompt",
1749
+ onClick: (event) => event.stopPropagation(),
1750
+ },
1751
+ React.createElement("div", { className: "ps-compose-header" },
1752
+ React.createElement("div", { className: "ps-modal-title" }, "Prompt"),
1753
+ React.createElement("button", {
1754
+ type: "button",
1755
+ className: "ps-modal-close",
1756
+ onClick: onClose,
1757
+ }, "Close")),
1758
+ React.createElement(PromptComposer, {
1759
+ controller,
1760
+ mobile: false,
1761
+ active: open,
1762
+ onAfterSend: onClose,
1763
+ })));
1764
+ }
1765
+
1766
+ function Toolbar({ controller, mobile, onToggleLegend, onOpenPrompt, chatFocusMode = false, onToggleChatFocus = null, chatFocusDisabled = false }) {
1767
+ const status = useControllerSelector(controller, (state) => selectStatusBar(state), shallowEqualObject);
1768
+ const canRename = useControllerSelector(
1769
+ controller,
1770
+ (state) => Boolean(state.sessions.activeSessionId && !state.sessions.byId[state.sessions.activeSessionId]?.isSystem),
1771
+ );
1772
+
1773
+ return React.createElement("div", { className: `ps-toolbar${mobile ? " is-mobile" : ""}` },
1774
+ React.createElement("div", { className: "ps-toolbar-actions" },
1775
+ React.createElement("button", {
1776
+ type: "button",
1777
+ className: "ps-toolbar-button",
1778
+ onClick: () => controller.handleCommand(UI_COMMANDS.NEW_SESSION).catch(() => {}),
1779
+ }, "New"),
1780
+ React.createElement("button", {
1781
+ type: "button",
1782
+ className: "ps-toolbar-button",
1783
+ onClick: () => controller.handleCommand(UI_COMMANDS.OPEN_MODEL_PICKER).catch(() => {}),
1784
+ }, mobile ? "Model" : "New + Model"),
1785
+ React.createElement("button", {
1786
+ type: "button",
1787
+ className: "ps-toolbar-button",
1788
+ onClick: onOpenPrompt,
1789
+ }, "Prompt"),
1790
+ React.createElement("button", {
1791
+ type: "button",
1792
+ className: "ps-toolbar-button",
1793
+ onClick: () => controller.handleCommand(UI_COMMANDS.OPEN_RENAME_SESSION).catch(() => {}),
1794
+ disabled: !canRename,
1795
+ }, mobile ? "Title" : "Rename"),
1796
+ React.createElement("button", {
1797
+ type: "button",
1798
+ className: "ps-toolbar-button",
1799
+ onClick: () => controller.handleCommand(UI_COMMANDS.REFRESH).catch(() => {}),
1800
+ }, "Refresh"),
1801
+ React.createElement("button", {
1802
+ type: "button",
1803
+ className: "ps-toolbar-button",
1804
+ onClick: () => controller.handleCommand(UI_COMMANDS.OPEN_THEME_PICKER).catch(() => {}),
1805
+ }, "Theme"),
1806
+ onToggleChatFocus ? React.createElement("button", {
1807
+ type: "button",
1808
+ className: `ps-toolbar-button${chatFocusMode ? " is-active" : ""}`,
1809
+ onClick: onToggleChatFocus,
1810
+ disabled: chatFocusDisabled,
1811
+ }, mobile
1812
+ ? (chatFocusMode ? "Exit Focus" : "Focus")
1813
+ : (chatFocusMode ? "Exit Focus" : "Chat Focus")) : null,
1814
+ React.createElement("button", {
1815
+ type: "button",
1816
+ className: "ps-toolbar-button",
1817
+ onClick: onToggleLegend,
1818
+ }, "Keys")),
1819
+ status.left
1820
+ ? React.createElement("div", { className: "ps-toolbar-status" }, status.left)
1821
+ : null,
1822
+ );
1823
+ }
1824
+
1825
+ function ColumnResizeHandle({ controller, paneAdjust = 0 }) {
1826
+ const dragStateRef = React.useRef(null);
1827
+ const [dragging, setDragging] = React.useState(false);
1828
+
1829
+ React.useEffect(() => {
1830
+ if (!dragging) return undefined;
1831
+
1832
+ const stopDragging = () => {
1833
+ dragStateRef.current = null;
1834
+ setDragging(false);
1835
+ document.body.classList.remove("is-resizing-pane-x");
1836
+ };
1837
+
1838
+ const onPointerMove = (event) => {
1839
+ const dragState = dragStateRef.current;
1840
+ if (!dragState) return;
1841
+ const deltaCells = Math.round((event.clientX - dragState.startX) / GRID_CELL_WIDTH);
1842
+ const deltaIncrement = deltaCells - dragState.appliedCells;
1843
+ if (!deltaIncrement) return;
1844
+ controller.adjustPaneSplit(deltaIncrement);
1845
+ dragState.appliedCells = deltaCells;
1846
+ };
1847
+
1848
+ window.addEventListener("pointermove", onPointerMove);
1849
+ window.addEventListener("pointerup", stopDragging);
1850
+ window.addEventListener("pointercancel", stopDragging);
1851
+
1852
+ return () => {
1853
+ window.removeEventListener("pointermove", onPointerMove);
1854
+ window.removeEventListener("pointerup", stopDragging);
1855
+ window.removeEventListener("pointercancel", stopDragging);
1856
+ document.body.classList.remove("is-resizing-pane-x");
1857
+ };
1858
+ }, [controller, dragging]);
1859
+
1860
+ return React.createElement("button", {
1861
+ type: "button",
1862
+ className: `ps-column-resizer${dragging ? " is-dragging" : ""}`,
1863
+ title: "Drag to resize the inspector column. Double-click to reset.",
1864
+ "aria-label": "Resize inspector column",
1865
+ onPointerDown: (event) => {
1866
+ if (event.button !== 0) return;
1867
+ event.preventDefault();
1868
+ dragStateRef.current = {
1869
+ startX: event.clientX,
1870
+ appliedCells: 0,
1871
+ };
1872
+ setDragging(true);
1873
+ document.body.classList.add("is-resizing-pane-x");
1874
+ },
1875
+ onDoubleClick: () => {
1876
+ if (!paneAdjust) return;
1877
+ controller.adjustPaneSplit(-paneAdjust);
1878
+ },
1879
+ onKeyDown: (event) => {
1880
+ if (event.key === "ArrowLeft") {
1881
+ event.preventDefault();
1882
+ controller.handleCommand(UI_COMMANDS.GROW_RIGHT_PANE).catch(() => {});
1883
+ return;
1884
+ }
1885
+ if (event.key === "ArrowRight") {
1886
+ event.preventDefault();
1887
+ controller.handleCommand(UI_COMMANDS.GROW_LEFT_PANE).catch(() => {});
1888
+ }
1889
+ },
1890
+ },
1891
+ React.createElement("span", { className: "ps-column-resizer-handle", "aria-hidden": "true" },
1892
+ React.createElement("span", { className: "ps-column-resizer-dot" }),
1893
+ React.createElement("span", { className: "ps-column-resizer-dot" }),
1894
+ React.createElement("span", { className: "ps-column-resizer-dot" })));
1895
+ }
1896
+
1897
+ function RowResizeHandle({ controller, sessionPaneAdjust = 0 }) {
1898
+ const dragStateRef = React.useRef(null);
1899
+ const [dragging, setDragging] = React.useState(false);
1900
+
1901
+ React.useEffect(() => {
1902
+ if (!dragging) return undefined;
1903
+
1904
+ const stopDragging = () => {
1905
+ dragStateRef.current = null;
1906
+ setDragging(false);
1907
+ document.body.classList.remove("is-resizing-pane-y");
1908
+ };
1909
+
1910
+ const onPointerMove = (event) => {
1911
+ const dragState = dragStateRef.current;
1912
+ if (!dragState) return;
1913
+ const deltaCells = Math.round((event.clientY - dragState.startY) / GRID_CELL_HEIGHT);
1914
+ const deltaIncrement = deltaCells - dragState.appliedCells;
1915
+ if (!deltaIncrement) return;
1916
+ controller.adjustSessionPaneSplit(deltaIncrement);
1917
+ dragState.appliedCells = deltaCells;
1918
+ };
1919
+
1920
+ window.addEventListener("pointermove", onPointerMove);
1921
+ window.addEventListener("pointerup", stopDragging);
1922
+ window.addEventListener("pointercancel", stopDragging);
1923
+
1924
+ return () => {
1925
+ window.removeEventListener("pointermove", onPointerMove);
1926
+ window.removeEventListener("pointerup", stopDragging);
1927
+ window.removeEventListener("pointercancel", stopDragging);
1928
+ document.body.classList.remove("is-resizing-pane-y");
1929
+ };
1930
+ }, [controller, dragging]);
1931
+
1932
+ return React.createElement("button", {
1933
+ type: "button",
1934
+ className: `ps-row-resizer${dragging ? " is-dragging" : ""}`,
1935
+ title: "Drag to resize the session list. Double-click to reset.",
1936
+ "aria-label": "Resize session list",
1937
+ onPointerDown: (event) => {
1938
+ if (event.button !== 0) return;
1939
+ event.preventDefault();
1940
+ dragStateRef.current = {
1941
+ startY: event.clientY,
1942
+ appliedCells: 0,
1943
+ };
1944
+ setDragging(true);
1945
+ document.body.classList.add("is-resizing-pane-y");
1946
+ },
1947
+ onDoubleClick: () => {
1948
+ if (!sessionPaneAdjust) return;
1949
+ controller.adjustSessionPaneSplit(-sessionPaneAdjust);
1950
+ },
1951
+ onKeyDown: (event) => {
1952
+ if (event.key === "ArrowUp") {
1953
+ event.preventDefault();
1954
+ controller.adjustSessionPaneSplit(-1);
1955
+ return;
1956
+ }
1957
+ if (event.key === "ArrowDown") {
1958
+ event.preventDefault();
1959
+ controller.adjustSessionPaneSplit(1);
1960
+ }
1961
+ },
1962
+ },
1963
+ React.createElement("span", { className: "ps-row-resizer-handle", "aria-hidden": "true" },
1964
+ React.createElement("span", { className: "ps-row-resizer-dot" }),
1965
+ React.createElement("span", { className: "ps-row-resizer-dot" }),
1966
+ React.createElement("span", { className: "ps-row-resizer-dot" })));
1967
+ }
1968
+
1969
+ function MobileNav({ activePane, setActivePane, controller }) {
1970
+ const tabs = [
1971
+ { id: "workspace", label: "Main", focus: "chat" },
1972
+ { id: "inspector", label: "Inspector", focus: "inspector" },
1973
+ { id: "activity", label: "Activity", focus: "activity" },
1974
+ ];
1975
+ return React.createElement("div", { className: "ps-mobile-nav" },
1976
+ tabs.map((tab) => React.createElement("button", {
1977
+ key: tab.id,
1978
+ type: "button",
1979
+ className: `ps-mobile-nav-button${activePane === tab.id ? " is-active" : ""}`,
1980
+ onClick: () => {
1981
+ setActivePane(tab.id);
1982
+ controller.setFocus(tab.focus);
1983
+ },
1984
+ }, tab.label)));
1985
+ }
1986
+
1987
+ function ModalLayer({ controller }) {
1988
+ const themeId = useControllerSelector(controller, (state) => state.ui.themeId);
1989
+ const theme = getTheme(themeId);
1990
+ const modalState = useControllerSelector(controller, (state) => ({
1991
+ rawModal: state.ui.modal,
1992
+ themePicker: selectThemePickerModal(state),
1993
+ modelPicker: selectModelPickerModal(state),
1994
+ sessionAgentPicker: selectSessionAgentPickerModal(state),
1995
+ artifactPicker: selectArtifactPickerModal(state),
1996
+ logFilter: selectLogFilterModal(state),
1997
+ filesFilter: selectFilesFilterModal(state),
1998
+ historyFormat: selectHistoryFormatModal(state),
1999
+ renameSession: selectRenameSessionModal(state),
2000
+ artifactUpload: selectArtifactUploadModal(state),
2001
+ logsFilter: state.logs.filter,
2002
+ filesFilterState: state.files.filter,
2003
+ historyFormatState: state.executionHistory?.format || "pretty",
2004
+ }), shallowEqualObject);
2005
+ const modal = modalState.rawModal;
2006
+ if (!modal) return null;
2007
+
2008
+ const close = () => controller.handleCommand(UI_COMMANDS.CLOSE_MODAL).catch(() => {});
2009
+
2010
+ const renderListModal = (presentation, confirmLabel = "Apply") => React.createElement("div", { className: "ps-modal-backdrop", onClick: close },
2011
+ React.createElement("div", { className: "ps-modal", onClick: (event) => event.stopPropagation() },
2012
+ React.createElement("div", { className: "ps-modal-header" },
2013
+ React.createElement("div", { className: "ps-modal-title" }, presentation.title),
2014
+ React.createElement("button", { type: "button", className: "ps-modal-close", onClick: close }, "Close"),
2015
+ ),
2016
+ React.createElement("div", { className: "ps-modal-grid" },
2017
+ React.createElement("div", { className: "ps-modal-list" },
2018
+ (modal.items || []).map((item, index) => React.createElement("button", {
2019
+ key: item.id || index,
2020
+ type: "button",
2021
+ className: `ps-list-button${index === modal.selectedIndex ? " is-selected" : ""}`,
2022
+ onClick: () => controller.dispatch({ type: "ui/modalSelection", index }),
2023
+ },
2024
+ React.createElement("div", { className: "ps-line" },
2025
+ React.createElement(Runs, {
2026
+ runs: Array.isArray(presentation.rows?.[index])
2027
+ ? presentation.rows[index]
2028
+ : normalizeLines([presentation.rows?.[index]])[0]?.runs || [{ text: presentation.rows?.[index]?.text || "", color: presentation.rows?.[index]?.color }],
2029
+ theme,
2030
+ })),
2031
+ )),
2032
+ ),
2033
+ React.createElement("div", { className: "ps-modal-details" },
2034
+ React.createElement("div", { className: "ps-modal-details-title" }, presentation.detailsTitle || "Details"),
2035
+ normalizeLines(presentation.detailsLines || []).map((line, index) => React.createElement(Line, { key: `detail:${index}`, line, theme })),
2036
+ ),
2037
+ ),
2038
+ React.createElement("div", { className: "ps-modal-footer" },
2039
+ React.createElement("button", { type: "button", className: "ps-modal-button", onClick: close }, "Cancel"),
2040
+ React.createElement("button", {
2041
+ type: "button",
2042
+ className: "ps-modal-button is-primary",
2043
+ onClick: () => controller.handleCommand(UI_COMMANDS.MODAL_CONFIRM).catch(() => {}),
2044
+ }, confirmLabel)),
2045
+ ));
2046
+
2047
+ if (modal.type === "themePicker" && modalState.themePicker) {
2048
+ return renderListModal(modalState.themePicker, "Apply Theme");
2049
+ }
2050
+ if (modal.type === "modelPicker" && modalState.modelPicker) {
2051
+ return renderListModal(modalState.modelPicker, "Create Session");
2052
+ }
2053
+ if (modal.type === "sessionAgentPicker" && modalState.sessionAgentPicker) {
2054
+ return renderListModal(modalState.sessionAgentPicker, "Create Session");
2055
+ }
2056
+ if (modal.type === "artifactPicker" && modalState.artifactPicker) {
2057
+ return renderListModal(modalState.artifactPicker, "Download");
2058
+ }
2059
+ if (modal.type === "renameSession" && modalState.renameSession) {
2060
+ return React.createElement("div", { className: "ps-modal-backdrop", onClick: close },
2061
+ React.createElement("div", { className: "ps-modal is-narrow", onClick: (event) => event.stopPropagation() },
2062
+ React.createElement("div", { className: "ps-modal-header" },
2063
+ React.createElement("div", { className: "ps-modal-title" }, modalState.renameSession.title),
2064
+ React.createElement("button", { type: "button", className: "ps-modal-close", onClick: close }, "Close"),
2065
+ ),
2066
+ React.createElement("input", {
2067
+ className: "ps-modal-input",
2068
+ value: modalState.renameSession.value,
2069
+ placeholder: modalState.renameSession.placeholder,
2070
+ onChange: (event) => controller.setRenameSessionValue(event.currentTarget.value, event.currentTarget.selectionStart || event.currentTarget.value.length),
2071
+ onKeyDown: (event) => {
2072
+ if (event.key === "Enter") {
2073
+ event.preventDefault();
2074
+ controller.handleCommand(UI_COMMANDS.MODAL_CONFIRM).catch(() => {});
2075
+ }
2076
+ },
2077
+ autoFocus: true,
2078
+ }),
2079
+ React.createElement("div", { className: "ps-modal-details" },
2080
+ normalizeLines(modalState.renameSession.helpLines || []).map((line, index) => React.createElement(Line, { key: `help:${index}`, line, theme })),
2081
+ ),
2082
+ React.createElement("div", { className: "ps-modal-footer" },
2083
+ React.createElement("button", { type: "button", className: "ps-modal-button", onClick: close }, "Cancel"),
2084
+ React.createElement("button", {
2085
+ type: "button",
2086
+ className: "ps-modal-button is-primary",
2087
+ onClick: () => controller.handleCommand(UI_COMMANDS.MODAL_CONFIRM).catch(() => {}),
2088
+ }, "Save")),
2089
+ ));
2090
+ }
2091
+ if (modal.type === "artifactUpload" && modalState.artifactUpload) {
2092
+ return React.createElement("div", { className: "ps-modal-backdrop", onClick: close },
2093
+ React.createElement("div", { className: "ps-modal is-narrow", onClick: (event) => event.stopPropagation() },
2094
+ React.createElement("div", { className: "ps-modal-header" },
2095
+ React.createElement("div", { className: "ps-modal-title" }, modalState.artifactUpload.title),
2096
+ React.createElement("button", { type: "button", className: "ps-modal-close", onClick: close }, "Close"),
2097
+ ),
2098
+ React.createElement("input", {
2099
+ className: "ps-modal-input",
2100
+ value: modalState.artifactUpload.value,
2101
+ placeholder: modalState.artifactUpload.placeholder,
2102
+ onChange: (event) => controller.setArtifactUploadValue(event.currentTarget.value, event.currentTarget.selectionStart || event.currentTarget.value.length),
2103
+ autoFocus: true,
2104
+ }),
2105
+ React.createElement("div", { className: "ps-modal-details" },
2106
+ normalizeLines(modalState.artifactUpload.helpLines || []).map((line, index) => React.createElement(Line, { key: `help:${index}`, line, theme })),
2107
+ ),
2108
+ React.createElement("div", { className: "ps-modal-footer" },
2109
+ React.createElement("button", { type: "button", className: "ps-modal-button", onClick: close }, "Cancel"),
2110
+ React.createElement("button", {
2111
+ type: "button",
2112
+ className: "ps-modal-button is-primary",
2113
+ onClick: () => controller.handleCommand(UI_COMMANDS.MODAL_CONFIRM).catch(() => {}),
2114
+ }, "Attach")),
2115
+ ));
2116
+ }
2117
+
2118
+ const filterPresentation = modal.type === "logFilter"
2119
+ ? modalState.logFilter
2120
+ : modal.type === "filesFilter"
2121
+ ? modalState.filesFilter
2122
+ : modal.type === "historyFormat"
2123
+ ? modalState.historyFormat
2124
+ : null;
2125
+ if (filterPresentation) {
2126
+ return React.createElement("div", { className: "ps-modal-backdrop", onClick: close },
2127
+ React.createElement("div", { className: "ps-modal is-wide", onClick: (event) => event.stopPropagation() },
2128
+ React.createElement("div", { className: "ps-modal-header" },
2129
+ React.createElement("div", { className: "ps-modal-title" }, filterPresentation.title),
2130
+ React.createElement("button", { type: "button", className: "ps-modal-close", onClick: close }, "Close"),
2131
+ ),
2132
+ React.createElement("div", { className: "ps-filter-grid" },
2133
+ (modal.items || []).map((item, itemIndex) => {
2134
+ const currentValue = modal.type === "filesFilter"
2135
+ ? modalState.filesFilterState?.[item.id] || item.options?.[0]?.id
2136
+ : modal.type === "historyFormat"
2137
+ ? modalState.historyFormatState
2138
+ : modalState.logsFilter?.[item.id] || item.options?.[0]?.id;
2139
+ return React.createElement("div", { key: item.id || itemIndex, className: "ps-filter-column" },
2140
+ React.createElement("div", { className: "ps-filter-title" }, item.label),
2141
+ (item.options || []).map((option) => React.createElement("button", {
2142
+ key: option.id,
2143
+ type: "button",
2144
+ className: `ps-filter-option${option.id === currentValue ? " is-selected" : ""}`,
2145
+ onClick: () => {
2146
+ controller.dispatch({ type: "ui/modalSelection", index: itemIndex });
2147
+ if (modal.type === "historyFormat") {
2148
+ controller.dispatch({ type: "executionHistory/format", format: option.id });
2149
+ } else if (modal.type === "filesFilter") {
2150
+ controller.dispatch({ type: "files/filter", filter: { [item.id]: option.id } });
2151
+ controller.ensureFilesForScope(option.id).catch(() => {});
2152
+ } else {
2153
+ controller.dispatch({ type: "logs/filter", filter: { [item.id]: option.id } });
2154
+ }
2155
+ },
2156
+ }, option.label)),
2157
+ );
2158
+ }),
2159
+ ),
2160
+ React.createElement("div", { className: "ps-modal-footer" },
2161
+ React.createElement("button", { type: "button", className: "ps-modal-button is-primary", onClick: close }, "Done")),
2162
+ ));
2163
+ }
2164
+
2165
+ return null;
2166
+ }
2167
+
2168
+ function useKeyboardShortcuts(
2169
+ controller,
2170
+ mobile,
2171
+ {
2172
+ legendOpen = false,
2173
+ onToggleLegend = null,
2174
+ onCloseLegend = null,
2175
+ promptOverlayOpen = false,
2176
+ onOpenPromptOverlay = null,
2177
+ onClosePromptOverlay = null,
2178
+ } = {},
2179
+ ) {
2180
+ React.useEffect(() => {
2181
+ const handler = (event) => {
2182
+ const target = event.target;
2183
+ const editable = target instanceof HTMLElement
2184
+ && (target.tagName === "TEXTAREA" || target.tagName === "INPUT" || target.isContentEditable);
2185
+ const modal = controller.getState().ui.modal;
2186
+ const visibleInspectorTabs = getVisibleInspectorTabs(controller);
2187
+ const currentInspectorTab = controller.getState().ui.inspectorTab;
2188
+ const focusRegion = controller.getState().ui.focusRegion;
2189
+ const isPlainShortcut = !event.metaKey && !event.ctrlKey && !event.altKey;
2190
+ const isShiftTheme = !event.metaKey && !event.ctrlKey && !event.altKey && event.key === "T" && event.shiftKey;
2191
+ const isShiftModel = !event.metaKey && !event.ctrlKey && !event.altKey && event.key === "N" && event.shiftKey;
2192
+ const selectVisibleInspectorTab = (delta) => {
2193
+ const nextTab = cycleTabs(visibleInspectorTabs, currentInspectorTab, delta);
2194
+ controller.selectInspectorTab(nextTab).catch(() => {});
2195
+ };
2196
+
2197
+ if (!editable && (event.key === "?" || (event.shiftKey && event.key === "/")) && !event.metaKey && !event.ctrlKey && !event.altKey) {
2198
+ event.preventDefault();
2199
+ if (legendOpen) {
2200
+ onCloseLegend?.();
2201
+ } else {
2202
+ onToggleLegend?.();
2203
+ }
2204
+ return;
2205
+ }
2206
+
2207
+ if (legendOpen) {
2208
+ if (event.key === "Escape") {
2209
+ event.preventDefault();
2210
+ onCloseLegend?.();
2211
+ }
2212
+ return;
2213
+ }
2214
+
2215
+ if (promptOverlayOpen && !modal && event.key === "Escape") {
2216
+ event.preventDefault();
2217
+ onClosePromptOverlay?.();
2218
+ return;
2219
+ }
2220
+
2221
+ if (!editable && isShiftTheme) {
2222
+ event.preventDefault();
2223
+ controller.handleCommand(UI_COMMANDS.OPEN_THEME_PICKER).catch(() => {});
2224
+ return;
2225
+ }
2226
+ if (!editable && isShiftModel) {
2227
+ event.preventDefault();
2228
+ controller.handleCommand(UI_COMMANDS.OPEN_MODEL_PICKER).catch(() => {});
2229
+ return;
2230
+ }
2231
+
2232
+ if (modal && !editable) {
2233
+ if (event.key === "Escape") {
2234
+ event.preventDefault();
2235
+ controller.handleCommand(UI_COMMANDS.CLOSE_MODAL).catch(() => {});
2236
+ return;
2237
+ }
2238
+ if (event.key === "Enter") {
2239
+ event.preventDefault();
2240
+ controller.handleCommand(UI_COMMANDS.MODAL_CONFIRM).catch(() => {});
2241
+ return;
2242
+ }
2243
+ if (event.key === "Tab" && event.shiftKey) {
2244
+ event.preventDefault();
2245
+ controller.handleCommand(UI_COMMANDS.MODAL_PANE_PREV).catch(() => {});
2246
+ return;
2247
+ }
2248
+ if (event.key === "Tab") {
2249
+ event.preventDefault();
2250
+ controller.handleCommand(UI_COMMANDS.MODAL_PANE_NEXT).catch(() => {});
2251
+ return;
2252
+ }
2253
+ if (event.key === "ArrowUp" || event.key === "k") {
2254
+ event.preventDefault();
2255
+ controller.handleCommand(UI_COMMANDS.MODAL_PREV).catch(() => {});
2256
+ return;
2257
+ }
2258
+ if (event.key === "ArrowDown" || event.key === "j") {
2259
+ event.preventDefault();
2260
+ controller.handleCommand(UI_COMMANDS.MODAL_NEXT).catch(() => {});
2261
+ }
2262
+ return;
2263
+ }
2264
+
2265
+ if (editable) {
2266
+ if (promptOverlayOpen && event.key === "Escape") {
2267
+ event.preventDefault();
2268
+ onClosePromptOverlay?.();
2269
+ return;
2270
+ }
2271
+ return;
2272
+ }
2273
+
2274
+ if (event.key === "r" && isPlainShortcut && focusRegion !== "prompt") {
2275
+ event.preventDefault();
2276
+ controller.handleCommand(UI_COMMANDS.REFRESH).catch(() => {});
2277
+ return;
2278
+ }
2279
+ if (event.key === "n" && isPlainShortcut) {
2280
+ event.preventDefault();
2281
+ controller.handleCommand(UI_COMMANDS.NEW_SESSION).catch(() => {});
2282
+ return;
2283
+ }
2284
+ if (
2285
+ focusRegion === "inspector"
2286
+ && currentInspectorTab === "files"
2287
+ && (
2288
+ (event.key === "u" && isPlainShortcut)
2289
+ || ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "a")
2290
+ )
2291
+ ) {
2292
+ event.preventDefault();
2293
+ controller.handleCommand(UI_COMMANDS.OPEN_ARTIFACT_UPLOAD).catch(() => {});
2294
+ return;
2295
+ }
2296
+ if (focusRegion === "inspector" && currentInspectorTab === "files" && event.key === "a" && isPlainShortcut) {
2297
+ event.preventDefault();
2298
+ controller.handleCommand(UI_COMMANDS.DOWNLOAD_SELECTED_FILE).catch(() => {});
2299
+ return;
2300
+ }
2301
+ if (focusRegion === "inspector" && currentInspectorTab === "files" && event.key === "o" && isPlainShortcut) {
2302
+ event.preventDefault();
2303
+ controller.handleCommand(UI_COMMANDS.OPEN_SELECTED_FILE).catch(() => {});
2304
+ return;
2305
+ }
2306
+ if (focusRegion === "inspector" && currentInspectorTab === "files" && event.key === "f" && isPlainShortcut) {
2307
+ event.preventDefault();
2308
+ controller.handleCommand(UI_COMMANDS.OPEN_FILES_FILTER).catch(() => {});
2309
+ return;
2310
+ }
2311
+ if (focusRegion === "inspector" && currentInspectorTab === "files" && event.key === "v" && isPlainShortcut) {
2312
+ event.preventDefault();
2313
+ controller.handleCommand(UI_COMMANDS.TOGGLE_FILE_PREVIEW_FULLSCREEN).catch(() => {});
2314
+ return;
2315
+ }
2316
+ if (focusRegion === "inspector" && currentInspectorTab === "logs" && event.key === "t" && isPlainShortcut) {
2317
+ event.preventDefault();
2318
+ controller.handleCommand(UI_COMMANDS.TOGGLE_LOG_TAIL).catch(() => {});
2319
+ return;
2320
+ }
2321
+ if (focusRegion === "inspector" && currentInspectorTab === "logs" && event.key === "f" && isPlainShortcut) {
2322
+ event.preventDefault();
2323
+ controller.handleCommand(UI_COMMANDS.OPEN_LOG_FILTER).catch(() => {});
2324
+ return;
2325
+ }
2326
+ if (focusRegion === "inspector" && currentInspectorTab === "history" && event.key === "f" && isPlainShortcut) {
2327
+ event.preventDefault();
2328
+ controller.handleCommand(UI_COMMANDS.OPEN_HISTORY_FORMAT).catch(() => {});
2329
+ return;
2330
+ }
2331
+ if (focusRegion === "inspector" && currentInspectorTab === "history" && event.key === "r" && isPlainShortcut) {
2332
+ event.preventDefault();
2333
+ controller.handleCommand(UI_COMMANDS.REFRESH_EXECUTION_HISTORY).catch(() => {});
2334
+ return;
2335
+ }
2336
+ if (focusRegion === "inspector" && currentInspectorTab === "history" && event.key === "a" && isPlainShortcut) {
2337
+ event.preventDefault();
2338
+ controller.handleCommand(UI_COMMANDS.EXPORT_EXECUTION_HISTORY).catch(() => {});
2339
+ return;
2340
+ }
2341
+ if (event.key === "a" && isPlainShortcut) {
2342
+ event.preventDefault();
2343
+ controller.handleCommand(UI_COMMANDS.OPEN_ARTIFACT_PICKER).catch(() => {});
2344
+ return;
2345
+ }
2346
+ if (event.key === "p" && isPlainShortcut) {
2347
+ event.preventDefault();
2348
+ if (onOpenPromptOverlay) {
2349
+ onOpenPromptOverlay();
2350
+ } else {
2351
+ controller.handleCommand(UI_COMMANDS.FOCUS_PROMPT).catch(() => {});
2352
+ }
2353
+ return;
2354
+ }
2355
+ if (event.key === "c" && isPlainShortcut) {
2356
+ event.preventDefault();
2357
+ controller.handleCommand(UI_COMMANDS.CANCEL_SESSION).catch(() => {});
2358
+ return;
2359
+ }
2360
+ if (event.key === "d" && isPlainShortcut) {
2361
+ event.preventDefault();
2362
+ controller.handleCommand(UI_COMMANDS.DONE_SESSION).catch(() => {});
2363
+ return;
2364
+ }
2365
+ if (event.key === "D" && event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey) {
2366
+ event.preventDefault();
2367
+ controller.handleCommand(UI_COMMANDS.DELETE_SESSION).catch(() => {});
2368
+ return;
2369
+ }
2370
+ if (event.key === "m" && isPlainShortcut && focusRegion === "inspector") {
2371
+ event.preventDefault();
2372
+ selectVisibleInspectorTab(1);
2373
+ return;
2374
+ }
2375
+ if (event.key === "[" && isPlainShortcut) {
2376
+ event.preventDefault();
2377
+ controller.handleCommand(UI_COMMANDS.GROW_LEFT_PANE).catch(() => {});
2378
+ return;
2379
+ }
2380
+ if (event.key === "]" && isPlainShortcut) {
2381
+ event.preventDefault();
2382
+ controller.handleCommand(UI_COMMANDS.GROW_RIGHT_PANE).catch(() => {});
2383
+ return;
2384
+ }
2385
+ if (event.key === "{" && isPlainShortcut) {
2386
+ event.preventDefault();
2387
+ controller.handleCommand(UI_COMMANDS.SHRINK_SESSION_PANE).catch(() => {});
2388
+ return;
2389
+ }
2390
+ if (event.key === "}" && isPlainShortcut) {
2391
+ event.preventDefault();
2392
+ controller.handleCommand(UI_COMMANDS.GROW_SESSION_PANE).catch(() => {});
2393
+ return;
2394
+ }
2395
+ if (focusRegion === "inspector" && event.key === "ArrowLeft") {
2396
+ event.preventDefault();
2397
+ selectVisibleInspectorTab(-1);
2398
+ return;
2399
+ }
2400
+ if (focusRegion === "inspector" && event.key === "ArrowRight") {
2401
+ event.preventDefault();
2402
+ selectVisibleInspectorTab(1);
2403
+ return;
2404
+ }
2405
+ if (focusRegion === "sessions" && (event.key === "ArrowUp" || event.key === "k")) {
2406
+ event.preventDefault();
2407
+ controller.handleCommand(UI_COMMANDS.MOVE_SESSION_UP).catch(() => {});
2408
+ return;
2409
+ }
2410
+ if (focusRegion === "sessions" && (event.key === "ArrowDown" || event.key === "j")) {
2411
+ event.preventDefault();
2412
+ controller.handleCommand(UI_COMMANDS.MOVE_SESSION_DOWN).catch(() => {});
2413
+ return;
2414
+ }
2415
+ if (focusRegion === "sessions" && (event.key === "PageUp" || (event.ctrlKey && event.key.toLowerCase() === "u"))) {
2416
+ event.preventDefault();
2417
+ controller.handleCommand(UI_COMMANDS.PAGE_UP).catch(() => {});
2418
+ return;
2419
+ }
2420
+ if (focusRegion === "sessions" && (event.key === "PageDown" || (event.ctrlKey && event.key.toLowerCase() === "d"))) {
2421
+ event.preventDefault();
2422
+ controller.handleCommand(UI_COMMANDS.PAGE_DOWN).catch(() => {});
2423
+ return;
2424
+ }
2425
+ if (focusRegion === "sessions" && event.key === "t" && isPlainShortcut) {
2426
+ event.preventDefault();
2427
+ controller.handleCommand(UI_COMMANDS.OPEN_RENAME_SESSION).catch(() => {});
2428
+ return;
2429
+ }
2430
+ if (!mobile && (event.key === "PageUp" || (event.ctrlKey && event.key.toLowerCase() === "u"))) {
2431
+ event.preventDefault();
2432
+ controller.handleCommand(UI_COMMANDS.PAGE_UP).catch(() => {});
2433
+ return;
2434
+ }
2435
+ if (!mobile && (event.key === "PageDown" || (event.ctrlKey && event.key.toLowerCase() === "d"))) {
2436
+ event.preventDefault();
2437
+ controller.handleCommand(UI_COMMANDS.PAGE_DOWN).catch(() => {});
2438
+ return;
2439
+ }
2440
+ if (!mobile && event.key === "g" && isPlainShortcut) {
2441
+ event.preventDefault();
2442
+ controller.handleCommand(UI_COMMANDS.SCROLL_TOP).catch(() => {});
2443
+ return;
2444
+ }
2445
+ if (!mobile && event.key === "G" && event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey) {
2446
+ event.preventDefault();
2447
+ controller.handleCommand(UI_COMMANDS.SCROLL_BOTTOM).catch(() => {});
2448
+ return;
2449
+ }
2450
+ if (focusRegion !== "prompt" && event.key === "h" && isPlainShortcut) {
2451
+ event.preventDefault();
2452
+ controller.handleCommand(UI_COMMANDS.FOCUS_LEFT).catch(() => {});
2453
+ return;
2454
+ }
2455
+ if (focusRegion !== "prompt" && event.key === "l" && isPlainShortcut) {
2456
+ event.preventDefault();
2457
+ controller.handleCommand(UI_COMMANDS.FOCUS_RIGHT).catch(() => {});
2458
+ return;
2459
+ }
2460
+ if (!mobile && (event.key === "ArrowUp" || event.key === "k")) {
2461
+ event.preventDefault();
2462
+ controller.handleCommand(UI_COMMANDS.SCROLL_UP).catch(() => {});
2463
+ return;
2464
+ }
2465
+ if (!mobile && (event.key === "ArrowDown" || event.key === "j")) {
2466
+ event.preventDefault();
2467
+ controller.handleCommand(UI_COMMANDS.SCROLL_DOWN).catch(() => {});
2468
+ return;
2469
+ }
2470
+ if (!mobile && event.key === "Escape") {
2471
+ event.preventDefault();
2472
+ controller.handleCommand(UI_COMMANDS.FOCUS_SESSIONS).catch(() => {});
2473
+ }
2474
+ };
2475
+
2476
+ window.addEventListener("keydown", handler);
2477
+ return () => window.removeEventListener("keydown", handler);
2478
+ }, [controller, legendOpen, mobile, onCloseLegend, onToggleLegend, onClosePromptOverlay, onOpenPromptOverlay, promptOverlayOpen]);
2479
+ }
2480
+
2481
+ export function createWebPilotSwarmController({ transport, mode = "remote", branding = null } = {}) {
2482
+ const themeId = readStoredThemeId();
2483
+ const store = createStore(appReducer, createInitialState({ mode, branding, themeId }));
2484
+ return new PilotSwarmUiController({ store, transport });
2485
+ }
2486
+
2487
+ export function PilotSwarmWebApp({ controller }) {
2488
+ const viewportRef = React.useRef(null);
2489
+ const viewport = useMeasuredViewport(viewportRef);
2490
+ const gridViewport = computeGridViewport(viewport);
2491
+ const [showKeyLegend, setShowKeyLegend] = React.useState(false);
2492
+ const [showPromptOverlay, setShowPromptOverlay] = React.useState(false);
2493
+ const [chatFocusMode, setChatFocusMode] = React.useState(() => readStoredChatFocusMode());
2494
+ const [chatFocusPane, setChatFocusPane] = React.useState(null);
2495
+ const state = useControllerSelector(controller, (rootState) => ({
2496
+ themeId: rootState.ui.themeId,
2497
+ promptRows: getStatePromptRows(rootState),
2498
+ paneAdjust: rootState.ui.layout?.paneAdjust ?? 0,
2499
+ sessionPaneAdjust: rootState.ui.layout?.sessionPaneAdjust ?? 0,
2500
+ focusRegion: rootState.ui.focusRegion,
2501
+ inspectorTab: rootState.ui.inspectorTab,
2502
+ filesFullscreen: Boolean(rootState.files.fullscreen),
2503
+ }), shallowEqualObject);
2504
+ const [mobilePane, setMobilePane] = React.useState("workspace");
2505
+ const [mobileSessionsCollapsed, setMobileSessionsCollapsed] = React.useState(false);
2506
+ const mobile = (viewport.width || window.innerWidth || 0) < MOBILE_BREAKPOINT;
2507
+ const canUploadArtifacts = supportsBrowserFileUploads(controller) || supportsPathArtifactUploads(controller);
2508
+ const canOpenLocally = supportsLocalFileOpen(controller);
2509
+ const openPromptOverlay = React.useCallback(() => {
2510
+ controller.handleCommand(UI_COMMANDS.FOCUS_PROMPT).catch(() => {});
2511
+ setShowPromptOverlay(true);
2512
+ }, [controller]);
2513
+ const closePromptOverlay = React.useCallback(() => {
2514
+ setShowPromptOverlay(false);
2515
+ }, []);
2516
+
2517
+ useKeyboardShortcuts(controller, mobile, {
2518
+ legendOpen: showKeyLegend,
2519
+ onToggleLegend: () => setShowKeyLegend((current) => !current),
2520
+ onCloseLegend: () => setShowKeyLegend(false),
2521
+ promptOverlayOpen: showPromptOverlay,
2522
+ onOpenPromptOverlay: openPromptOverlay,
2523
+ onClosePromptOverlay: closePromptOverlay,
2524
+ });
2525
+
2526
+ React.useEffect(() => {
2527
+ controller.setViewport(gridViewport);
2528
+ }, [controller, gridViewport.height, gridViewport.width]);
2529
+
2530
+ React.useEffect(() => {
2531
+ applyDocumentTheme(state.themeId);
2532
+ writeStoredThemeId(state.themeId);
2533
+ }, [state.themeId]);
2534
+
2535
+ React.useEffect(() => {
2536
+ writeStoredChatFocusMode(chatFocusMode);
2537
+ }, [chatFocusMode]);
2538
+
2539
+ React.useEffect(() => {
2540
+ if (mobile && state.focusRegion !== "prompt") {
2541
+ setMobilePane(state.focusRegion === "activity"
2542
+ ? "activity"
2543
+ : state.focusRegion === "inspector"
2544
+ ? "inspector"
2545
+ : "workspace");
2546
+ }
2547
+ }, [mobile, state.focusRegion]);
2548
+
2549
+ React.useEffect(() => {
2550
+ const visibleTabs = getVisibleInspectorTabs(controller);
2551
+ if (!visibleTabs.includes(state.inspectorTab) && visibleTabs.length > 0) {
2552
+ controller.selectInspectorTab(visibleTabs[0]).catch(() => {});
2553
+ }
2554
+ }, [controller, state.inspectorTab]);
2555
+
2556
+ const layout = React.useMemo(
2557
+ () => computeLegacyLayout(gridViewport, state.paneAdjust, state.promptRows, state.sessionPaneAdjust),
2558
+ [gridViewport, state.paneAdjust, state.promptRows, state.sessionPaneAdjust],
2559
+ );
2560
+ const filesFullscreenActive = state.filesFullscreen && state.inspectorTab === "files";
2561
+
2562
+ React.useEffect(() => {
2563
+ if (!filesFullscreenActive || !chatFocusMode) return;
2564
+ setChatFocusMode(false);
2565
+ setChatFocusPane(null);
2566
+ }, [chatFocusMode, filesFullscreenActive]);
2567
+
2568
+ const toggleChatFocusMode = React.useCallback(() => {
2569
+ setChatFocusMode((current) => {
2570
+ const next = !current;
2571
+ if (!next) {
2572
+ setChatFocusPane(null);
2573
+ } else {
2574
+ controller.setFocus("chat");
2575
+ }
2576
+ return next;
2577
+ });
2578
+ }, [controller]);
2579
+
2580
+ const toggleChatFocusPane = React.useCallback((paneId) => {
2581
+ setChatFocusPane((current) => {
2582
+ const next = current === paneId ? null : paneId;
2583
+ controller.setFocus(next || "chat");
2584
+ return next;
2585
+ });
2586
+ }, [controller]);
2587
+
2588
+ const desktopWorkspace = React.createElement("div", {
2589
+ className: "ps-workspace-grid",
2590
+ style: {
2591
+ gridTemplateColumns: `minmax(0, ${layout.leftWidth}fr) 16px minmax(0, ${layout.rightWidth}fr)`,
2592
+ },
2593
+ },
2594
+ React.createElement("div", {
2595
+ className: "ps-workspace-column",
2596
+ style: { gridTemplateRows: `${layout.sessionPaneHeight}fr 16px ${layout.chatPaneHeight}fr` },
2597
+ },
2598
+ React.createElement(SessionPane, { controller }),
2599
+ React.createElement(RowResizeHandle, { controller, sessionPaneAdjust: state.sessionPaneAdjust }),
2600
+ React.createElement(ChatPane, { controller })),
2601
+ React.createElement(ColumnResizeHandle, { controller, paneAdjust: state.paneAdjust }),
2602
+ React.createElement("div", {
2603
+ className: "ps-workspace-column",
2604
+ style: { gridTemplateRows: `${layout.inspectorPaneHeight}fr ${layout.activityPaneHeight}fr` },
2605
+ },
2606
+ React.createElement(InspectorPane, { controller, mobile: false }),
2607
+ React.createElement(ActivityPane, { controller })));
2608
+ const chatFocusWorkspace = React.createElement(ChatFocusWorkspace, {
2609
+ controller,
2610
+ openPane: chatFocusPane,
2611
+ onTogglePane: toggleChatFocusPane,
2612
+ mobile,
2613
+ });
2614
+ const fullscreenWorkspace = React.createElement("div", { className: "ps-workspace-full" },
2615
+ React.createElement(InspectorPane, { controller, mobile: false }));
2616
+
2617
+ let mobileContent = null;
2618
+ if (filesFullscreenActive) mobileContent = React.createElement("div", { className: "ps-mobile-pane-fill" },
2619
+ React.createElement(InspectorPane, { controller, mobile: true }));
2620
+ else if (mobilePane === "inspector") mobileContent = React.createElement("div", { className: "ps-mobile-pane-fill" },
2621
+ React.createElement(InspectorPane, { controller, mobile: true }));
2622
+ else if (mobilePane === "activity") mobileContent = React.createElement("div", { className: "ps-mobile-pane-fill" },
2623
+ React.createElement(ActivityPane, { controller }));
2624
+ else mobileContent = React.createElement(MobileWorkspace, {
2625
+ controller,
2626
+ sessionsCollapsed: mobileSessionsCollapsed,
2627
+ setSessionsCollapsed: setMobileSessionsCollapsed,
2628
+ });
2629
+
2630
+ return React.createElement("div", { ref: viewportRef, className: "ps-web-shell" },
2631
+ React.createElement(Toolbar, {
2632
+ controller,
2633
+ mobile,
2634
+ onToggleLegend: () => setShowKeyLegend((current) => !current),
2635
+ onOpenPrompt: openPromptOverlay,
2636
+ chatFocusMode,
2637
+ onToggleChatFocus: toggleChatFocusMode,
2638
+ chatFocusDisabled: filesFullscreenActive,
2639
+ }),
2640
+ React.createElement("div", { className: "ps-workspace" },
2641
+ filesFullscreenActive
2642
+ ? fullscreenWorkspace
2643
+ : (chatFocusMode
2644
+ ? chatFocusWorkspace
2645
+ : (mobile ? mobileContent : desktopWorkspace))),
2646
+ React.createElement("div", { className: "ps-footer-shell" },
2647
+ React.createElement(PromptComposer, { controller, mobile, active: !showPromptOverlay })),
2648
+ mobile && !chatFocusMode ? React.createElement(MobileNav, { activePane: mobilePane, setActivePane: setMobilePane, controller }) : null,
2649
+ React.createElement(ModalLayer, { controller }),
2650
+ React.createElement(KeybindingLegend, {
2651
+ open: showKeyLegend,
2652
+ onClose: () => setShowKeyLegend(false),
2653
+ canUpload: canUploadArtifacts,
2654
+ canOpenLocally,
2655
+ }),
2656
+ React.createElement(PromptOverlay, {
2657
+ controller,
2658
+ open: showPromptOverlay,
2659
+ onClose: closePromptOverlay,
2660
+ }));
2661
+ }