@bakapiano/ccsm 0.22.5 → 0.22.7

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 (59) hide show
  1. package/CLAUDE.md +538 -538
  2. package/README.md +189 -189
  3. package/bin/ccsm.js +235 -235
  4. package/lib/cliActivity.js +139 -139
  5. package/lib/codexSeed.js +183 -183
  6. package/lib/config.js +279 -274
  7. package/lib/devices.js +229 -229
  8. package/lib/folders.js +124 -124
  9. package/lib/localCliSessions.js +519 -519
  10. package/lib/persistedSessions.js +129 -129
  11. package/lib/tunnel.js +621 -621
  12. package/lib/webTerminal.js +225 -225
  13. package/lib/workspace.js +233 -233
  14. package/package.json +57 -57
  15. package/public/css/base.css +99 -99
  16. package/public/css/cards.css +183 -183
  17. package/public/css/feedback.css +504 -504
  18. package/public/css/forms.css +453 -453
  19. package/public/css/layout.css +177 -176
  20. package/public/css/modal.css +190 -190
  21. package/public/css/responsive.css +176 -176
  22. package/public/css/sidebar.css +707 -707
  23. package/public/css/terminals.css +547 -553
  24. package/public/css/tokens.css +81 -81
  25. package/public/css/wco.css +196 -196
  26. package/public/css/widgets.css +2725 -2725
  27. package/public/index.html +152 -152
  28. package/public/js/api.js +371 -371
  29. package/public/js/backend.js +149 -149
  30. package/public/js/components/App.js +73 -73
  31. package/public/js/components/DirectoryPicker.js +203 -203
  32. package/public/js/components/EntityFormModal.js +153 -153
  33. package/public/js/components/Modal.js +57 -57
  34. package/public/js/components/OfflineBanner.js +67 -67
  35. package/public/js/components/PageTitleBar.js +13 -13
  36. package/public/js/components/PendingApprovalOverlay.js +128 -128
  37. package/public/js/components/Picker.js +179 -179
  38. package/public/js/components/Popover.js +55 -55
  39. package/public/js/components/RestartOverlay.js +36 -36
  40. package/public/js/components/Sidebar.js +380 -380
  41. package/public/js/components/TerminalInstance.js +28 -9
  42. package/public/js/components/XtermTerminal.js +62 -2
  43. package/public/js/components/useDragSort.js +67 -67
  44. package/public/js/dialog.js +67 -67
  45. package/public/js/icons.js +212 -212
  46. package/public/js/main.js +296 -296
  47. package/public/js/pages/AboutPage.js +90 -90
  48. package/public/js/pages/ConfigurePage.js +728 -713
  49. package/public/js/pages/LaunchPage.js +421 -421
  50. package/public/js/pages/RemotePage.js +743 -743
  51. package/public/js/pages/SessionsPage.js +73 -80
  52. package/public/js/state.js +335 -335
  53. package/scripts/dev.js +149 -149
  54. package/scripts/install.js +153 -153
  55. package/scripts/restart-helper.js +96 -96
  56. package/scripts/upgrade-helper.js +687 -687
  57. package/server.js +1820 -1807
  58. package/public/manifest.webmanifest +0 -25
  59. package/public/setup/index.html +0 -567
@@ -24,6 +24,8 @@ export class TerminalInstance {
24
24
  this.lastLayoutDimensions = null;
25
25
  this.lastSentDimensions = null;
26
26
  this.pendingLayoutFrame = null;
27
+ this.themeRefreshTimer = null;
28
+ this.pendingThemeRefresh = false;
27
29
  this.layoutRetryTimers = new Set();
28
30
  this.disposables = [];
29
31
  this.helperTextarea = null;
@@ -61,6 +63,8 @@ export class TerminalInstance {
61
63
 
62
64
  applyTheme() {
63
65
  this.xterm.applyResolvedTheme();
66
+ this.xterm.forceRedraw();
67
+ this._scheduleThemeRefreshForCli();
64
68
  }
65
69
 
66
70
  focus() {
@@ -116,6 +120,10 @@ export class TerminalInstance {
116
120
  if (nextVisible) {
117
121
  this.resizeDebouncer.flush();
118
122
  this.scheduleLayout({ immediate: true, retries: true, forceRedraw: true });
123
+ if (this.pendingThemeRefresh) {
124
+ this.pendingThemeRefresh = false;
125
+ this._scheduleThemeRefreshForCli();
126
+ }
119
127
  }
120
128
  return didChange;
121
129
  }
@@ -123,6 +131,9 @@ export class TerminalInstance {
123
131
  dispose() {
124
132
  this.closedByUs = true;
125
133
  if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
134
+ if (this.themeRefreshTimer) clearTimeout(this.themeRefreshTimer);
135
+ this.themeRefreshTimer = null;
136
+ this.pendingThemeRefresh = false;
126
137
  this._cancelScheduledLayout();
127
138
  this.resizeDebouncer.dispose();
128
139
  for (const dispose of this.disposables.splice(0)) {
@@ -205,15 +216,6 @@ export class TerminalInstance {
205
216
  ro.observe(host);
206
217
  this.disposables.push(() => ro.disconnect());
207
218
 
208
- const vv = window.visualViewport;
209
- const onVisualResize = () => this.scheduleLayout({ retries: true });
210
- vv?.addEventListener?.('resize', onVisualResize);
211
- vv?.addEventListener?.('scroll', onVisualResize);
212
- this.disposables.push(() => {
213
- vv?.removeEventListener?.('resize', onVisualResize);
214
- vv?.removeEventListener?.('scroll', onVisualResize);
215
- });
216
-
217
219
  const onHostClick = () => this.xterm.focus();
218
220
  if (this.xterm.isMobile) {
219
221
  host.addEventListener('click', onHostClick);
@@ -349,6 +351,23 @@ export class TerminalInstance {
349
351
  }
350
352
  }
351
353
 
354
+ _scheduleThemeRefreshForCli() {
355
+ if (this.cliType !== 'codex') return;
356
+ if (!this.isVisible) {
357
+ this.pendingThemeRefresh = true;
358
+ return;
359
+ }
360
+ if (this.themeRefreshTimer) clearTimeout(this.themeRefreshTimer);
361
+ // Codex caches terminal default colours for its composer. A focus-in
362
+ // event makes it re-query OSC 10/11, which our handlers answer from the
363
+ // just-applied xterm theme.
364
+ this.themeRefreshTimer = setTimeout(() => {
365
+ this.themeRefreshTimer = null;
366
+ if (this.closedByUs || this.cliType !== 'codex' || !this.isVisible) return;
367
+ this._sendFrame({ type: 'input', data: '\x1b[I' });
368
+ }, 40);
369
+ }
370
+
352
371
  _sendResize(cols, rows, force = false) {
353
372
  if (!(cols > 0 && rows > 0)) return;
354
373
  if (!force
@@ -61,6 +61,8 @@ export class XtermTerminal {
61
61
  this.webglAddon = null;
62
62
  this.webglContextLossDisposable = null;
63
63
  this.refreshDimensionListeners = new Set();
64
+ this.resizeScrollState = null;
65
+ this.resizeScrollStateTimer = null;
64
66
  this.host = null;
65
67
 
66
68
  this.raw = new Terminal({
@@ -151,7 +153,7 @@ export class XtermTerminal {
151
153
  if (!proposed) return null;
152
154
 
153
155
  if (proposed.cols !== this.raw.cols || proposed.rows !== this.raw.rows) {
154
- try { this.raw.resize(proposed.cols, proposed.rows); } catch {}
156
+ this._resizeRaw(proposed.cols, proposed.rows);
155
157
  }
156
158
  lastKnownGridDimensions = proposed;
157
159
  return proposed;
@@ -163,7 +165,7 @@ export class XtermTerminal {
163
165
 
164
166
  resize(cols, rows) {
165
167
  if (!(cols > 0 && rows > 0)) return;
166
- try { this.raw.resize(cols, rows); } catch {}
168
+ this._resizeRaw(cols, rows);
167
169
  lastKnownGridDimensions = { cols: this.raw.cols, rows: this.raw.rows };
168
170
  }
169
171
 
@@ -217,6 +219,9 @@ export class XtermTerminal {
217
219
  }
218
220
 
219
221
  dispose() {
222
+ if (this.resizeScrollStateTimer) clearTimeout(this.resizeScrollStateTimer);
223
+ this.resizeScrollState = null;
224
+ this.resizeScrollStateTimer = null;
220
225
  if (this.host?.xterm === this.raw) {
221
226
  try { delete this.host.xterm; } catch { this.host.xterm = undefined; }
222
227
  }
@@ -268,6 +273,61 @@ export class XtermTerminal {
268
273
  }
269
274
  }
270
275
 
276
+ _resizeRaw(cols, rows) {
277
+ const scrollState = this._scrollStateForResize();
278
+ try { this.raw.resize(cols, rows); } catch {}
279
+ this._restoreScrollStateIfNeeded(scrollState);
280
+ requestAnimationFrame(() => this._restoreScrollStateIfNeeded(scrollState));
281
+ setTimeout(() => this._restoreScrollStateIfNeeded(scrollState), 150);
282
+ setTimeout(() => this._restoreScrollStateIfNeeded(scrollState), 350);
283
+ }
284
+
285
+ _scrollStateForResize() {
286
+ const state = this._captureScrollState();
287
+ if (state?.viewportY > 0) {
288
+ this._rememberResizeScrollState(state);
289
+ return state;
290
+ }
291
+ return this.resizeScrollState;
292
+ }
293
+
294
+ _rememberResizeScrollState(state) {
295
+ this.resizeScrollState = state;
296
+ if (this.resizeScrollStateTimer) clearTimeout(this.resizeScrollStateTimer);
297
+ this.resizeScrollStateTimer = setTimeout(() => {
298
+ this.resizeScrollState = null;
299
+ this.resizeScrollStateTimer = null;
300
+ }, 500);
301
+ }
302
+
303
+ _captureScrollState() {
304
+ const buffer = this.raw?.buffer?.active;
305
+ if (!buffer) return null;
306
+ return {
307
+ viewportY: buffer.viewportY,
308
+ baseY: buffer.baseY,
309
+ atBottom: buffer.viewportY >= buffer.baseY,
310
+ };
311
+ }
312
+
313
+ _restoreScrollStateIfNeeded(state) {
314
+ if (!state || !(state.viewportY > 0)) return;
315
+ const buffer = this.raw?.buffer?.active;
316
+ if (!buffer) return;
317
+
318
+ if (state.atBottom) {
319
+ if (buffer.viewportY < buffer.baseY) {
320
+ try { this.raw.scrollToBottom(); } catch {}
321
+ }
322
+ return;
323
+ }
324
+
325
+ const target = Math.min(state.viewportY, buffer.baseY);
326
+ if (target > 0 && buffer.viewportY !== target) {
327
+ try { this.raw.scrollToLine(target); } catch {}
328
+ }
329
+ }
330
+
271
331
  _installSelectionCopyGuard() {
272
332
  this.raw.attachCustomKeyEventHandler((ev) => {
273
333
  if (ev.type === 'keydown'
@@ -1,67 +1,67 @@
1
- // Lightweight HTML5 drag-reorder helper.
2
- //
3
- // Usage:
4
- // const dnd = useDragSort(items.map((i) => i.id), async (nextIds) => {
5
- // await reorderFolders(nextIds);
6
- // });
7
- // items.map((it) => html`
8
- // <div ...${dnd.rowProps(it.id)}>
9
- // <span ...${dnd.handleProps(it.id)}>⋮⋮</span>
10
- // ...
11
- // </div>`);
12
- //
13
- // rowProps spreads onDragOver / onDrop / data-* on the row container.
14
- // handleProps spreads draggable + onDragStart on the drag handle. We
15
- // gate "draggable" on the handle so clicks inside the row don't start a
16
- // drag and the user can still click rows normally.
17
-
18
- import { useRef, useState } from 'preact/hooks';
19
-
20
- export function useDragSort(ids, onCommit) {
21
- const dragging = useRef(null);
22
- const [overId, setOverId] = useState(null);
23
-
24
- const handleProps = (id) => ({
25
- draggable: true,
26
- onDragStart: (ev) => {
27
- dragging.current = id;
28
- ev.dataTransfer.effectAllowed = 'move';
29
- // Setting some data is required for Firefox to actually start a drag.
30
- try { ev.dataTransfer.setData('text/plain', id); } catch {}
31
- },
32
- onDragEnd: () => { dragging.current = null; setOverId(null); },
33
- });
34
-
35
- const rowProps = (id) => ({
36
- 'data-dnd-id': id,
37
- 'data-dnd-over': overId === id ? 'true' : undefined,
38
- onDragOver: (ev) => {
39
- if (dragging.current == null || dragging.current === id) return;
40
- ev.preventDefault();
41
- ev.dataTransfer.dropEffect = 'move';
42
- if (overId !== id) setOverId(id);
43
- },
44
- onDragLeave: (ev) => {
45
- // Only clear if the pointer leaves the row entirely (not when entering a child).
46
- const rt = ev.relatedTarget;
47
- if (rt && ev.currentTarget.contains(rt)) return;
48
- if (overId === id) setOverId(null);
49
- },
50
- onDrop: (ev) => {
51
- ev.preventDefault();
52
- const src = dragging.current;
53
- dragging.current = null;
54
- setOverId(null);
55
- if (src == null || src === id) return;
56
- const cur = [...ids];
57
- const from = cur.indexOf(src);
58
- const to = cur.indexOf(id);
59
- if (from < 0 || to < 0) return;
60
- cur.splice(from, 1);
61
- cur.splice(to, 0, src);
62
- onCommit?.(cur);
63
- },
64
- });
65
-
66
- return { handleProps, rowProps, draggingId: dragging.current, overId };
67
- }
1
+ // Lightweight HTML5 drag-reorder helper.
2
+ //
3
+ // Usage:
4
+ // const dnd = useDragSort(items.map((i) => i.id), async (nextIds) => {
5
+ // await reorderFolders(nextIds);
6
+ // });
7
+ // items.map((it) => html`
8
+ // <div ...${dnd.rowProps(it.id)}>
9
+ // <span ...${dnd.handleProps(it.id)}>⋮⋮</span>
10
+ // ...
11
+ // </div>`);
12
+ //
13
+ // rowProps spreads onDragOver / onDrop / data-* on the row container.
14
+ // handleProps spreads draggable + onDragStart on the drag handle. We
15
+ // gate "draggable" on the handle so clicks inside the row don't start a
16
+ // drag and the user can still click rows normally.
17
+
18
+ import { useRef, useState } from 'preact/hooks';
19
+
20
+ export function useDragSort(ids, onCommit) {
21
+ const dragging = useRef(null);
22
+ const [overId, setOverId] = useState(null);
23
+
24
+ const handleProps = (id) => ({
25
+ draggable: true,
26
+ onDragStart: (ev) => {
27
+ dragging.current = id;
28
+ ev.dataTransfer.effectAllowed = 'move';
29
+ // Setting some data is required for Firefox to actually start a drag.
30
+ try { ev.dataTransfer.setData('text/plain', id); } catch {}
31
+ },
32
+ onDragEnd: () => { dragging.current = null; setOverId(null); },
33
+ });
34
+
35
+ const rowProps = (id) => ({
36
+ 'data-dnd-id': id,
37
+ 'data-dnd-over': overId === id ? 'true' : undefined,
38
+ onDragOver: (ev) => {
39
+ if (dragging.current == null || dragging.current === id) return;
40
+ ev.preventDefault();
41
+ ev.dataTransfer.dropEffect = 'move';
42
+ if (overId !== id) setOverId(id);
43
+ },
44
+ onDragLeave: (ev) => {
45
+ // Only clear if the pointer leaves the row entirely (not when entering a child).
46
+ const rt = ev.relatedTarget;
47
+ if (rt && ev.currentTarget.contains(rt)) return;
48
+ if (overId === id) setOverId(null);
49
+ },
50
+ onDrop: (ev) => {
51
+ ev.preventDefault();
52
+ const src = dragging.current;
53
+ dragging.current = null;
54
+ setOverId(null);
55
+ if (src == null || src === id) return;
56
+ const cur = [...ids];
57
+ const from = cur.indexOf(src);
58
+ const to = cur.indexOf(id);
59
+ if (from < 0 || to < 0) return;
60
+ cur.splice(from, 1);
61
+ cur.splice(to, 0, src);
62
+ onCommit?.(cur);
63
+ },
64
+ });
65
+
66
+ return { handleProps, rowProps, draggingId: dragging.current, overId };
67
+ }
@@ -1,67 +1,67 @@
1
- // Promise-based confirm/prompt rendered through DialogHost. The stack
2
- // signal lets us nest dialogs if ever needed; .close() pops by id.
3
-
4
- import { signal } from '@preact/signals';
5
- import { html } from './html.js';
6
-
7
- export const dialogs = signal([]);
8
- let nextId = 1;
9
-
10
- function push(entry) {
11
- return new Promise((resolve) => {
12
- const id = nextId++;
13
- const close = (action, host) => {
14
- dialogs.value = dialogs.value.filter((d) => d.id !== id);
15
- resolve(entry.onResolve(action, host));
16
- };
17
- dialogs.value = [...dialogs.value, { id, ...entry, close }];
18
- });
19
- }
20
-
21
- const CLOSE_X = html`
22
- <button class="modal-close" type="button" aria-label="Close" data-action="cancel">
23
- <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
24
- <line x1="3" y1="3" x2="13" y2="13"/>
25
- <line x1="13" y1="3" x2="3" y2="13"/>
26
- </svg>
27
- </button>`;
28
-
29
- export function ccsmConfirm(message, opts = {}) {
30
- const { title = 'Confirm', okLabel = 'Confirm', cancelLabel = 'Cancel', danger = false } = opts;
31
- return push({
32
- render: () => html`<div class="modal modal-dialog">
33
- <header class="modal-head"><h2>${title}</h2>${CLOSE_X}</header>
34
- <div class="modal-body"><p class="dialog-msg">${message}</p></div>
35
- <footer class="modal-foot">
36
- <button class="action" data-action="cancel">${cancelLabel}</button>
37
- <button class=${`action ${danger ? 'danger' : 'primary'}`} data-action="ok">${okLabel}</button>
38
- </footer>
39
- </div>`,
40
- onResolve: (action) => action === 'ok' || action === 'enter',
41
- });
42
- }
43
-
44
- export function ccsmPrompt(message, defaultValue = '', opts = {}) {
45
- const { title, okLabel = 'Save', cancelLabel = 'Cancel', placeholder = '' } = opts;
46
- return push({
47
- render: () => html`<div class="modal modal-dialog">
48
- <header class="modal-head"><h2>${title || message}</h2>${CLOSE_X}</header>
49
- <div class="modal-body">
50
- ${title ? html`<p class="dialog-msg">${message}</p>` : null}
51
- <input type="text" class="input" placeholder=${placeholder} value=${defaultValue} />
52
- </div>
53
- <footer class="modal-foot">
54
- <button class="action" data-action="cancel">${cancelLabel}</button>
55
- <button class="action primary" data-action="ok">${okLabel}</button>
56
- </footer>
57
- </div>`,
58
- initialFocus: (host) => {
59
- const inp = host.querySelector('input');
60
- if (inp) { inp.focus(); inp.select(); }
61
- },
62
- onResolve: (action, host) => {
63
- const inp = host?.querySelector('input');
64
- return (action === 'ok' || action === 'enter') ? (inp?.value ?? '') : null;
65
- },
66
- });
67
- }
1
+ // Promise-based confirm/prompt rendered through DialogHost. The stack
2
+ // signal lets us nest dialogs if ever needed; .close() pops by id.
3
+
4
+ import { signal } from '@preact/signals';
5
+ import { html } from './html.js';
6
+
7
+ export const dialogs = signal([]);
8
+ let nextId = 1;
9
+
10
+ function push(entry) {
11
+ return new Promise((resolve) => {
12
+ const id = nextId++;
13
+ const close = (action, host) => {
14
+ dialogs.value = dialogs.value.filter((d) => d.id !== id);
15
+ resolve(entry.onResolve(action, host));
16
+ };
17
+ dialogs.value = [...dialogs.value, { id, ...entry, close }];
18
+ });
19
+ }
20
+
21
+ const CLOSE_X = html`
22
+ <button class="modal-close" type="button" aria-label="Close" data-action="cancel">
23
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
24
+ <line x1="3" y1="3" x2="13" y2="13"/>
25
+ <line x1="13" y1="3" x2="3" y2="13"/>
26
+ </svg>
27
+ </button>`;
28
+
29
+ export function ccsmConfirm(message, opts = {}) {
30
+ const { title = 'Confirm', okLabel = 'Confirm', cancelLabel = 'Cancel', danger = false } = opts;
31
+ return push({
32
+ render: () => html`<div class="modal modal-dialog">
33
+ <header class="modal-head"><h2>${title}</h2>${CLOSE_X}</header>
34
+ <div class="modal-body"><p class="dialog-msg">${message}</p></div>
35
+ <footer class="modal-foot">
36
+ <button class="action" data-action="cancel">${cancelLabel}</button>
37
+ <button class=${`action ${danger ? 'danger' : 'primary'}`} data-action="ok">${okLabel}</button>
38
+ </footer>
39
+ </div>`,
40
+ onResolve: (action) => action === 'ok' || action === 'enter',
41
+ });
42
+ }
43
+
44
+ export function ccsmPrompt(message, defaultValue = '', opts = {}) {
45
+ const { title, okLabel = 'Save', cancelLabel = 'Cancel', placeholder = '' } = opts;
46
+ return push({
47
+ render: () => html`<div class="modal modal-dialog">
48
+ <header class="modal-head"><h2>${title || message}</h2>${CLOSE_X}</header>
49
+ <div class="modal-body">
50
+ ${title ? html`<p class="dialog-msg">${message}</p>` : null}
51
+ <input type="text" class="input" placeholder=${placeholder} value=${defaultValue} />
52
+ </div>
53
+ <footer class="modal-foot">
54
+ <button class="action" data-action="cancel">${cancelLabel}</button>
55
+ <button class="action primary" data-action="ok">${okLabel}</button>
56
+ </footer>
57
+ </div>`,
58
+ initialFocus: (host) => {
59
+ const inp = host.querySelector('input');
60
+ if (inp) { inp.focus(); inp.select(); }
61
+ },
62
+ onResolve: (action, host) => {
63
+ const inp = host?.querySelector('input');
64
+ return (action === 'ok' || action === 'enter') ? (inp?.value ?? '') : null;
65
+ },
66
+ });
67
+ }