pilotswarm-cli 0.1.15 → 0.1.17

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