aicodeman 1.33.2 → 1.33.3

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 (91) hide show
  1. package/dist/web/public/admin-ui.js.gz +0 -0
  2. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  3. package/dist/web/public/app.0879466b.js +43 -0
  4. package/dist/web/public/app.0879466b.js.br +0 -0
  5. package/dist/web/public/app.0879466b.js.gz +0 -0
  6. package/dist/web/public/approvals-ui.js.gz +0 -0
  7. package/dist/web/public/{constants.cc927eb8.js → constants.d95fade9.js} +19 -2
  8. package/dist/web/public/constants.d95fade9.js.br +0 -0
  9. package/dist/web/public/constants.d95fade9.js.gz +0 -0
  10. package/dist/web/public/cron-ui.js.gz +0 -0
  11. package/dist/web/public/entrance-animations.js.gz +0 -0
  12. package/dist/web/public/home-sessions.js.gz +0 -0
  13. package/dist/web/public/host-wake-ui.js.gz +0 -0
  14. package/dist/web/public/i18n.7f8be985.js +1 -0
  15. package/dist/web/public/i18n.7f8be985.js.br +0 -0
  16. package/dist/web/public/i18n.7f8be985.js.gz +0 -0
  17. package/dist/web/public/image-input.a216cf9e.js.gz +0 -0
  18. package/dist/web/public/index.html +11 -8
  19. package/dist/web/public/index.html.br +0 -0
  20. package/dist/web/public/index.html.gz +0 -0
  21. package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
  22. package/dist/web/public/keyboard-accessory.c1e7c7b7.js.gz +0 -0
  23. package/dist/web/public/mobile-handlers.23a85db1.js.gz +0 -0
  24. package/dist/web/public/mobile-overview.js.gz +0 -0
  25. package/dist/web/public/mobile.1aa4952d.css.gz +0 -0
  26. package/dist/web/public/notification-manager.50bb87d8.js.gz +0 -0
  27. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  28. package/dist/web/public/{panels-ui.67646701.js → panels-ui.6a5898d9.js} +69 -67
  29. package/dist/web/public/panels-ui.6a5898d9.js.br +0 -0
  30. package/dist/web/public/panels-ui.6a5898d9.js.gz +0 -0
  31. package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
  32. package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
  33. package/dist/web/public/readmymind-ui.js.gz +0 -0
  34. package/dist/web/public/reboot-restore-ui.js.gz +0 -0
  35. package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
  36. package/dist/web/public/sanitize-html.e7147e76.js +1 -0
  37. package/dist/web/public/sanitize-html.e7147e76.js.br +0 -0
  38. package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
  39. package/dist/web/public/session-lineage.js.gz +0 -0
  40. package/dist/web/public/session-ui.7575a231.js.gz +0 -0
  41. package/dist/web/public/settings-ui.c68e7f59.js.gz +0 -0
  42. package/dist/web/public/{styles.3fe3312d.css → styles.f2b78b86.css} +1 -1
  43. package/dist/web/public/styles.f2b78b86.css.br +0 -0
  44. package/dist/web/public/styles.f2b78b86.css.gz +0 -0
  45. package/dist/web/public/subagent-windows.e6ca799f.js.gz +0 -0
  46. package/dist/web/public/sw.js +2 -2
  47. package/dist/web/public/sw.js.br +0 -0
  48. package/dist/web/public/sw.js.gz +0 -0
  49. package/dist/web/public/tab-rail-resize.dcdc5772.js.gz +0 -0
  50. package/dist/web/public/terminal-keycode229-recovery.fb91b25b.js.gz +0 -0
  51. package/dist/web/public/terminal-split.js +192 -7
  52. package/dist/web/public/terminal-split.js.br +0 -0
  53. package/dist/web/public/terminal-split.js.gz +0 -0
  54. package/dist/web/public/terminal-ui.d5223c0e.js.gz +0 -0
  55. package/dist/web/public/ultracode-panel.js.gz +0 -0
  56. package/dist/web/public/ultracode-windows.js.gz +0 -0
  57. package/dist/web/public/upload.html.gz +0 -0
  58. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  59. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  60. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  61. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  62. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  63. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  64. package/dist/web/public/vendor/xterm-predictive-echo.bd6882b8.js.gz +0 -0
  65. package/dist/web/public/vendor/xterm-zerolag-input.6fee72f2.js.gz +0 -0
  66. package/dist/web/public/vendor/xterm.css.gz +0 -0
  67. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  68. package/dist/web/public/voice-input.c4b51eb6.js.gz +0 -0
  69. package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
  70. package/dist/web/public/webview-tabs.js +15 -4
  71. package/dist/web/public/webview-tabs.js.br +0 -0
  72. package/dist/web/public/webview-tabs.js.gz +0 -0
  73. package/dist/web/routes/file-routes.d.ts.map +1 -1
  74. package/dist/web/routes/file-routes.js +2 -1
  75. package/dist/web/routes/file-routes.js.map +1 -1
  76. package/package.json +1 -1
  77. package/dist/web/public/app.91131d2c.js +0 -43
  78. package/dist/web/public/app.91131d2c.js.br +0 -0
  79. package/dist/web/public/app.91131d2c.js.gz +0 -0
  80. package/dist/web/public/constants.cc927eb8.js.br +0 -0
  81. package/dist/web/public/constants.cc927eb8.js.gz +0 -0
  82. package/dist/web/public/i18n.f094d01f.js +0 -1
  83. package/dist/web/public/i18n.f094d01f.js.br +0 -0
  84. package/dist/web/public/i18n.f094d01f.js.gz +0 -0
  85. package/dist/web/public/panels-ui.67646701.js.br +0 -0
  86. package/dist/web/public/panels-ui.67646701.js.gz +0 -0
  87. package/dist/web/public/sanitize-html.bc7078d6.js +0 -1
  88. package/dist/web/public/sanitize-html.bc7078d6.js.br +0 -0
  89. package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
  90. package/dist/web/public/styles.3fe3312d.css.br +0 -0
  91. package/dist/web/public/styles.3fe3312d.css.gz +0 -0
@@ -26,7 +26,7 @@
26
26
  // not the current one, so the old constant key meant that cleanup never deleted
27
27
  // anything — hashed assets from every release ever deployed accumulated in one
28
28
  // bucket until the origin hit its storage quota.
29
- const BUILD_ID = '403212f981d2';
29
+ const BUILD_ID = '454a058ea4d8';
30
30
  const CACHE_NAME = `codeman-${BUILD_ID}`;
31
31
 
32
32
  // Reverse-proxy base path: the worker is served at `<base>/sw.js`, so its own
@@ -48,7 +48,7 @@ const B = (p) => (p && p[0] === '/' ? SW_BASE + p : p);
48
48
  // handler caches every successful GET at runtime, but the precache warmed
49
49
  // nothing while looking like it did. Deriving it from the same manifest that
50
50
  // renames the files is the only thing that keeps the two from drifting again.
51
- const HASHED_ASSETS = ["styles.3fe3312d.css", "mobile.1aa4952d.css", "constants.cc927eb8.js", "i18n.f094d01f.js", "mobile-handlers.23a85db1.js", "voice-input.c4b51eb6.js", "notification-manager.50bb87d8.js", "keyboard-accessory.c1e7c7b7.js", "input-cjk.8bc46081.js", "terminal-keycode229-recovery.fb91b25b.js", "sanitize-html.bc7078d6.js", "app.91131d2c.js", "tab-rail-resize.dcdc5772.js", "terminal-ui.d5223c0e.js", "respawn-ui.ff0dae4c.js", "ralph-panel.b350e2ef.js", "settings-ui.c68e7f59.js", "panels-ui.67646701.js", "session-ui.7575a231.js", "ralph-wizard.13a1831e.js", "api-client.c9b1cddc.js", "subagent-windows.e6ca799f.js", "image-input.a216cf9e.js", "vendor/xterm-zerolag-input.6fee72f2.js", "vendor/xterm-predictive-echo.bd6882b8.js"];
51
+ const HASHED_ASSETS = ["styles.f2b78b86.css", "mobile.1aa4952d.css", "constants.d95fade9.js", "i18n.7f8be985.js", "mobile-handlers.23a85db1.js", "voice-input.c4b51eb6.js", "notification-manager.50bb87d8.js", "keyboard-accessory.c1e7c7b7.js", "input-cjk.8bc46081.js", "terminal-keycode229-recovery.fb91b25b.js", "sanitize-html.e7147e76.js", "app.0879466b.js", "tab-rail-resize.dcdc5772.js", "terminal-ui.d5223c0e.js", "respawn-ui.ff0dae4c.js", "ralph-panel.b350e2ef.js", "settings-ui.c68e7f59.js", "panels-ui.6a5898d9.js", "session-ui.7575a231.js", "ralph-wizard.13a1831e.js", "api-client.c9b1cddc.js", "subagent-windows.e6ca799f.js", "image-input.a216cf9e.js", "vendor/xterm-zerolag-input.6fee72f2.js", "vendor/xterm-predictive-echo.bd6882b8.js"];
52
52
 
53
53
  // Core app shell -- cached on install for instant startup
54
54
  const APP_SHELL = [
Binary file
Binary file
@@ -14,6 +14,9 @@
14
14
  */
15
15
 
16
16
  (function (global) {
17
+ // How long a scroll-to-top history pull may hold Pane B's live output.
18
+ const HISTORY_PULL_TIMEOUT_MS = 10000;
19
+
17
20
  /**
18
21
  * Minimal chunked write for Pane B's own xterm instance — write() in
19
22
  * TERMINAL_CHUNK_SIZE slices, yielding a frame between each, instead of one
@@ -68,10 +71,18 @@
68
71
  this.fitAddon = null;
69
72
  this.ws = null;
70
73
  this._wsReady = false;
74
+ this._wsClosed = false;
71
75
  this._destroyed = false;
72
76
  // Single-flight state for _loadBuffer()/_refreshBuffer() below.
73
77
  this._bufferLoading = false;
74
78
  this._bufferRefreshPending = false;
79
+ // Scroll-to-top history pull (shell panes only), see _maybeLoadMoreHistory().
80
+ // `_liveQueue` is non-null exactly while a pull is replaying: live frames
81
+ // are held there with their arrival time instead of written under it.
82
+ this._historyPullAt = 0;
83
+ this._historyPullUseless = false;
84
+ this._liveQueue = null;
85
+ this._onWheel = null;
75
86
  }
76
87
 
77
88
  async connect() {
@@ -95,6 +106,8 @@
95
106
  this.terminal.open(this.mountEl);
96
107
  this.fitAddon.fit();
97
108
 
109
+ this._installWheelListener();
110
+
98
111
  this.terminal.onData((data) => {
99
112
  if (this.ws && this.ws.readyState === WebSocket.OPEN) {
100
113
  this.ws.send(JSON.stringify({ t: 'i', d: data }));
@@ -254,9 +267,9 @@
254
267
  try {
255
268
  const msg = JSON.parse(event.data);
256
269
  if (msg.t === 'o') {
257
- this.terminal.write(msg.d);
270
+ this._onLiveOutput(msg.d);
258
271
  } else if (msg.t === 'c') {
259
- this.terminal.clear();
272
+ this._onLiveClear();
260
273
  } else if (msg.t === 'r') {
261
274
  // Server-triggered refresh (SSE backpressure cleared, terminal
262
275
  // data was dropped). The primary pane routes this to
@@ -280,16 +293,30 @@
280
293
  // normal while it quietly ate everything typed into it. v1 scope is
281
294
  // "say so", not reconnect — collapsing the split would lose the
282
295
  // user's place in Pane B's scrollback for a transient blip.
283
- this.ws.onclose = () => {
284
- this._wsReady = false;
285
- this.terminal?.write('\r\n\x1b[2m[Pane B disconnected — close and reopen the split to reconnect]\x1b[0m\r\n');
286
- };
296
+ this.ws.onclose = () => this._onSocketClosed();
287
297
 
288
298
  this.ws.onerror = () => {
289
299
  // onclose fires after onerror — cleanup happens there.
290
300
  };
291
301
  }
292
302
 
303
+ // The socket's close, split out of connect() so the tests can drive it.
304
+ // While a history pull is running the marker waits for the pull's finally
305
+ // block: written now, it would sit above the output the pull is still
306
+ // holding (flushed after it on a skip, a downgrade or a failed fetch) or
307
+ // land in the middle of a chunked replay.
308
+ _onSocketClosed() {
309
+ this._wsReady = false;
310
+ this._wsClosed = true;
311
+ if (!this._liveQueue) this._writeDisconnectedMarker();
312
+ }
313
+
314
+ // Extracted so both _onSocketClosed() and a history pull that ends on a
315
+ // closed socket can write it (see _pullHistory()'s finally block).
316
+ _writeDisconnectedMarker() {
317
+ this.terminal?.write('\r\n\x1b[2m[Pane B disconnected — close and reopen the split to reconnect]\x1b[0m\r\n');
318
+ }
319
+
293
320
  // Fetches and writes the session's current scrollback. Used both by
294
321
  // connect() (initial load) and by the `{t:'r'}` server-refresh frame
295
322
  // (below) — the primary pane's own _onSessionNeedsRefresh (app.js) is
@@ -324,14 +351,168 @@
324
351
  } catch {
325
352
  /* Best-effort — live output still arrives once the socket connects. */
326
353
  } finally {
327
- this._bufferLoading = false;
354
+ this._endBufferLoad();
328
355
  }
356
+ }
357
+
358
+ // Ends a single-flight load (initial, refresh or history pull): clears the
359
+ // flag, then runs the ONE trailing refresh that arrived while it was busy.
360
+ _endBufferLoad() {
361
+ this._bufferLoading = false;
329
362
  if (this._bufferRefreshPending && !this._destroyed) {
330
363
  this._bufferRefreshPending = false;
331
364
  this._refreshBuffer();
332
365
  }
333
366
  }
334
367
 
368
+ // Live terminal output. Written straight through, except while a history
369
+ // pull is replaying: a capture is current only up to the instant tmux took
370
+ // it, so a frame arriving mid-replay is held with its arrival time and
371
+ // replayed behind the snapshot by _pullHistory() (the primary pane's
372
+ // _finishBufferLoad `since` rule), never written underneath it.
373
+ _onLiveOutput(data) {
374
+ if (this._liveQueue) this._liveQueue.push({ at: performance.now(), data });
375
+ else this.terminal?.write(data);
376
+ }
377
+
378
+ // The server's `{t:'c'}` clear frame takes the same route as output, for the
379
+ // same reason: clearing straight away, mid-replay, would wipe the half-written
380
+ // snapshot and leave _pullHistory() measuring a buffer that is no longer the
381
+ // one it is restoring. Queued, it lands in order with the frames around it.
382
+ _onLiveClear() {
383
+ if (this._liveQueue) this._liveQueue.push({ at: performance.now(), clear: true });
384
+ else this.terminal?.clear();
385
+ }
386
+
387
+ // Capture phase, because xterm's own wheel handler stopPropagation()s every
388
+ // event it consumes, so a bubbling listener here would never see the wheel
389
+ // while the pane still has scrollback to scroll. Passive: this only observes,
390
+ // xterm keeps doing the scrolling.
391
+ _installWheelListener() {
392
+ this._onWheel = (ev) => {
393
+ if (ev.deltaY < 0) this._maybeLoadMoreHistory();
394
+ };
395
+ this.mountEl.addEventListener('wheel', this._onWheel, { capture: true, passive: true });
396
+ }
397
+
398
+ // Wheel-up at the top of a SHELL pane's scrollback. tmux repaints a burst of
399
+ // output (`cat` of a file longer than the screen) instead of scrolling it,
400
+ // so this pane's xterm ends up with about one screen of scrollback while
401
+ // tmux holds every line — and nothing here ever went back to ask, so the
402
+ // history was unreachable. The primary pane has the same pull
403
+ // (app.js _maybeRefetchFullHistory); Pane B is a separate xterm and needs its
404
+ // own. Shell only: a non-shell CLI's history is out of scope for this pull
405
+ // (its load already takes `full=1`; codex and Claude's inline renderer do
406
+ // grow tmux history, this just isn't how they recover it). The alternate-
407
+ // screen skip (nano, vim, less) only matters for a direct-PTY shell — under
408
+ // tmux the browser xterm never enters the alternate buffer.
409
+ _maybeLoadMoreHistory() {
410
+ if (this.sessionMode !== 'shell' || this._destroyed || !this.terminal) return;
411
+ if (this._bufferLoading) return;
412
+ // Mirrors app.js _maybeRefetchFullHistory and this pane's own
413
+ // _sendResize(): a detached session's own window already owns its PTY
414
+ // size and scrollback, so Pane B has nothing of its own to reconcile.
415
+ if (this.detachedSessions?.has(this.sessionId)) return;
416
+ const active = this.terminal.buffer.active;
417
+ if (active.type !== 'normal' || active.viewportY !== 0) return;
418
+ // Momentum scrolling fires this dozens of times per flick, so cooldown
419
+ // rather than latch; a pull that could only have downgraded the pane
420
+ // waits far longer.
421
+ const cooldown = this._historyPullUseless ? 60000 : 4000;
422
+ const now = Date.now();
423
+ if (now - this._historyPullAt < cooldown) return;
424
+ this._historyPullAt = now;
425
+ void this._pullHistory();
426
+ }
427
+
428
+ // Pulls a BOUNDED window of tmux's full history (the same TERMINAL_TAIL_SIZE
429
+ // a tab switch loads, so a multi-megabyte capture never lands on xterm's
430
+ // main thread) and replays it under the reader's current place. Holds the
431
+ // single-flight flag across the fetch AND the replay, like _loadBuffer().
432
+ async _pullHistory() {
433
+ // A close before the pull already wrote its marker; one during it did not.
434
+ const closedBefore = this._wsClosed;
435
+ this._bufferLoading = true;
436
+ this._liveQueue = [];
437
+ let replayed = false;
438
+ let capturedAt = 0;
439
+ try {
440
+ // A deadline, because live output is held for as long as this runs: a
441
+ // request that hangs would otherwise freeze the whole pane. Aborting
442
+ // lands in the catch below, which releases the flag and the queue. It
443
+ // covers the body read too, not just the headers.
444
+ const res = await fetch(`/api/sessions/${this.sessionId}/terminal?full=1&tail=${TERMINAL_TAIL_SIZE}`, {
445
+ signal: global.AbortSignal?.timeout?.(HISTORY_PULL_TIMEOUT_MS),
446
+ });
447
+ // The cutoff below is the response's arrival, the same `since` rule the
448
+ // primary pane uses (_finishBufferLoad). It is a client clock standing in
449
+ // for the instant tmux took the capture, which lies somewhere in the
450
+ // round trip, so a frame in that window can be lost or doubled. Bounded
451
+ // by one round trip and not closable without a server-side capture time.
452
+ capturedAt = performance.now();
453
+ const payload = (await res.json())?.data;
454
+ const buffer = payload?.terminalBuffer;
455
+ const term = this.terminal;
456
+ if (!buffer || !term || this._destroyed) return;
457
+ const rowsBefore = term.buffer.active.length;
458
+ const rowsIncoming = global.app?._estimateReplayRows?.(buffer, term.cols) ?? buffer.split('\n').length;
459
+ // xterm keeps at most `scrollback + rows` rows while tmux keeps far more
460
+ // lines, so a window of short lines can carry more rows than this pane
461
+ // can ever hold, and `rowsIncoming <= rowsBefore` would never come true.
462
+ const scrollbackCap = term.options?.scrollback || 0;
463
+ const paneFull = scrollbackCap > 0 && rowsBefore >= scrollbackCap + term.rows;
464
+ // Nothing to gain (this also covers a downgrade, which would delete
465
+ // history mid-scroll), and a reset+rewrite would jump the viewport. An
466
+ // untruncated window IS all of tmux's history and the next burst can add
467
+ // more, so keep the 4 s cooldown. A truncated window can never reach past
468
+ // what the pane shows, and every ask costs the server a capture-pane of
469
+ // the whole history (`tail` is cut after it): back off to 60 s, as the
470
+ // primary pane does (app.js _maybeRefetchFullHistory). A full pane backs
471
+ // off too, since no window can ever fit in it.
472
+ if (rowsIncoming <= rowsBefore || paneFull) {
473
+ if (payload.truncated || paneFull) this._historyPullUseless = true;
474
+ return;
475
+ }
476
+ this._historyPullUseless = false;
477
+ term.write('\x1bc');
478
+ replayed = true;
479
+ await writeChunked(term, buffer, () => this._destroyed);
480
+ if (this._destroyed || !this.terminal) return;
481
+ // xterm parses asynchronously: an empty write's callback fires only
482
+ // after everything before it, so the row count below is the settled one.
483
+ await new Promise((resolve) => this.terminal.write('', resolve));
484
+ if (this._destroyed || !this.terminal) return;
485
+ // The replay grew the buffer UPWARD, so what was row 0 is now `delta`
486
+ // rows down; land there and the recovered history sits above it.
487
+ const delta = this.terminal.buffer.active.length - rowsBefore;
488
+ if (delta > 0) this.terminal.scrollToLine(delta);
489
+ else this.terminal.scrollToTop();
490
+ } catch {
491
+ /* Best-effort — live output keeps arriving whatever happens here. */
492
+ } finally {
493
+ const queued = this._liveQueue ?? [];
494
+ this._liveQueue = null;
495
+ // After a replay, only frames that arrived after the capture are news;
496
+ // earlier ones are already in it. With no replay, every held frame is.
497
+ const cutoff = replayed ? capturedAt : 0;
498
+ for (const entry of queued) {
499
+ if (entry.at < cutoff) continue;
500
+ if (entry.clear) this.terminal?.clear();
501
+ else this.terminal?.write(entry.data);
502
+ }
503
+ // A replay's own `\x1bc` wipes a marker written before the pull,
504
+ // painting a fresh, current-looking history while onData keeps
505
+ // silently dropping every keystroke on the dead socket, so re-stamp it
506
+ // after a replay. A close DURING the pull wrote no marker at all
507
+ // (_onSocketClosed() defers it while the queue is live), so write it
508
+ // whether or not this pull replayed. Checked after the queue flush so
509
+ // it is the last thing on screen, matching what the close would have
510
+ // left had the pull never run.
511
+ if (this._wsClosed && (replayed || !closedBefore)) this._writeDisconnectedMarker();
512
+ this._endBufferLoad();
513
+ }
514
+ }
515
+
335
516
  // The `{t:'r'}` server-refresh path: clear, then replay. Two refresh
336
517
  // frames in a row used to start two concurrent replays, each clearing
337
518
  // the terminal under the other's chunked write. A refresh that arrives
@@ -381,6 +562,10 @@
381
562
 
382
563
  destroy() {
383
564
  this._destroyed = true;
565
+ if (this._onWheel) {
566
+ this.mountEl?.removeEventListener('wheel', this._onWheel, { capture: true });
567
+ this._onWheel = null;
568
+ }
384
569
  if (this.ws) {
385
570
  this.ws.onopen = null;
386
571
  this.ws.onmessage = null;
Binary file
Binary file
@@ -274,7 +274,10 @@ Object.assign(CodemanApp.prototype, {
274
274
  // one `/`, and refuse whatever still opens a second one. The proxied form
275
275
  // is refused server-side as well (resolveUpstreamUrl).
276
276
  const path = data.path.replace(/[\t\n\r]/g, '').replace(/^[/\\]+/, '/');
277
- void this.openWebview(id, { path: path.startsWith('/') && !/^\/[/\\]/.test(path) ? path : '/' });
277
+ void this.openWebview(id, {
278
+ path: path.startsWith('/') && !/^\/[/\\]/.test(path) ? path : '/',
279
+ auto: true,
280
+ });
278
281
  return;
279
282
  }
280
283
  };
@@ -356,15 +359,23 @@ Object.assign(CodemanApp.prototype, {
356
359
  */
357
360
  /**
358
361
  * @param {string} id
359
- * @param {{path?: string}} [options] `path` (pathname+search+hash) opens a
362
+ * @param {{path?: string, auto?: boolean}} [options] `path` (pathname+search+hash) opens a
360
363
  * deep link inside the dashboard: appended to the proxy prefix, or resolved
361
364
  * against the real URL in direct mode. A mounted frame is navigated there
362
- * rather than left on whatever page it was showing.
365
+ * rather than left on whatever page it was showing. `auto: true` marks an
366
+ * open the APP made (a frame recovering itself, the fallback after the
367
+ * active web tab closes), as on selectSession().
363
368
  */
364
369
  async openWebview(id, options = {}) {
365
370
  const webview = this.webviews.get(id);
366
371
  if (!webview) return;
367
372
 
373
+ // Opening a web tab yourself is choosing something else, so a
374
+ // `#session=<id>` link still waiting for its session must not take the
375
+ // screen from this tab later. Retired before the await below, which a
376
+ // session:created could otherwise land inside.
377
+ if (options.auto !== true) this._retireUrlSession?.();
378
+
368
379
  if (!this.webviewOrder.includes(id)) {
369
380
  this.webviewOrder.push(id);
370
381
  this._persistWebviewOrder();
@@ -513,7 +524,7 @@ Object.assign(CodemanApp.prototype, {
513
524
  this.activeWebviewId = null;
514
525
  const next = this.webviewOrder[0];
515
526
  if (next) {
516
- this.openWebview(next);
527
+ this.openWebview(next, { auto: true });
517
528
  } else {
518
529
  this._hideWebviewLayer();
519
530
  // Fall back to whatever session was last shown, or the welcome screen.
Binary file
Binary file
@@ -1 +1 @@
1
- {"version":3,"file":"file-routes.d.ts","sourceRoot":"","sources":["../../../src/web/routes/file-routes.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,eAAe,EAAqB,MAAM,SAAS,CAAC;AAyD7D,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAsoC5E,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,IAAI,CAkzCxG"}
1
+ {"version":3,"file":"file-routes.d.ts","sourceRoot":"","sources":["../../../src/web/routes/file-routes.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,eAAe,EAAqB,MAAM,SAAS,CAAC;AAyD7D,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAsoC5E,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,IAAI,CAmzCxG"}
@@ -1363,7 +1363,7 @@ export function registerFileRoutes(app, ctx) {
1363
1363
  // breadth of formats the attachments viewer renders (image/audio/video/pdf)
1364
1364
  // so the file viewer can open the same files.
1365
1365
  const ext = filePath.split('.').pop()?.toLowerCase() || '';
1366
- const imageExts = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'ico']);
1366
+ const imageExts = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'svg', 'bmp', 'ico']);
1367
1367
  // Shared with the attachment registry so a video plays the same whether it
1368
1368
  // sits in the workspace or is reached by id from outside it.
1369
1369
  const videoExts = VIDEO_ATTACHMENT_EXTENSIONS;
@@ -1642,6 +1642,7 @@ export function registerFileRoutes(app, ctx) {
1642
1642
  jpeg: 'image/jpeg',
1643
1643
  gif: 'image/gif',
1644
1644
  webp: 'image/webp',
1645
+ avif: 'image/avif',
1645
1646
  ico: 'image/x-icon',
1646
1647
  bmp: 'image/bmp',
1647
1648
  mp4: 'video/mp4',