mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 08:59:40 +02:00
fix(tiles): a tile that joined before its pane existed resends its size
A session Run makes while the grid is open joins as a tile right away, so the tile connects and sends its size before Run starts the pane. The server only records a resize for a session with no PTY and spawns the pane at 120x40, and Run's own resize step measures the parked main terminal (display: none, so nothing). Measured live for Shell and Claude: a 97x17 tile over a 120x40 pane, for good (#464). The chrome refresh now remembers each tile's last-seen pid and calls TerminalTile.paneStarted() when it appears or changes. paneStarted() forgets the sent size and sends it; a hidden tile (a zoomed neighbour) sends nothing and keeps it forgotten, so its next fit() sends it, which a plain fit({ force: true }) would lose. Keyed on the sessions map, so a handleInit after an SSE drop counts too. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -278,7 +278,7 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph
|
|||||||
|
|
||||||
**Split-pane sessions** (`showSplitButton`, header button, default OFF, desktop-only, per-device): a second live session ("Pane B") beside the active one, in a `TerminalTile` (terminal-tile.js; the picker, divider and auto-collapse stay in terminal-split.js) with its own xterm + WebSocket, resizable via a draggable divider. Pane B reconnects after a drop, sends input through the exactly-once queue over its own socket (`_registerInputSocket`), has clickable paths and image paste, and owns its geometry (no 40x10 floor, `zc` columns adopted, font changes call `tile.fit()`). ⚠️ Only typed input enters that persisted queue: xterm's query replies are dropped and focus/mouse reports go out ephemeral. ⚠️ App-level terminal actions find their pane through `_focusedPane()` (the terminal focused last), never `this.terminal`. Still plainer than the primary pane (no local-echo overlay, CJK IME or touch handlers) and NOT persisted across reloads. The tile grid (below) reuses `TerminalTile`, and the two are never open together. → [architecture-invariants#split-pane-sessions](docs/architecture-invariants.md#split-pane-sessions)
|
**Split-pane sessions** (`showSplitButton`, header button, default OFF, desktop-only, per-device): a second live session ("Pane B") beside the active one, in a `TerminalTile` (terminal-tile.js; the picker, divider and auto-collapse stay in terminal-split.js) with its own xterm + WebSocket, resizable via a draggable divider. Pane B reconnects after a drop, sends input through the exactly-once queue over its own socket (`_registerInputSocket`), has clickable paths and image paste, and owns its geometry (no 40x10 floor, `zc` columns adopted, font changes call `tile.fit()`). ⚠️ Only typed input enters that persisted queue: xterm's query replies are dropped and focus/mouse reports go out ephemeral. ⚠️ App-level terminal actions find their pane through `_focusedPane()` (the terminal focused last), never `this.terminal`. Still plainer than the primary pane (no local-echo overlay, CJK IME or touch handlers) and NOT persisted across reloads. The tile grid (below) reuses `TerminalTile`, and the two are never open together. → [architecture-invariants#split-pane-sessions](docs/architecture-invariants.md#split-pane-sessions)
|
||||||
|
|
||||||
**Tile grid** (`showTileGridButton`, header Tiles button + `Ctrl+Shift+G`, default OFF, desktop-only at 1180px, per-device; tile-grid.js, design `docs/tile-grid-plan.md`): 1 to 9 live sessions side by side, each a `TerminalTile` with a header (state dot, name, menu, zoom, +, ×), laid out by count (`CodemanTileGrid`, constants.js) with draggable column/row dividers, zoom (tmux-style), an Attach overlay, and per-device persistence (`codeman:tile-grid`, ids only, restored INSIDE `handleInit` in place of the single-view select, so the main terminal never loads on that page load). While the grid is open the main terminal is PARKED: `activeSessionId` is the focused tile's session, and every main-terminal path that would write, fetch, resize or reconnect stands aside through `_tilesOwnTerminal()` (the WebGL long-task observer included). ⚠️ Every capture a tile fetches (initial, reconnect refresh, `{t:'r'}`, history pull) goes through ONE `TileLoadQueue` (concurrency 1, a deadline covering the body), because each is a synchronous tmux call on the server. ⚠️ Only a USER-initiated pick of a non-tiled session (or `leaveTiles`, a followed link) leaves the grid; an `auto` selection never collapses it, and every app-driven fallback (close, delete, restore) picks a tile. A caller of `closeTileGrid({ reselect: false })` must null `activeSessionId` before any `_cleanupPreviousSession`, or the parked terminal's stale content is saved as a snapshot. ⚠️ The grid and the split are never open together. ⚠️ Tile chords go through `tileShortcutFor` and are swallowed in every xterm key handler BEFORE the Shift+Enter gate; the toggle follows `showTileGridButton` (OFF: inert). ⚠️ A divider drag reflows locally per frame and sends ONE resize per affected tile at pointer-up. ⚠️ Sessions Run from this tab join the open grid (`_joinTileGridFromRun`, called from `_ensureCreatedSessionVisible`); sessions created elsewhere never do. ⚠️ An agent that exited in a live pane (`paneExit`) cannot be re-attached in place (both attach routes refuse while the pane's tmux client runs, and say so in a 200 envelope): its tile shows the exit and points at Close session. → [architecture-invariants#tile-grid](docs/architecture-invariants.md#tile-grid)
|
**Tile grid** (`showTileGridButton`, header Tiles button + `Ctrl+Shift+G`, default OFF, desktop-only at 1180px, per-device; tile-grid.js, design `docs/tile-grid-plan.md`): 1 to 9 live sessions side by side, each a `TerminalTile` with a header (state dot, name, menu, zoom, +, ×), laid out by count (`CodemanTileGrid`, constants.js) with draggable column/row dividers, zoom (tmux-style), an Attach overlay, and per-device persistence (`codeman:tile-grid`, ids only, restored INSIDE `handleInit` in place of the single-view select, so the main terminal never loads on that page load). While the grid is open the main terminal is PARKED: `activeSessionId` is the focused tile's session, and every main-terminal path that would write, fetch, resize or reconnect stands aside through `_tilesOwnTerminal()` (the WebGL long-task observer included). ⚠️ Every capture a tile fetches (initial, reconnect refresh, `{t:'r'}`, history pull) goes through ONE `TileLoadQueue` (concurrency 1, a deadline covering the body), because each is a synchronous tmux call on the server. ⚠️ Only a USER-initiated pick of a non-tiled session (or `leaveTiles`, a followed link) leaves the grid; an `auto` selection never collapses it, and every app-driven fallback (close, delete, restore) picks a tile. A caller of `closeTileGrid({ reselect: false })` must null `activeSessionId` before any `_cleanupPreviousSession`, or the parked terminal's stale content is saved as a snapshot. ⚠️ The grid and the split are never open together. ⚠️ Tile chords go through `tileShortcutFor` and are swallowed in every xterm key handler BEFORE the Shift+Enter gate; the toggle follows `showTileGridButton` (OFF: inert). ⚠️ A divider drag reflows locally per frame and sends ONE resize per affected tile at pointer-up. ⚠️ Sessions Run from this tab join the open grid (`_joinTileGridFromRun`, called from `_ensureCreatedSessionVisible`); sessions created elsewhere never do. Such a tile connects before its pane exists, the server drops that resize and spawns at 120x40, and Run's own resize measures the parked terminal (nothing), so the chrome refresh calls `tile.paneStarted()` when the session's pid appears or changes. ⚠️ An agent that exited in a live pane (`paneExit`) cannot be re-attached in place (both attach routes refuse while the pane's tmux client runs, and say so in a 200 envelope): its tile shows the exit and points at Close session. → [architecture-invariants#tile-grid](docs/architecture-invariants.md#tile-grid)
|
||||||
|
|
||||||
**Terminal touch gestures: link taps and text selection**: on touch devices xterm's linkifier and SelectionService never see the gesture, so both are driven explicitly (terminal-ui.js). ⚠️ A tap activates the link under it through the SAME provider as the hover linkifier (`_terminalLinkAtPoint`), synchronously inside `touchend` (keeps the user gesture `window.open` needs) and BEFORE any mouse report; the caret's logical line (`_tapIsOnCaretLine`) and TUI-owned rows (`_isActionableMobileTerminalTap`) keep their meaning. ⚠️ Gate on the caret line, never on tap intent (a shell calls every tap `'input'`). ⚠️ Long-press selects via xterm's public `select()`; keep the three guards: suppress the compat mouse pair after `touchend`, the bounded focus guard + `contextmenu` suppression for the platform long-press, and no closing `terminal.focus()` on phones. Tests: `test/terminal-touch-tap.test.ts`. → [architecture-invariants#terminal-touch-gestures-link-taps-and-text-selection](docs/architecture-invariants.md#terminal-touch-gestures-link-taps-and-text-selection)
|
**Terminal touch gestures: link taps and text selection**: on touch devices xterm's linkifier and SelectionService never see the gesture, so both are driven explicitly (terminal-ui.js). ⚠️ A tap activates the link under it through the SAME provider as the hover linkifier (`_terminalLinkAtPoint`), synchronously inside `touchend` (keeps the user gesture `window.open` needs) and BEFORE any mouse report; the caret's logical line (`_tapIsOnCaretLine`) and TUI-owned rows (`_isActionableMobileTerminalTap`) keep their meaning. ⚠️ Gate on the caret line, never on tap intent (a shell calls every tap `'input'`). ⚠️ Long-press selects via xterm's public `select()`; keep the three guards: suppress the compat mouse pair after `touchend`, the bounded focus guard + `contextmenu` suppression for the platform long-press, and no closing `terminal.focus()` on phones. Tests: `test/terminal-touch-tap.test.ts`. → [architecture-invariants#terminal-touch-gestures-link-taps-and-text-selection](docs/architecture-invariants.md#terminal-touch-gestures-link-taps-and-text-selection)
|
||||||
|
|
||||||
|
|||||||
@@ -811,7 +811,7 @@ Further detail: with many sessions the horizontal strip stops being scannable, w
|
|||||||
|
|
||||||
⚠️ **Attach.** A tile whose session has no PTY (`pid === null`) or whose socket closed because it exited (4009) shows an Attach overlay (absolute, the body keeps its size): `POST /interactive` (or `/shell`) with NO body, one in flight per session, a tripped PTY-exit breaker only through the same confirm as the single view, then the tile is remounted (a stopped socket cannot reconnect). The routes report a refusal in the ENVELOPE of a 200, so the response body is read, not `res.ok`. An agent that exited in a live pane (`paneExit`) cannot be started again in place (both routes refuse while the pane's tmux client runs): its tile shows the exit and points at Close session.
|
⚠️ **Attach.** A tile whose session has no PTY (`pid === null`) or whose socket closed because it exited (4009) shows an Attach overlay (absolute, the body keeps its size): `POST /interactive` (or `/shell`) with NO body, one in flight per session, a tripped PTY-exit breaker only through the same confirm as the single view, then the tile is remounted (a stopped socket cannot reconnect). The routes report a refusal in the ENVELOPE of a 200, so the response body is read, not `res.ok`. An agent that exited in a live pane (`paneExit`) cannot be started again in place (both routes refuse while the pane's tmux client runs): its tile shows the exit and points at Close session.
|
||||||
|
|
||||||
⚠️ **Persistence and joining.** `codeman:tile-grid` holds `{ v: 1, open, ids, focused, zoomed, colFr, rowFr }`, ids only, written as the grid changes; closing keeps it as `open: false` (the Tiles toggle, the picker and Ctrl/Cmd+click bring it back), the last tile leaving forgets it, a solo window never reads or writes it, an automatic zoom is not stored. Sessions created by THIS tab's Run join the open grid (`_joinTileGridFromRun`, called from session-ui.js `_ensureCreatedSessionVisible`; a wrapper from tile-grid.js would be overwritten by session-ui.js's later `Object.assign`); sessions created elsewhere arrive only by `session:created` and never join. Tests: `test/tile-grid-*.test.ts` over the shared vm harness `test/mocks/tile-grid-vm.ts`.
|
⚠️ **Persistence and joining.** `codeman:tile-grid` holds `{ v: 1, open, ids, focused, zoomed, colFr, rowFr }`, ids only, written as the grid changes; closing keeps it as `open: false` (the Tiles toggle, the picker and Ctrl/Cmd+click bring it back), the last tile leaving forgets it, a solo window never reads or writes it, an automatic zoom is not stored. Sessions created by THIS tab's Run join the open grid (`_joinTileGridFromRun`, called from session-ui.js `_ensureCreatedSessionVisible`; a wrapper from tile-grid.js would be overwritten by session-ui.js's later `Object.assign`); sessions created elsewhere arrive only by `session:created` and never join. ⚠️ A tile that joins that way connects BEFORE Run starts its pane: its resize reaches a session with no PTY, which `Session.resize()` only records (`_lastDesktopDims`) while the spawn uses a fixed 120x40, and Run's own resize step measures the parked main terminal (`display: none`, so `proposeDimensions()` is NaN and the step is skipped). Measured live: a 97x17 tile over a 120x40 pane. So `_renderTileChrome` remembers each tile's last-seen pid and calls `tile.paneStarted()` when it appears or changes (keyed on the sessions map, so a `handleInit` after an SSE drop counts too); `paneStarted()` forgets the sent size and sends it, and a hidden tile (a zoomed neighbour) sends nothing but keeps the size forgotten, so its next `fit()` sends it. A plain `fit({ force: true })` would lose that case. Tests: `test/tile-grid-*.test.ts` over the shared vm harness `test/mocks/tile-grid-vm.ts`.
|
||||||
|
|
||||||
### Gesture control: the setting
|
### Gesture control: the setting
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ or settled a question the spec left open. The invariants as built are in
|
|||||||
- **4009 lands on the Attach overlay**, and 4003/4004/4010 remove the tile.
|
- **4009 lands on the Attach overlay**, and 4003/4004/4010 remove the tile.
|
||||||
- **"+ / New session in this case"** runs the normal Run for that case and joins through
|
- **"+ / New session in this case"** runs the normal Run for that case and joins through
|
||||||
the same auto-join as any Run from this tab.
|
the same auto-join as any Run from this tab.
|
||||||
|
- **A tile that joins before its pane exists resends its size when the pid appears**
|
||||||
|
(`TerminalTile.paneStarted()`): the server drops a resize for a session with no PTY and
|
||||||
|
spawns at 120x40, and Run's own resize measures the parked main terminal. Applying a
|
||||||
|
pre-spawn resize at spawn time on the server would make this unnecessary (follow-up).
|
||||||
|
|
||||||
## Problem
|
## Problem
|
||||||
|
|
||||||
|
|||||||
@@ -921,6 +921,17 @@
|
|||||||
this.ws.send(JSON.stringify({ t: 'z', c: cols, r: rows, v: 'desktop' }));
|
this.ws.send(JSON.stringify({ t: 'z', c: cols, r: rows, v: 'desktop' }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The session's PTY is new: a tile can connect before its session has a
|
||||||
|
// pane (one Run started while the grid is open joins first), and the
|
||||||
|
// server drops a resize that arrives with no PTY, then spawns at its own
|
||||||
|
// default size. Forget what was sent, so the size goes out now, or with
|
||||||
|
// the next fit() when this tile is hidden right now (a zoomed neighbour):
|
||||||
|
// _sendResize() returns before recording anything it did not send.
|
||||||
|
paneStarted() {
|
||||||
|
this._lastSentDims = null;
|
||||||
|
this._sendResize();
|
||||||
|
}
|
||||||
|
|
||||||
// The geometry the PTY actually holds (`{t:'zc'}`, the server's answer to
|
// The geometry the PTY actually holds (`{t:'zc'}`, the server's answer to
|
||||||
// every resize). A PTY and a terminal that disagree on WIDTH render
|
// every resize). A PTY and a terminal that disagree on WIDTH render
|
||||||
// garbled, so a different column count is adopted; rows stay local, as in
|
// garbled, so a different column count is adopted; rows stay local, as in
|
||||||
|
|||||||
@@ -791,6 +791,8 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
name: header.name,
|
name: header.name,
|
||||||
zoomBtn: header.zoomBtn,
|
zoomBtn: header.zoomBtn,
|
||||||
renaming: false,
|
renaming: false,
|
||||||
|
// The pid this tile last saw, so a pane that starts later is noticed.
|
||||||
|
pid: this.sessions.get(sessionId)?.pid ?? null,
|
||||||
});
|
});
|
||||||
grid.ids.push(sessionId);
|
grid.ids.push(sessionId);
|
||||||
this._renderTileHeader(sessionId);
|
this._renderTileHeader(sessionId);
|
||||||
@@ -1187,7 +1189,19 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
_renderTileChrome() {
|
_renderTileChrome() {
|
||||||
const grid = this._tileGrid;
|
const grid = this._tileGrid;
|
||||||
if (!grid?.open) return;
|
if (!grid?.open) return;
|
||||||
for (const id of grid.tiles.keys()) this._renderTileHeader(id);
|
for (const [id, entry] of grid.tiles) {
|
||||||
|
this._renderTileHeader(id);
|
||||||
|
// A pane that started after the tile connected (a session Run made
|
||||||
|
// while the grid is open, an Attach from elsewhere) was spawned at the
|
||||||
|
// server's default size: the tile's own resize went out before there
|
||||||
|
// was a PTY to take it, and Run's resize measures the parked main
|
||||||
|
// terminal (nothing). Keyed on the sessions map, so a handleInit after
|
||||||
|
// an SSE drop is seen too; a render skipped during an inline tab rename
|
||||||
|
// is caught up by the rename's own render.
|
||||||
|
const pid = this.sessions.get(id)?.pid ?? null;
|
||||||
|
if (pid !== null && pid !== entry.pid) entry.tile.paneStarted?.();
|
||||||
|
entry.pid = pid;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ export class FakeTile {
|
|||||||
reconnectNow = vi.fn();
|
reconnectNow = vi.fn();
|
||||||
fit = vi.fn();
|
fit = vi.fn();
|
||||||
localFit = vi.fn();
|
localFit = vi.fn();
|
||||||
|
paneStarted = vi.fn();
|
||||||
destroy = vi.fn(() => {
|
destroy = vi.fn(() => {
|
||||||
this._destroyed = true;
|
this._destroyed = true;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -583,6 +583,39 @@ describe('TerminalTile geometry (#464: the pane and its PTY never disagree)', ()
|
|||||||
expect(resizeFrames(ws).at(-1)).toEqual({ t: 'z', c: 28, r: 30, v: 'desktop' });
|
expect(resizeFrames(ws).at(-1)).toEqual({ t: 'z', c: 28, r: 30, v: 'desktop' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('paneStarted() resends an unchanged size: the first one went out before there was a PTY', async () => {
|
||||||
|
const { tile, ws } = await connectTile(makeApp());
|
||||||
|
ws.open();
|
||||||
|
tile.paneStarted();
|
||||||
|
expect(resizeFrames(ws)).toEqual([
|
||||||
|
{ t: 'z', c: 80, r: 24, v: 'desktop' },
|
||||||
|
{ t: 'z', c: 80, r: 24, v: 'desktop' },
|
||||||
|
]);
|
||||||
|
// Only once: the size is recorded again, so a plain fit does not repeat it.
|
||||||
|
tile.fit();
|
||||||
|
expect(resizeFrames(ws)).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('paneStarted() on a hidden tile sends nothing, and its next fit sends the size', async () => {
|
||||||
|
const { tile, ws } = await connectTile(makeApp());
|
||||||
|
ws.open();
|
||||||
|
FakeFit.proposed = { cols: NaN, rows: NaN };
|
||||||
|
tile.paneStarted();
|
||||||
|
expect(resizeFrames(ws)).toHaveLength(1);
|
||||||
|
// Shown again (a zoom ends) at the same size it had: still sent.
|
||||||
|
FakeFit.proposed = { cols: 80, rows: 24 };
|
||||||
|
tile.fit();
|
||||||
|
expect(resizeFrames(ws)).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('paneStarted() before the socket opens sends nothing; the open sends the size once', async () => {
|
||||||
|
const { tile, ws } = await connectTile(makeApp());
|
||||||
|
tile.paneStarted();
|
||||||
|
expect(resizeFrames(ws)).toHaveLength(0);
|
||||||
|
ws.open();
|
||||||
|
expect(resizeFrames(ws)).toEqual([{ t: 'z', c: 80, r: 24, v: 'desktop' }]);
|
||||||
|
});
|
||||||
|
|
||||||
it('reports nothing while hidden (the fit addon measures NaN)', async () => {
|
it('reports nothing while hidden (the fit addon measures NaN)', async () => {
|
||||||
const { tile, ws, term } = await connectTile(makeApp());
|
const { tile, ws, term } = await connectTile(makeApp());
|
||||||
ws.open();
|
ws.open();
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
* `session:created` and never join. A grid already holding what the window
|
* `session:created` and never join. A grid already holding what the window
|
||||||
* fits does not take it: Run's selection then shows it alone, with a hint.
|
* fits does not take it: Run's selection then shows it alone, with a hint.
|
||||||
*
|
*
|
||||||
|
* A tile that joined before its pane existed resends its size once the pid
|
||||||
|
* appears (the server spawned the pane at its own default size).
|
||||||
|
*
|
||||||
* A tile's + also offers "New session in this case": the normal Run for the
|
* A tile's + also offers "New session in this case": the normal Run for the
|
||||||
* case the tile's session belongs to, the toolbar's case put back afterwards.
|
* case the tile's session belongs to, the toolbar's case put back afterwards.
|
||||||
*
|
*
|
||||||
@@ -20,6 +23,7 @@ import { resolve } from 'node:path';
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import {
|
import {
|
||||||
FakeEl,
|
FakeEl,
|
||||||
|
FakeTile,
|
||||||
body,
|
body,
|
||||||
bySelector,
|
bySelector,
|
||||||
makeGridApp,
|
makeGridApp,
|
||||||
@@ -95,6 +99,45 @@ describe('Run from this tab', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('a pane that starts after its tile connected (#464)', () => {
|
||||||
|
// The tile sent its size before there was a PTY; the server dropped it and
|
||||||
|
// spawned the pane at its default size, so the size must go out again.
|
||||||
|
const tileOf = (id: string) => FakeTile.all.filter((t) => t.sessionId === id).at(-1)!;
|
||||||
|
const setPid = (app: GridApp, id: string, pid: number | null) =>
|
||||||
|
app.sessions.set(id, { ...app.sessions.get(id), pid });
|
||||||
|
|
||||||
|
it('resends the size once when the pid appears, and again only for a new PTY', () => {
|
||||||
|
const app = makeGridApp(IDS);
|
||||||
|
app.openTileGrid(IDS);
|
||||||
|
addSession(app, 's-new');
|
||||||
|
app._joinTileGridFromRun('s-new');
|
||||||
|
const tile = tileOf('s-new');
|
||||||
|
app._renderTileChrome();
|
||||||
|
expect(tile.paneStarted).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
setPid(app, 's-new', 4242);
|
||||||
|
app._renderTileChrome();
|
||||||
|
expect(tile.paneStarted).toHaveBeenCalledTimes(1);
|
||||||
|
app._renderTileChrome();
|
||||||
|
expect(tile.paneStarted).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// The pane went away and a new one started (an Attach, a respawned pane).
|
||||||
|
setPid(app, 's-new', null);
|
||||||
|
app._renderTileChrome();
|
||||||
|
setPid(app, 's-new', 4343);
|
||||||
|
app._renderTileChrome();
|
||||||
|
expect(tile.paneStarted).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('a tile made for a session that already runs never asks', () => {
|
||||||
|
const app = makeGridApp(IDS);
|
||||||
|
app.openTileGrid(IDS);
|
||||||
|
app._renderTileChrome();
|
||||||
|
app._renderTileChrome();
|
||||||
|
for (const id of IDS) expect(tileOf(id).paneStarted).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('+ / New session in this case', () => {
|
describe('+ / New session in this case', () => {
|
||||||
function openMenu(app: GridApp, fromId: string) {
|
function openMenu(app: GridApp, fromId: string) {
|
||||||
app.openTileAddMenu({ stopPropagation: vi.fn(), preventDefault: vi.fn(), currentTarget: null }, fromId);
|
app.openTileAddMenu({ stopPropagation: vi.fn(), preventDefault: vi.fn(), currentTarget: null }, fromId);
|
||||||
|
|||||||
Reference in New Issue
Block a user