aicodeman 1.40.0 → 1.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -7
- package/dist/cli-agent.d.ts +225 -0
- package/dist/cli-agent.d.ts.map +1 -0
- package/dist/cli-agent.js +767 -0
- package/dist/cli-agent.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -30
- package/dist/cli.js.map +1 -1
- package/dist/codeman-credentials.d.ts +27 -0
- package/dist/codeman-credentials.d.ts.map +1 -0
- package/dist/codeman-credentials.js +66 -0
- package/dist/codeman-credentials.js.map +1 -0
- package/dist/config/cli-registry/schema.d.ts +2 -0
- package/dist/config/cli-registry/schema.d.ts.map +1 -1
- package/dist/config/cli-registry/schema.js +3 -0
- package/dist/config/cli-registry/schema.js.map +1 -1
- package/dist/config/cli-registry/stock.d.ts.map +1 -1
- package/dist/config/cli-registry/stock.js +5 -0
- package/dist/config/cli-registry/stock.js.map +1 -1
- package/dist/config/cli-registry/types.d.ts +12 -1
- package/dist/config/cli-registry/types.d.ts.map +1 -1
- package/dist/git-clone.d.ts.map +1 -1
- package/dist/git-clone.js +4 -0
- package/dist/git-clone.js.map +1 -1
- package/dist/image-watcher.d.ts.map +1 -1
- package/dist/image-watcher.js +6 -2
- package/dist/image-watcher.js.map +1 -1
- package/dist/mcp-sync-targets.d.ts +34 -0
- package/dist/mcp-sync-targets.d.ts.map +1 -0
- package/dist/mcp-sync-targets.js +55 -0
- package/dist/mcp-sync-targets.js.map +1 -0
- package/dist/mcp-sync.d.ts +2 -1
- package/dist/mcp-sync.d.ts.map +1 -1
- package/dist/mcp-sync.js +62 -1
- package/dist/mcp-sync.js.map +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +10 -4
- package/dist/session.js.map +1 -1
- package/dist/subagent-watcher.d.ts.map +1 -1
- package/dist/subagent-watcher.js +29 -6
- package/dist/subagent-watcher.js.map +1 -1
- package/dist/templates/case-template.md +2 -0
- package/dist/tmux-manager.d.ts +5 -0
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +47 -22
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tui/tui-client.d.ts +3 -17
- package/dist/tui/tui-client.d.ts.map +1 -1
- package/dist/tui/tui-client.js +3 -44
- package/dist/tui/tui-client.js.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/process-exit-wait.d.ts +40 -0
- package/dist/utils/process-exit-wait.d.ts.map +1 -0
- package/dist/utils/process-exit-wait.js +68 -0
- package/dist/utils/process-exit-wait.js.map +1 -0
- package/dist/web/paste-image-gc.d.ts +46 -6
- package/dist/web/paste-image-gc.d.ts.map +1 -1
- package/dist/web/paste-image-gc.js +106 -32
- package/dist/web/paste-image-gc.js.map +1 -1
- package/dist/web/public/admin-ui.js.gz +0 -0
- package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
- package/dist/web/public/app.6f8552e1.js +43 -0
- package/dist/web/public/app.6f8552e1.js.br +0 -0
- package/dist/web/public/app.6f8552e1.js.gz +0 -0
- package/dist/web/public/approvals-ui.js.gz +0 -0
- package/dist/web/public/{constants.c0f8f9de.js → constants.bc9c1f63.js} +295 -28
- package/dist/web/public/constants.bc9c1f63.js.br +0 -0
- package/dist/web/public/constants.bc9c1f63.js.gz +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/entrance-animations.js +490 -43
- package/dist/web/public/entrance-animations.js.br +0 -0
- package/dist/web/public/entrance-animations.js.gz +0 -0
- package/dist/web/public/git-status-ui.js.gz +0 -0
- package/dist/web/public/home-sessions.js +2 -1
- package/dist/web/public/home-sessions.js.br +0 -0
- package/dist/web/public/home-sessions.js.gz +0 -0
- package/dist/web/public/host-wake-ui.js.gz +0 -0
- package/dist/web/public/i18n.a9840ea2.js +2 -0
- package/dist/web/public/i18n.a9840ea2.js.br +0 -0
- package/dist/web/public/i18n.a9840ea2.js.gz +0 -0
- package/dist/web/public/{image-input.898a538b.js → image-input.e00471c6.js} +3 -1
- package/dist/web/public/image-input.e00471c6.js.br +0 -0
- package/dist/web/public/image-input.e00471c6.js.gz +0 -0
- package/dist/web/public/index.html +128 -33
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.c1e7c7b7.js.gz +0 -0
- package/dist/web/public/mobile-handlers.0ef9c311.js.gz +0 -0
- package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
- package/dist/web/public/mobile-overview.js.gz +0 -0
- package/dist/web/public/mobile.111b187d.css.gz +0 -0
- package/dist/web/public/{notification-manager.50bb87d8.js → notification-manager.feb82c04.js} +19 -2
- package/dist/web/public/notification-manager.feb82c04.js.br +0 -0
- package/dist/web/public/notification-manager.feb82c04.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/{panels-ui.4735dfc2.js → panels-ui.71846fcb.js} +1 -1
- package/dist/web/public/panels-ui.71846fcb.js.br +0 -0
- package/dist/web/public/panels-ui.71846fcb.js.gz +0 -0
- package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
- package/dist/web/public/ralph-wizard.b6c0dd19.js.gz +0 -0
- package/dist/web/public/readmymind-ui.js.gz +0 -0
- package/dist/web/public/reboot-restore-ui.js.gz +0 -0
- package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
- package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
- package/dist/web/public/session-lineage.js.gz +0 -0
- package/dist/web/public/session-ui.f731602c.js.gz +0 -0
- package/dist/web/public/settings-ui.4e33390f.js +97 -0
- package/dist/web/public/settings-ui.4e33390f.js.br +0 -0
- package/dist/web/public/settings-ui.4e33390f.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview.af82fa2f.js.gz +0 -0
- package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
- package/dist/web/public/styles.81a72906.css +1 -0
- package/dist/web/public/styles.81a72906.css.br +0 -0
- package/dist/web/public/styles.81a72906.css.gz +0 -0
- package/dist/web/public/{subagent-windows.e6ca799f.js → subagent-windows.07277a58.js} +7 -5
- package/dist/web/public/subagent-windows.07277a58.js.br +0 -0
- package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
- package/dist/web/public/sw.js +2 -2
- package/dist/web/public/sw.js.br +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/tab-layout-browser.065fcba4.js.gz +0 -0
- package/dist/web/public/tab-rail-resize.a5867793.js.gz +0 -0
- package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.gz +0 -0
- package/dist/web/public/terminal-split.js.gz +0 -0
- package/dist/web/public/terminal-tile.js +92 -15
- package/dist/web/public/terminal-tile.js.br +0 -0
- package/dist/web/public/terminal-tile.js.gz +0 -0
- package/dist/web/public/terminal-ui.b7267005.js +2 -0
- package/dist/web/public/terminal-ui.b7267005.js.br +0 -0
- package/dist/web/public/terminal-ui.b7267005.js.gz +0 -0
- package/dist/web/public/tile-grid.js +330 -119
- package/dist/web/public/tile-grid.js.br +0 -0
- package/dist/web/public/tile-grid.js.gz +0 -0
- package/dist/web/public/ultracode-panel.js.gz +0 -0
- package/dist/web/public/ultracode-windows.js +9 -6
- package/dist/web/public/ultracode-windows.js.br +0 -0
- package/dist/web/public/ultracode-windows.js.gz +0 -0
- package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
- package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
- package/dist/web/public/vendor/fflate.min.js.gz +0 -0
- package/dist/web/public/vendor/marked.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-predictive-echo.ae5e3d5d.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js +4 -0
- package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.br +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.gz +0 -0
- package/dist/web/public/vendor/xterm.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/voice-input.2342c37b.js.gz +0 -0
- package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
- package/dist/web/public/webview-tabs.js.gz +0 -0
- package/dist/web/routes/cli-registry-routes.d.ts +23 -4
- package/dist/web/routes/cli-registry-routes.d.ts.map +1 -1
- package/dist/web/routes/cli-registry-routes.js +92 -7
- package/dist/web/routes/cli-registry-routes.js.map +1 -1
- package/dist/web/routes/mcp-sync-routes.d.ts +7 -2
- package/dist/web/routes/mcp-sync-routes.d.ts.map +1 -1
- package/dist/web/routes/mcp-sync-routes.js +10 -3
- package/dist/web/routes/mcp-sync-routes.js.map +1 -1
- package/dist/web/routes/reboot-restore-routes.js +1 -1
- package/dist/web/routes/reboot-restore-routes.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +68 -35
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +3 -0
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +9 -0
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/server.d.ts +1 -1
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +15 -10
- package/dist/web/server.js.map +1 -1
- package/package.json +4 -4
- package/skills/codeman/SKILL.md +6 -0
- package/README.zh-CN.md +0 -1147
- package/dist/web/public/app.abadc970.js +0 -43
- package/dist/web/public/app.abadc970.js.br +0 -0
- package/dist/web/public/app.abadc970.js.gz +0 -0
- package/dist/web/public/constants.c0f8f9de.js.br +0 -0
- package/dist/web/public/constants.c0f8f9de.js.gz +0 -0
- package/dist/web/public/i18n.a31747a9.js +0 -2
- package/dist/web/public/i18n.a31747a9.js.br +0 -0
- package/dist/web/public/i18n.a31747a9.js.gz +0 -0
- package/dist/web/public/image-input.898a538b.js.br +0 -0
- package/dist/web/public/image-input.898a538b.js.gz +0 -0
- package/dist/web/public/notification-manager.50bb87d8.js.br +0 -0
- package/dist/web/public/notification-manager.50bb87d8.js.gz +0 -0
- package/dist/web/public/panels-ui.4735dfc2.js.br +0 -0
- package/dist/web/public/panels-ui.4735dfc2.js.gz +0 -0
- package/dist/web/public/settings-ui.a97ab16f.js +0 -97
- package/dist/web/public/settings-ui.a97ab16f.js.br +0 -0
- package/dist/web/public/settings-ui.a97ab16f.js.gz +0 -0
- package/dist/web/public/styles.8ee12666.css +0 -1
- package/dist/web/public/styles.8ee12666.css.br +0 -0
- package/dist/web/public/styles.8ee12666.css.gz +0 -0
- package/dist/web/public/subagent-windows.e6ca799f.js.br +0 -0
- package/dist/web/public/subagent-windows.e6ca799f.js.gz +0 -0
- package/dist/web/public/terminal-ui.5059b30e.js +0 -2
- package/dist/web/public/terminal-ui.5059b30e.js.br +0 -0
- package/dist/web/public/terminal-ui.5059b30e.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.bbb5f84c.js +0 -4
- package/dist/web/public/vendor/xterm-zerolag-input.bbb5f84c.js.br +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.bbb5f84c.js.gz +0 -0
|
@@ -23,14 +23,18 @@
|
|
|
23
23
|
|
|
24
24
|
// Per-device tile font size (a tile is a fraction of the screen).
|
|
25
25
|
const TILE_GRID_FONT_KEY = 'codeman-tile-font-size';
|
|
26
|
-
// The grid this
|
|
27
|
-
// `{ v: 1, open, ids, focused, zoomed, colFr, rowFr }`,
|
|
28
|
-
// in reading order with `null` for an empty one
|
|
29
|
-
//
|
|
26
|
+
// The grid this browser last had, as the user left it (sanitizeTileGridState,
|
|
27
|
+
// constants.js): `{ v: 1, open, ids, count, focused, zoomed, colFr, rowFr }`,
|
|
28
|
+
// `ids` being the cells in reading order with `null` for an empty one, `count`
|
|
29
|
+
// how many tiles the user's own last change left (a session that went away by
|
|
30
|
+
// itself does not lower it). Session ids and layout only, never content.
|
|
31
|
+
// Written on every change while the grid is open; `open: false` keeps it for
|
|
32
|
+
// the Tiles toggle, which brings it back exactly; a grid stored open is
|
|
33
|
+
// restored on reload inside handleInit. Never sent to the server.
|
|
30
34
|
const TILE_GRID_STORAGE_KEY = 'codeman:tile-grid';
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
35
|
+
// The count last picked in the Tiles button's right-click menu (2, 4 or 6;
|
|
36
|
+
// owner decision 10), per device: what a click opens when there is no stored
|
|
37
|
+
// grid to bring back, and what a pick re-forms the grid to.
|
|
34
38
|
const TILE_GRID_COUNT_KEY = 'codeman:tile-count';
|
|
35
39
|
// The leaving tiles' fade (styles.css .tile--leaving) plus slack: the still
|
|
36
40
|
// copy of a closing grid goes even if animationend never comes (a hidden tab,
|
|
@@ -147,6 +151,11 @@ class TileGridModel {
|
|
|
147
151
|
// session id, or null for an empty cell. THE source of truth for where
|
|
148
152
|
// each tile is (owner: an empty cell can be any cell); `ids` derives from it.
|
|
149
153
|
this.cells = [];
|
|
154
|
+
// How many tiles the user's own last change left (open, add, remove, a
|
|
155
|
+
// count picked): a session that goes away by itself (deleted, popped out,
|
|
156
|
+
// refused) does not lower it, so the next time the grid opens the ranking
|
|
157
|
+
// fills that place. Stored with the grid (_persistTileGrid).
|
|
158
|
+
this.count = 0;
|
|
150
159
|
// id -> { tile: TerminalTile, el: HTMLElement }
|
|
151
160
|
this.tiles = new Map();
|
|
152
161
|
this.focusedId = null;
|
|
@@ -237,8 +246,15 @@ Object.assign(CodemanApp.prototype, {
|
|
|
237
246
|
if (state !== 'idle') this._setTileLoadingLabel(entry.body);
|
|
238
247
|
entry.el.classList.toggle('tile--loading', state !== 'idle');
|
|
239
248
|
// The first capture has landed (or failed): the terminal fades in,
|
|
240
|
-
// whole, instead of showing its replay scroll by
|
|
241
|
-
|
|
249
|
+
// whole, instead of showing its replay scroll by, or plays the
|
|
250
|
+
// terminal pane's entrance style (entrance-animations.js).
|
|
251
|
+
if (state === 'idle') {
|
|
252
|
+
entry.el.classList.remove('tile--revealing');
|
|
253
|
+
if (entry.screenOwed) {
|
|
254
|
+
entry.screenOwed = false;
|
|
255
|
+
this.playTileScreenEntrance?.(entry.body);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
242
258
|
},
|
|
243
259
|
});
|
|
244
260
|
}
|
|
@@ -389,6 +405,9 @@ Object.assign(CodemanApp.prototype, {
|
|
|
389
405
|
wanted.forEach((id, k) => this._mountTile(id, { enterIndex: k }));
|
|
390
406
|
// Packed from the first cell (_applyTileLayout pads the shape with empty cells).
|
|
391
407
|
grid.cells = wanted.filter((id) => grid.tiles.has(id));
|
|
408
|
+
// What the user opened is the grid they want (_openStoredTileGrid keeps a
|
|
409
|
+
// stored count instead).
|
|
410
|
+
grid.count = grid.ids.length;
|
|
392
411
|
this._applyTileLayout();
|
|
393
412
|
// The tiles' frames paint first; their terminals are built one per frame
|
|
394
413
|
// after it, the focused tile's first, so its capture is the one the queue
|
|
@@ -412,11 +431,13 @@ Object.assign(CodemanApp.prototype, {
|
|
|
412
431
|
* Leaves the grid: every tile destroyed (sockets closed, xterms disposed,
|
|
413
432
|
* queued loads dropped), the main terminal unparked.
|
|
414
433
|
*
|
|
415
|
-
* `keepStored`
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
434
|
+
* `keepStored` (every caller) keeps the grid as it was, holes, count, sizes,
|
|
435
|
+
* focus and zoom, closed, for the Tiles toggle to bring back (a reload
|
|
436
|
+
* restores only a grid stored open); false would forget it. `reselect` shows the focused session in the single
|
|
437
|
+
* view through a forced reload; pass false when the caller selects
|
|
438
|
+
* something itself. `animate` (the Tiles toggle only, owner answer 4)
|
|
439
|
+
* leaves a still copy of the tiles over the stage until the single view has
|
|
440
|
+
* its content (_ghostTileGrid).
|
|
420
441
|
*
|
|
421
442
|
* The main terminal's cached content for EVERY tiled id is invalidated: it was
|
|
422
443
|
* written before the grid opened, possibly hours ago, and selectSession paints
|
|
@@ -457,6 +478,7 @@ Object.assign(CodemanApp.prototype, {
|
|
|
457
478
|
grid.colFr = [];
|
|
458
479
|
grid.rowFr = [];
|
|
459
480
|
grid.cells = [];
|
|
481
|
+
grid.count = 0;
|
|
460
482
|
grid.cols = 0;
|
|
461
483
|
grid.rows = 0;
|
|
462
484
|
grid.focusedId = null;
|
|
@@ -536,14 +558,28 @@ Object.assign(CodemanApp.prototype, {
|
|
|
536
558
|
// Zoomed, only the zoomed tile is on screen (the others are display: none
|
|
537
559
|
// under .tile-grid--zoomed, a rule the ghost layer does not carry).
|
|
538
560
|
const zoomed = section.classList.contains('tile-grid--zoomed');
|
|
561
|
+
const copies = [];
|
|
539
562
|
for (const id of grid.ids) {
|
|
540
563
|
const el = grid.tiles.get(id)?.el;
|
|
541
564
|
if (!el || (zoomed && !el.classList.contains('tile--zoomed'))) continue;
|
|
542
565
|
const ghost = el.cloneNode(true);
|
|
543
|
-
ghost.classList.remove(
|
|
566
|
+
ghost.classList.remove(
|
|
567
|
+
'tile--entering',
|
|
568
|
+
'tile--enter-themed',
|
|
569
|
+
'tile--enter-hold',
|
|
570
|
+
'tile--needs',
|
|
571
|
+
'tile--loading',
|
|
572
|
+
'tile--drop-target',
|
|
573
|
+
'tile--dragging'
|
|
574
|
+
);
|
|
575
|
+
ghost.querySelector?.('.tile-body')?.classList.remove('term-enter');
|
|
544
576
|
ghost.classList.add('tile--leaving');
|
|
545
577
|
layer.appendChild(ghost);
|
|
578
|
+
copies.push({ ghost, el, sessionId: id });
|
|
546
579
|
}
|
|
580
|
+
// The entrance style's own way out (entrance-animations.js): back into the
|
|
581
|
+
// tabs, a CRT switch-off. Placed while the real tiles are still measurable.
|
|
582
|
+
const leaveMs = this._stageTileExit?.(copies, { now: !hold }) || 0;
|
|
547
583
|
main.appendChild(layer);
|
|
548
584
|
let fallback = null;
|
|
549
585
|
let holdCap = null;
|
|
@@ -557,10 +593,11 @@ Object.assign(CodemanApp.prototype, {
|
|
|
557
593
|
if (fallback !== null || !layer.isConnected) return;
|
|
558
594
|
clearTimeout(holdCap);
|
|
559
595
|
layer.classList.add('tile-grid-ghosts--release');
|
|
560
|
-
fallback = setTimeout(done, TILE_GHOST_FALLBACK_MS);
|
|
596
|
+
fallback = setTimeout(done, Math.max(TILE_GHOST_FALLBACK_MS, leaveMs));
|
|
561
597
|
};
|
|
562
598
|
layer.addEventListener('animationend', (e) => {
|
|
563
|
-
if (
|
|
599
|
+
if (e.pseudoElement || e.target !== layer.lastElementChild) return;
|
|
600
|
+
if (/^tile-leave/.test(e.animationName)) done();
|
|
564
601
|
});
|
|
565
602
|
if (hold) holdCap = setTimeout(release, TILE_GHOST_HOLD_MAX_MS);
|
|
566
603
|
else release();
|
|
@@ -695,8 +732,11 @@ Object.assign(CodemanApp.prototype, {
|
|
|
695
732
|
|
|
696
733
|
/**
|
|
697
734
|
* The card's text, from the remembered count, the grid's state and what the
|
|
698
|
-
* window fits.
|
|
699
|
-
*
|
|
735
|
+
* window fits. With the grid closed and a stored grid to bring back, the
|
|
736
|
+
* click opens that grid as it was (never trimmed to the window), so the
|
|
737
|
+
* window line only says what fits. Compared with the last English text set,
|
|
738
|
+
* never the DOM (in zh-CN the DOM holds the translation; see
|
|
739
|
+
* _renderTileOverlay).
|
|
700
740
|
*/
|
|
701
741
|
_renderTileHint() {
|
|
702
742
|
const hint = this._tileHint;
|
|
@@ -712,10 +752,12 @@ Object.assign(CodemanApp.prototype, {
|
|
|
712
752
|
set(hint.title, 'titleText', `Tiles \u00B7 ${count}`);
|
|
713
753
|
set(hint.click, 'clickText', open ? 'Click: close the grid' : 'Click: open the grid');
|
|
714
754
|
const plural = capacity === 1 ? '' : 's';
|
|
755
|
+
const restoring = open ? null : this._storedTileGridSet();
|
|
756
|
+
const opens = restoring ? restoring.ids.length : count;
|
|
715
757
|
const fitsText =
|
|
716
|
-
|
|
758
|
+
opens <= capacity
|
|
717
759
|
? ''
|
|
718
|
-
: open
|
|
760
|
+
: open || restoring
|
|
719
761
|
? `This window fits ${capacity} tile${plural}`
|
|
720
762
|
: `This window fits ${capacity} tile${plural}: a click opens ${capacity}`;
|
|
721
763
|
set(hint.fits.text, 'fitsText', fitsText);
|
|
@@ -976,22 +1018,25 @@ Object.assign(CodemanApp.prototype, {
|
|
|
976
1018
|
|
|
977
1019
|
/**
|
|
978
1020
|
* A count picked in the menu: remembered for the click, then the grid opens
|
|
979
|
-
* with that many tiles (
|
|
980
|
-
*
|
|
981
|
-
* sessions than the count give
|
|
1021
|
+
* with that many tiles (a stored grid re-formed to it, its tiles first in
|
|
1022
|
+
* their cells), or the open grid is re-formed to it (_reformTileGrid).
|
|
1023
|
+
* Never more than the window fits; fewer open sessions than the count give
|
|
1024
|
+
* fewer tiles. Either way the grid's count is what the pick left.
|
|
982
1025
|
*/
|
|
983
1026
|
_pickTileCount(count) {
|
|
984
1027
|
this.closeTileCountMenu({ refocus: false });
|
|
985
1028
|
this._rememberTileGridCount(count);
|
|
986
1029
|
const grid = this._tileGrid;
|
|
987
1030
|
if (!grid?.open) {
|
|
988
|
-
this.
|
|
1031
|
+
this._activateTileGrid({ count });
|
|
989
1032
|
return;
|
|
990
1033
|
}
|
|
991
1034
|
const T = window.CodemanTileGrid;
|
|
992
1035
|
const n = Math.min(T.sanitizeTileCount(count), this._tileGridLimit().capacity);
|
|
993
|
-
|
|
994
|
-
this._reformTileGrid(T.tileGridSetForCount(grid.ids,
|
|
1036
|
+
// The tiles that join are the ranking's best (_tileGridRanking).
|
|
1037
|
+
this._reformTileGrid(T.tileGridSetForCount(grid.ids, this._tileGridRanking(), n, grid.focusedId));
|
|
1038
|
+
grid.count = grid.ids.length;
|
|
1039
|
+
this._persistTileGrid();
|
|
995
1040
|
},
|
|
996
1041
|
|
|
997
1042
|
/**
|
|
@@ -1126,14 +1171,18 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1126
1171
|
/**
|
|
1127
1172
|
* The Tiles button's click and Ctrl+Shift+G, one function so the two never
|
|
1128
1173
|
* drift (owner decision 8): opens the grid at once, no menu in the way, or
|
|
1129
|
-
* closes it to the single view of the focused session.
|
|
1130
|
-
*
|
|
1131
|
-
*
|
|
1132
|
-
*
|
|
1133
|
-
*
|
|
1134
|
-
*
|
|
1135
|
-
*
|
|
1136
|
-
*
|
|
1174
|
+
* closes it to the single view of the focused session. The grid this
|
|
1175
|
+
* browser last had comes back EXACTLY as the user left it (owner request:
|
|
1176
|
+
* "when I turn tiles off and on, always keep what the last setting was"):
|
|
1177
|
+
* its tiles in their cells, holes included, its count, divider sizes, focus
|
|
1178
|
+
* and zoom; a cell whose session no longer exists is filled from the
|
|
1179
|
+
* ranking (_openStoredTileGrid). With none of its sessions left (or nothing
|
|
1180
|
+
* stored), `tileGridOpenSet` (constants.js) takes an open split's two
|
|
1181
|
+
* sessions, else the open sessions as the ranking orders them
|
|
1182
|
+
* (_tileGridRanking: working, then needing input, then the most recent),
|
|
1183
|
+
* the active one always among them and focused, filled to the remembered
|
|
1184
|
+
* count (the right-click menu's last pick, default 6, at most what the
|
|
1185
|
+
* window fits; owner decision 10).
|
|
1137
1186
|
*/
|
|
1138
1187
|
toggleTileGrid() {
|
|
1139
1188
|
this.closeTileCountMenu();
|
|
@@ -1141,28 +1190,43 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1141
1190
|
this.closeTileGrid({ keepStored: true, reselect: true, animate: true });
|
|
1142
1191
|
return;
|
|
1143
1192
|
}
|
|
1144
|
-
|
|
1193
|
+
this._activateTileGrid();
|
|
1194
|
+
},
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* Opens the grid as the toggle does (toggleTileGrid), or, with `count` (a
|
|
1198
|
+
* pick in the count menu while the grid is closed), with that many tiles: a
|
|
1199
|
+
* stored grid re-formed to it, its tiles first in their cells, the ones the
|
|
1200
|
+
* count adds filling its empty cells first, the rest from the ranking.
|
|
1201
|
+
*
|
|
1202
|
+
* @returns {boolean} whether the grid opened
|
|
1203
|
+
*/
|
|
1204
|
+
_activateTileGrid({ count = null } = {}) {
|
|
1205
|
+
if (!this.canOpenTileGrid()) return false;
|
|
1145
1206
|
const stored = this._readStoredTileGrid();
|
|
1146
|
-
const set = this._tileGridOpenSet(stored);
|
|
1207
|
+
const set = this._tileGridOpenSet(stored, count);
|
|
1147
1208
|
if (!set) {
|
|
1148
1209
|
this.showToast?.('No sessions to show as tiles', 'info');
|
|
1149
|
-
return;
|
|
1210
|
+
return false;
|
|
1150
1211
|
}
|
|
1151
|
-
if (set.source === 'stored') this._openStoredTileGrid(stored, set
|
|
1152
|
-
|
|
1212
|
+
if (set.source === 'stored') return this._openStoredTileGrid(stored, set, { keepCount: count === null });
|
|
1213
|
+
return this.openTileGrid(set.ids, { focusedId: set.focusedId });
|
|
1153
1214
|
},
|
|
1154
1215
|
|
|
1155
1216
|
/**
|
|
1156
|
-
* What the toggle would open now (see toggleTileGrid):
|
|
1157
|
-
*
|
|
1158
|
-
*
|
|
1217
|
+
* What the toggle would open now (see toggleTileGrid): a stored grid as it
|
|
1218
|
+
* was, or `count` tiles (the remembered count by default), at most what the
|
|
1219
|
+
* window fits. With an explicit `count`, a stored grid is trimmed or filled
|
|
1220
|
+
* to it too. `stored` saves a second read.
|
|
1159
1221
|
*/
|
|
1160
|
-
_tileGridOpenSet(stored = this._readStoredTileGrid(), count =
|
|
1222
|
+
_tileGridOpenSet(stored = this._readStoredTileGrid(), count = null) {
|
|
1161
1223
|
const T = window.CodemanTileGrid;
|
|
1162
|
-
const n = Math.max(1, Math.min(count, this._tileGridLimit().capacity));
|
|
1224
|
+
const n = Math.max(1, Math.min(count ?? this._tileGridCount(), this._tileGridLimit().capacity));
|
|
1225
|
+
const ranked = this._tileGridRanking();
|
|
1163
1226
|
const set = T.tileGridOpenSet({
|
|
1164
1227
|
stored,
|
|
1165
1228
|
split: this._splitPane ? [this.activeSessionId, this._splitSessionId] : null,
|
|
1229
|
+
ranked,
|
|
1166
1230
|
sessions: this.sessions,
|
|
1167
1231
|
sessionOrder: this.sessionOrder,
|
|
1168
1232
|
detachedIds: this.detachedSessions,
|
|
@@ -1170,8 +1234,42 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1170
1234
|
limit: n,
|
|
1171
1235
|
});
|
|
1172
1236
|
if (!set) return null;
|
|
1173
|
-
|
|
1174
|
-
|
|
1237
|
+
// A stored grid comes back as it was: the count does not apply to it
|
|
1238
|
+
// unless one was asked for.
|
|
1239
|
+
if (set.source === 'stored' && count === null) return set;
|
|
1240
|
+
return { ...set, ids: T.tileGridSetForCount(set.ids, ranked, n, set.focusedId) };
|
|
1241
|
+
},
|
|
1242
|
+
|
|
1243
|
+
/**
|
|
1244
|
+
* The open sessions the grid takes when nobody said which (the Tiles button
|
|
1245
|
+
* with nothing stored, and every tile it fills on its own), best first:
|
|
1246
|
+
* working (the most recently started turn first), then the ones needing
|
|
1247
|
+
* input (the red and yellow tab alerts), then the rest by most recent
|
|
1248
|
+
* activity, tab order breaking ties (rankTileSessions, constants.js). The
|
|
1249
|
+
* states and stamps are the home screens' own (`_mobileOverviewState()`,
|
|
1250
|
+
* mobile-overview.js). Detached sessions are never in it. Guarded like
|
|
1251
|
+
* the sorted rail: without the classifier (a stale cached
|
|
1252
|
+
* mobile-overview.js) it is plain tab order.
|
|
1253
|
+
*
|
|
1254
|
+
* @returns {string[]}
|
|
1255
|
+
*/
|
|
1256
|
+
_tileGridRanking() {
|
|
1257
|
+
const T = window.CodemanTileGrid;
|
|
1258
|
+
const open = T.buildTilePickerSessions(this.sessions, this.sessionOrder, this.detachedSessions);
|
|
1259
|
+
if (typeof this._mobileOverviewState !== 'function' || typeof T.rankTileSessions !== 'function') {
|
|
1260
|
+
return open.map((c) => c.id);
|
|
1261
|
+
}
|
|
1262
|
+
const rows = open.map(({ id }, orderIndex) => {
|
|
1263
|
+
const session = this.sessions.get(id);
|
|
1264
|
+
return {
|
|
1265
|
+
id,
|
|
1266
|
+
state: this._mobileOverviewState(session, this.pendingHooks?.get(id)),
|
|
1267
|
+
lastActivityAt: Number(session.lastActivityAt) || 0,
|
|
1268
|
+
lastSubmitAt: Number(session.lastSubmitAt) || 0,
|
|
1269
|
+
orderIndex,
|
|
1270
|
+
};
|
|
1271
|
+
});
|
|
1272
|
+
return T.rankTileSessions(rows);
|
|
1175
1273
|
},
|
|
1176
1274
|
|
|
1177
1275
|
/** Alt+Shift+Arrows: a human selection of the tile in that direction. */
|
|
@@ -1242,6 +1340,7 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1242
1340
|
if (grid.ids.length >= window.CodemanTileGrid.TILE_GRID_MAX) return false;
|
|
1243
1341
|
if (!this._mountTile(sessionId, { enterIndex })) return false;
|
|
1244
1342
|
this._placeTile(sessionId, cell);
|
|
1343
|
+
grid.count = grid.ids.length;
|
|
1245
1344
|
// A tile added while one is zoomed by hand is meant to be seen.
|
|
1246
1345
|
if (grid.zoomedId && !grid.autoZoom) grid.zoomedId = null;
|
|
1247
1346
|
this._applyTileLayout();
|
|
@@ -1286,17 +1385,20 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1286
1385
|
* moves focus to the neighbouring tile (next in grid order, else previous),
|
|
1287
1386
|
* as the app's choice (`auto`: no idle alert is spent); `focus: false` keeps
|
|
1288
1387
|
* DOM focus where it is (an app-driven removal: a socket the server closed),
|
|
1289
|
-
* so keystrokes never land in the neighbour's PTY unasked.
|
|
1290
|
-
*
|
|
1291
|
-
*
|
|
1292
|
-
* the
|
|
1388
|
+
* so keystrokes never land in the neighbour's PTY unasked. `gone`: the
|
|
1389
|
+
* session went away by itself (deleted, popped out, its socket refused), not
|
|
1390
|
+
* by the user's hand, so the grid's count stays and the next time it opens
|
|
1391
|
+
* the ranking fills that place. The last tile leaving closes the grid, kept
|
|
1392
|
+
* as it was: with `refocus` the single view then shows that session (or,
|
|
1393
|
+
* popped out, the next one: _selectAfterTileGrid), without it the caller
|
|
1394
|
+
* decides what comes next.
|
|
1293
1395
|
*/
|
|
1294
|
-
removeTile(sessionId, { refocus = true, focus = true } = {}) {
|
|
1396
|
+
removeTile(sessionId, { refocus = true, focus = true, gone = false } = {}) {
|
|
1295
1397
|
const grid = this._tileGrid;
|
|
1296
1398
|
const entry = grid?.open ? grid.tiles.get(sessionId) : null;
|
|
1297
1399
|
if (!entry) return false;
|
|
1298
1400
|
if (grid.ids.length === 1) {
|
|
1299
|
-
this.closeTileGrid({ keepStored:
|
|
1401
|
+
this.closeTileGrid({ keepStored: true, reselect: refocus });
|
|
1300
1402
|
return true;
|
|
1301
1403
|
}
|
|
1302
1404
|
const wasFocused = grid.focusedId === sessionId;
|
|
@@ -1312,6 +1414,7 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1312
1414
|
entry.el.remove();
|
|
1313
1415
|
grid.tiles.delete(sessionId);
|
|
1314
1416
|
grid.cells[grid.cells.indexOf(sessionId)] = null;
|
|
1417
|
+
if (!gone) grid.count = grid.ids.length;
|
|
1315
1418
|
// Before the layout, which may zoom the focused tile on a small window.
|
|
1316
1419
|
if (wasFocused) grid.focusedId = null;
|
|
1317
1420
|
this._applyTileLayout();
|
|
@@ -1323,11 +1426,10 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1323
1426
|
|
|
1324
1427
|
/**
|
|
1325
1428
|
* Builds one tile (header, body, TerminalTile); where it goes is the
|
|
1326
|
-
* caller's (grid.cells). It enters
|
|
1327
|
-
* (
|
|
1328
|
-
*
|
|
1329
|
-
*
|
|
1330
|
-
* Opacity and transform only, never anything its fit reads.
|
|
1429
|
+
* caller's (grid.cells). It enters as the `enterIndex`-th of a staggered
|
|
1430
|
+
* group (_beginTileEntrance), and its terminal stays transparent until its
|
|
1431
|
+
* first capture lands (.tile--revealing, cleared by the load queue, with a
|
|
1432
|
+
* backstop timer). Opacity and transform only, never anything its fit reads.
|
|
1331
1433
|
*/
|
|
1332
1434
|
_mountTile(sessionId, { enterIndex = 0 } = {}) {
|
|
1333
1435
|
const grid = this._tileGrid;
|
|
@@ -1336,16 +1438,11 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1336
1438
|
const el = document.createElement('div');
|
|
1337
1439
|
el.className = 'tile tile--revealing';
|
|
1338
1440
|
setTimeout(() => el.classList.remove('tile--revealing'), TILE_REVEAL_FALLBACK_MS);
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
el.removeEventListener('animationend', onEnd);
|
|
1345
|
-
el.classList.remove('tile--entering');
|
|
1346
|
-
};
|
|
1347
|
-
el.addEventListener('animationend', onEnd);
|
|
1348
|
-
}
|
|
1441
|
+
// Its screen plays the terminal pane's entrance once its first capture
|
|
1442
|
+
// lands, when its frame entered in a tile style (Tile Animations): never
|
|
1443
|
+
// with the default `settle`, nor on a reload's restore, which settles.
|
|
1444
|
+
const entered = this._beginTileEntrance(el, sessionId, enterIndex);
|
|
1445
|
+
const screenOwed = !!entered && entered !== 'settle';
|
|
1349
1446
|
el.dataset.sessionId = sessionId;
|
|
1350
1447
|
// Header and body are siblings: the chrome is refreshed in place
|
|
1351
1448
|
// (_renderTileHeader), never by rewriting the tile, which would take the
|
|
@@ -1391,12 +1488,61 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1391
1488
|
renaming: false,
|
|
1392
1489
|
// The pid this tile last saw, so a pane that starts later is noticed.
|
|
1393
1490
|
pid: this.sessions.get(sessionId)?.pid ?? null,
|
|
1491
|
+
// Its screen plays the terminal entrance once its first capture lands.
|
|
1492
|
+
screenOwed,
|
|
1394
1493
|
});
|
|
1395
1494
|
// Where it goes is the caller's (grid.cells).
|
|
1396
1495
|
this._renderTileHeader(sessionId);
|
|
1397
1496
|
return true;
|
|
1398
1497
|
},
|
|
1399
1498
|
|
|
1499
|
+
/**
|
|
1500
|
+
* A tile's frame enters as the `enterIndex`-th of a staggered group, in the
|
|
1501
|
+
* entrance style (entrance-animations.js, App Settings → Entrance
|
|
1502
|
+
* Animations). `settle` is the grid's own fade and settle (styles.css
|
|
1503
|
+
* .tile--entering), and what a reload restores with whatever the theme:
|
|
1504
|
+
* nothing animates on page load. Any other style is timed by the entrance
|
|
1505
|
+
* module a frame later, once the layout is final (_stageTileEntrance).
|
|
1506
|
+
* Opacity and transform only, never anything the fit reads.
|
|
1507
|
+
*
|
|
1508
|
+
* @returns {string|null} the style it enters with, or null when it does not animate
|
|
1509
|
+
*/
|
|
1510
|
+
_beginTileEntrance(el, sessionId, enterIndex = 0) {
|
|
1511
|
+
const style = this._tileEnterQuiet ? 'settle' : this.tileEntranceStyle?.() || 'settle';
|
|
1512
|
+
if (!this._tileMotionAllowed() || style === 'off') return null;
|
|
1513
|
+
el.classList.add('tile--entering');
|
|
1514
|
+
el.style.setProperty('--tile-enter-index', String(enterIndex));
|
|
1515
|
+
let backstop = null;
|
|
1516
|
+
const finish = () => {
|
|
1517
|
+
clearTimeout(backstop);
|
|
1518
|
+
el.removeEventListener('animationend', onEnd);
|
|
1519
|
+
el.classList.remove('tile--entering', 'tile--enter-themed', 'tile--enter-hold');
|
|
1520
|
+
if (el._tileEnterFinish === finish) el._tileEnterFinish = null;
|
|
1521
|
+
};
|
|
1522
|
+
// The tile's own entrance (`tile-enter`, or a themed `tile-enter-*`),
|
|
1523
|
+
// never a child's animation or a wash on its ::before.
|
|
1524
|
+
const onEnd = (e) => {
|
|
1525
|
+
if (e.target !== el || e.pseudoElement || !/^tile-enter/.test(e.animationName || '')) return;
|
|
1526
|
+
finish();
|
|
1527
|
+
};
|
|
1528
|
+
el.addEventListener('animationend', onEnd);
|
|
1529
|
+
el._tileEnterFinish = finish;
|
|
1530
|
+
if (style !== 'settle') {
|
|
1531
|
+
this._stageTileEntrance?.(el, sessionId, (ms) => {
|
|
1532
|
+
clearTimeout(backstop);
|
|
1533
|
+
backstop = setTimeout(finish, ms);
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
return style;
|
|
1537
|
+
},
|
|
1538
|
+
|
|
1539
|
+
/** Plays a mounted tile's entrance again (the entrance lab's replay): nothing is remounted or refitted. */
|
|
1540
|
+
_replayTileEntrance(el, sessionId, enterIndex = 0) {
|
|
1541
|
+
el._tileEnterFinish?.();
|
|
1542
|
+
void el.offsetWidth;
|
|
1543
|
+
return this._beginTileEntrance(el, sessionId, enterIndex);
|
|
1544
|
+
},
|
|
1545
|
+
|
|
1400
1546
|
/**
|
|
1401
1547
|
* Makes `el` a drop target for a session tab dragged from the strip (the
|
|
1402
1548
|
* strip's own drag sets `draggedTabId`) and for a tile dragged by its header
|
|
@@ -1575,10 +1721,12 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1575
1721
|
* Ctrl/Cmd+click on a tab: that session joins the grid and takes focus (a
|
|
1576
1722
|
* human selection: the user clicked its tab). With the grid closed it opens
|
|
1577
1723
|
* what the Tiles toggle would, with this session among the tiles and
|
|
1578
|
-
* focused
|
|
1579
|
-
* questions
|
|
1580
|
-
*
|
|
1581
|
-
*
|
|
1724
|
+
* focused, never past the remembered count (owner answer to decision 10's
|
|
1725
|
+
* questions: N, not N+1): it joins while the grid holds fewer than the
|
|
1726
|
+
* count (a stored grid's first empty cell), else it takes the last tile's
|
|
1727
|
+
* place. A stored grid keeps its cells, sizes and holes around it.
|
|
1728
|
+
* Returns false when the grid cannot open here (narrow or solo window), so
|
|
1729
|
+
* the click is an ordinary one.
|
|
1582
1730
|
*/
|
|
1583
1731
|
addSessionToTiles(sessionId) {
|
|
1584
1732
|
if (!this.canOpenTileGrid() || !this.sessions.has(sessionId) || this.detachedSessions?.has(sessionId)) {
|
|
@@ -1599,7 +1747,26 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1599
1747
|
return true;
|
|
1600
1748
|
}
|
|
1601
1749
|
const n = Math.max(1, Math.min(this._tileGridCount(), capacity));
|
|
1602
|
-
const
|
|
1750
|
+
const stored = this._readStoredTileGrid();
|
|
1751
|
+
const plan = this._tileGridOpenSet(stored);
|
|
1752
|
+
if (plan?.source === 'stored') {
|
|
1753
|
+
let { ids, cells } = plan;
|
|
1754
|
+
if (!ids.includes(sessionId)) {
|
|
1755
|
+
if (ids.length < n) {
|
|
1756
|
+
ids = [...ids, sessionId];
|
|
1757
|
+
} else {
|
|
1758
|
+
const last = ids.at(-1);
|
|
1759
|
+
ids = ids.map((id) => (id === last ? sessionId : id));
|
|
1760
|
+
cells = cells.map((id) => (id === last ? sessionId : id));
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
return this._openStoredTileGrid(
|
|
1764
|
+
stored,
|
|
1765
|
+
{ ids, cells, focusedId: sessionId },
|
|
1766
|
+
{ focusedId: sessionId, auto: false, keepCount: false }
|
|
1767
|
+
);
|
|
1768
|
+
}
|
|
1769
|
+
const base = plan?.ids || [];
|
|
1603
1770
|
const ids = [...base.filter((id) => id !== sessionId).slice(0, n - 1), sessionId];
|
|
1604
1771
|
// Exactly these: an open split is already in `base` (tileGridOpenSet seeds
|
|
1605
1772
|
// it), and merging it again went past the count (N+1) and the window.
|
|
@@ -1642,7 +1809,8 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1642
1809
|
_replaceTileGrid(ids) {
|
|
1643
1810
|
const focus = ids.includes(this.activeSessionId) ? this.activeSessionId : ids[0];
|
|
1644
1811
|
if (this._tilesOwnTerminal()) {
|
|
1645
|
-
|
|
1812
|
+
// Kept until the group's grid, opened next, takes its place.
|
|
1813
|
+
this.closeTileGrid({ keepStored: true, reselect: false });
|
|
1646
1814
|
this.activeSessionId = null;
|
|
1647
1815
|
}
|
|
1648
1816
|
// Exactly the group: an open split closes without joining it (merged, its
|
|
@@ -2127,7 +2295,7 @@ Object.assign(CodemanApp.prototype, {
|
|
|
2127
2295
|
this._renderTileOverlay(sessionId);
|
|
2128
2296
|
return;
|
|
2129
2297
|
}
|
|
2130
|
-
this.removeTile(sessionId, { focus: false });
|
|
2298
|
+
this.removeTile(sessionId, { focus: false, gone: true });
|
|
2131
2299
|
},
|
|
2132
2300
|
|
|
2133
2301
|
/**
|
|
@@ -2481,17 +2649,20 @@ Object.assign(CodemanApp.prototype, {
|
|
|
2481
2649
|
this._persistTileGrid();
|
|
2482
2650
|
},
|
|
2483
2651
|
|
|
2484
|
-
// ── Persistence (codeman:tile-grid, per
|
|
2652
|
+
// ── Persistence (codeman:tile-grid, per browser: ids and layout, never content) ──
|
|
2485
2653
|
|
|
2486
2654
|
/**
|
|
2487
|
-
* Writes the open grid
|
|
2488
|
-
*
|
|
2489
|
-
*
|
|
2490
|
-
* window
|
|
2655
|
+
* Writes the open grid as it is, on every change (a move, a divider drag, a
|
|
2656
|
+
* tile added or removed, a count picked, a focus, a zoom): its cells, its
|
|
2657
|
+
* count, focus, a zoom the user chose (an automatic one is worked out again
|
|
2658
|
+
* from the window) and the divider fractions. Never content, never to the
|
|
2659
|
+
* server. `open: false` is the closed-but-remembered state. Never in a solo
|
|
2660
|
+
* window, nor while a stored grid is being put back (_openStoredTileGrid
|
|
2661
|
+
* writes once it is); a storage failure only costs the convenience.
|
|
2491
2662
|
*/
|
|
2492
2663
|
_persistTileGrid({ open = true } = {}) {
|
|
2493
2664
|
const grid = this._tileGrid;
|
|
2494
|
-
if (this.isSoloWindow || !grid || grid.ids.length === 0) return;
|
|
2665
|
+
if (this.isSoloWindow || this._tilePersistHold || !grid || grid.ids.length === 0) return;
|
|
2495
2666
|
if (open && !grid.open) return;
|
|
2496
2667
|
const state = {
|
|
2497
2668
|
v: 1,
|
|
@@ -2500,6 +2671,9 @@ Object.assign(CodemanApp.prototype, {
|
|
|
2500
2671
|
// (sanitizeTileGridState; a build before cells drops the nulls and reads
|
|
2501
2672
|
// the tiles packed, as it always did).
|
|
2502
2673
|
ids: grid.cells.slice(),
|
|
2674
|
+
// Never fewer than the tiles shown (a build before `count` derives it
|
|
2675
|
+
// from `ids`, and ignores it).
|
|
2676
|
+
count: Math.min(Math.max(grid.count || 0, grid.ids.length), window.CodemanTileGrid.TILE_GRID_MAX),
|
|
2503
2677
|
focused: grid.focusedId,
|
|
2504
2678
|
zoomed: grid.autoZoom ? null : grid.zoomedId,
|
|
2505
2679
|
colFr: grid.colFr.slice(),
|
|
@@ -2534,47 +2708,74 @@ Object.assign(CodemanApp.prototype, {
|
|
|
2534
2708
|
},
|
|
2535
2709
|
|
|
2536
2710
|
/**
|
|
2537
|
-
*
|
|
2538
|
-
*
|
|
2539
|
-
*
|
|
2540
|
-
*
|
|
2541
|
-
* or filled to the remembered count, a shape change the cell model's rule
|
|
2542
|
-
* handles (reformTileCells).
|
|
2711
|
+
* The stored grid as it would come back now (restoreTileGridCells): its
|
|
2712
|
+
* cells as stored, a cell whose session no longer exists filled from the
|
|
2713
|
+
* ranking while the grid holds fewer than its count. Null when nothing is
|
|
2714
|
+
* stored or none of its sessions survive.
|
|
2543
2715
|
*/
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2716
|
+
_storedTileGridSet(stored = this._readStoredTileGrid()) {
|
|
2717
|
+
if (!stored) return null;
|
|
2718
|
+
return window.CodemanTileGrid.restoreTileGridCells(stored, this._tileGridRanking());
|
|
2719
|
+
},
|
|
2720
|
+
|
|
2721
|
+
/**
|
|
2722
|
+
* Opens a stored grid as the user left it: `set` (the stored cells with any
|
|
2723
|
+
* freed cell filled, _storedTileGridSet, by default; the Tiles button may
|
|
2724
|
+
* pass it trimmed or filled to a picked count, or with a Ctrl/Cmd+clicked
|
|
2725
|
+
* session in it) goes back into its cells, holes included, a shape change
|
|
2726
|
+
* following the cell model's rule (reformTileCells: the tiles keep their row
|
|
2727
|
+
* and column when all fit, the ones that join fill the empty cells in
|
|
2728
|
+
* reading order); then the fractions it had (only if they still match the
|
|
2729
|
+
* layout) and a zoom the user chose on the focused tile. A grid larger than
|
|
2730
|
+
* the window fits is never trimmed: the focused tile shows alone until the
|
|
2731
|
+
* window fits it (_applyTileLayout), and the arrangement stays. `auto`: the
|
|
2732
|
+
* app is putting it back, so no idle alert is spent. `keepCount`: the
|
|
2733
|
+
* stored count stays the grid's (a session that went away may be filled the
|
|
2734
|
+
* next time); false makes it what opens (a pick, a Ctrl/Cmd+click). Nothing
|
|
2735
|
+
* is written until the grid is back, so a stored grid is never overwritten
|
|
2736
|
+
* by a half-built one.
|
|
2737
|
+
*/
|
|
2738
|
+
_openStoredTileGrid(
|
|
2739
|
+
stored,
|
|
2740
|
+
set = this._storedTileGridSet(stored),
|
|
2741
|
+
{ focusedId = null, auto = true, keepCount = true } = {}
|
|
2742
|
+
) {
|
|
2743
|
+
if (!set?.ids?.length) return false;
|
|
2550
2744
|
const T = window.CodemanTileGrid;
|
|
2551
|
-
|
|
2552
|
-
//
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2745
|
+
const focus = focusedId || set.focusedId;
|
|
2746
|
+
// An open split closes without joining it (decision 8, case a).
|
|
2747
|
+
this._tilePersistHold = true;
|
|
2748
|
+
let opened = false;
|
|
2749
|
+
try {
|
|
2750
|
+
opened = this.openTileGrid(set.ids, { focusedId: focus, auto, mergeSplit: false });
|
|
2751
|
+
} finally {
|
|
2752
|
+
this._tilePersistHold = false;
|
|
2753
|
+
}
|
|
2754
|
+
if (!opened) return false;
|
|
2755
|
+
const grid = this._tileGrid;
|
|
2756
|
+
// openTileGrid packed the tiles from the first cell. They go back to their
|
|
2757
|
+
// cells (a session gone since leaves its cell empty, unless the ranking
|
|
2758
|
+
// filled it).
|
|
2759
|
+
const base = (set.cells || []).map((id) => (id && grid.tiles.has(id) ? id : null));
|
|
2760
|
+
const kept = base.filter(Boolean);
|
|
2761
|
+
const cells = T.reformTileCells(
|
|
2762
|
+
base,
|
|
2763
|
+
T.tileCellCols(base.length),
|
|
2764
|
+
kept,
|
|
2765
|
+
grid.ids.filter((id) => !kept.includes(id)),
|
|
2766
|
+
grid.cols,
|
|
2767
|
+
grid.rows
|
|
2768
|
+
);
|
|
2569
2769
|
if (cells.length === grid.cols * grid.rows && cells.filter(Boolean).length === grid.tiles.size) {
|
|
2570
2770
|
grid.cells = cells;
|
|
2571
2771
|
}
|
|
2772
|
+
grid.count = keepCount ? Math.min(Math.max(grid.ids.length, stored.count || 0), T.TILE_GRID_MAX) : grid.ids.length;
|
|
2572
2773
|
// openTileGrid laid the grid out with equal tracks. The stored ones go back
|
|
2573
2774
|
// on; _applyTileLayout drops them again if they do not match the column or
|
|
2574
2775
|
// row count (the window may have changed the layout since).
|
|
2575
2776
|
if (stored.colFr) grid.colFr = stored.colFr.slice();
|
|
2576
2777
|
if (stored.rowFr) grid.rowFr = stored.rowFr.slice();
|
|
2577
|
-
if (stored.zoomed && grid.tiles.has(stored.zoomed)) {
|
|
2778
|
+
if (stored.zoomed && stored.zoomed === grid.focusedId && grid.tiles.has(stored.zoomed)) {
|
|
2578
2779
|
grid.zoomedId = stored.zoomed;
|
|
2579
2780
|
grid.autoZoom = false;
|
|
2580
2781
|
}
|
|
@@ -2598,17 +2799,27 @@ Object.assign(CodemanApp.prototype, {
|
|
|
2598
2799
|
if (this._tilesOwnTerminal()) return false;
|
|
2599
2800
|
const stored = this._readStoredTileGrid();
|
|
2600
2801
|
if (!stored?.open || stored.ids.length === 0) return false;
|
|
2601
|
-
|
|
2802
|
+
// Put back, not opened: the tiles settle in whatever the entrance theme.
|
|
2803
|
+
this._tileEnterQuiet = true;
|
|
2804
|
+
try {
|
|
2805
|
+
return this._openStoredTileGrid(stored);
|
|
2806
|
+
} finally {
|
|
2807
|
+
this._tileEnterQuiet = false;
|
|
2808
|
+
}
|
|
2602
2809
|
},
|
|
2603
2810
|
|
|
2604
|
-
/**
|
|
2811
|
+
/**
|
|
2812
|
+
* A followed `#session=` link took the screen on load: the stored grid stays
|
|
2813
|
+
* remembered, closed. Only `open` changes: the value is written back as it
|
|
2814
|
+
* was stored, so a session gone since still frees its cell for the ranking
|
|
2815
|
+
* the next time the grid opens.
|
|
2816
|
+
*/
|
|
2605
2817
|
_closeStoredTileGrid() {
|
|
2606
2818
|
const stored = this._readStoredTileGrid();
|
|
2607
2819
|
if (!stored?.open) return;
|
|
2608
|
-
// Stored as it was read, holes included (`ids` carries the cells).
|
|
2609
|
-
const { cells, ...rest } = stored;
|
|
2610
2820
|
try {
|
|
2611
|
-
localStorage.
|
|
2821
|
+
const raw = JSON.parse(localStorage.getItem(TILE_GRID_STORAGE_KEY));
|
|
2822
|
+
localStorage.setItem(TILE_GRID_STORAGE_KEY, JSON.stringify({ ...raw, open: false }));
|
|
2612
2823
|
} catch {
|
|
2613
2824
|
/* Per-device convenience only. */
|
|
2614
2825
|
}
|
|
@@ -2637,7 +2848,7 @@ Object.assign(CodemanApp.prototype, {
|
|
|
2637
2848
|
const grid = this._tileGrid;
|
|
2638
2849
|
if (!grid?.open) return false;
|
|
2639
2850
|
for (const id of grid.ids.slice()) {
|
|
2640
|
-
if (!this.sessions.has(id) || this.detachedSessions?.has(id)) this.removeTile(id, { refocus: false });
|
|
2851
|
+
if (!this.sessions.has(id) || this.detachedSessions?.has(id)) this.removeTile(id, { refocus: false, gone: true });
|
|
2641
2852
|
}
|
|
2642
2853
|
if (!grid.open) return false;
|
|
2643
2854
|
// Only a focus that is gone moves: re-selecting the same tile would hide an
|
|
@@ -2665,7 +2876,7 @@ CodemanApp.prototype._onSessionDeleted = function (data) {
|
|
|
2665
2876
|
if (grid?.has(data.id)) {
|
|
2666
2877
|
const wasFocused = grid.focusedId === data.id;
|
|
2667
2878
|
const neighbor = window.CodemanTileGrid.tileNeighbor(grid.ids, data.id);
|
|
2668
|
-
this.removeTile(data.id, { refocus: false });
|
|
2879
|
+
this.removeTile(data.id, { refocus: false, gone: true });
|
|
2669
2880
|
if (wasFocused && grid.open && neighbor && !this._closingSessions?.has(data.id)) {
|
|
2670
2881
|
this._selectTiledSession(neighbor, { auto: true, focus: false });
|
|
2671
2882
|
}
|