mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 08:59:40 +02:00
feat(tiles): the grid is cells, and an empty cell can be any cell
Owner feedback 1: "so the empty tab doesnt always have to be the last
one! so I can move freely around and the empty tab can also be tab nr
4 or 3". This replaces the slot refusal of aecada8c.
grid.cells (a session id or null per cell) is now the one source of
truth; grid.ids is a getter deriving the tiles in reading order, so
everything that only wants the tiled sessions (focus neighbour,
cycling, the load queue's order, the picker, closeSession) is
unchanged. The shape still comes from the tile count and the cap
counts tiles, never empty cells.
- A tile dragged onto an empty cell moves there and leaves its own
cell empty, nothing else moving (_moveTileToCell, through
_reorderTiles: no remount, reconnect or reload; only a tile whose
cell size changed fits). A tiled session's tab does the same; a tab
of a session not tiled yet joins in the cell it is dropped on. Each
slot knows its cell and reads "Drop a tab or a tile here".
- Move Tile goes to the adjacent cell: into it when empty, a swap when
a tile is there (tileCellInDirection).
- Removing a tile leaves its cell empty; adding one takes the first
empty cell. A shape change goes through fitTileCells: each tile keeps
its row and column when all fit (2x2 growing to 3x2), else the tiles
pack in reading order.
- Focus never lands on an empty cell: Alt+Shift+Arrows run over the
cells, Ctrl+Tab and Alt+[ ] over the tiles.
- codeman:tile-grid stays ids only: its ids are the cells with null for
an empty one. A reload brings the holes back when the shape is the
same (a session gone since leaves its cell empty), another shape
packs, the old packed format reads unchanged, and a followed
#session= link keeps the holes.
Docs: the spec's as-built bullet (rewritten in place), the wiki's Tile
Grid page and Keyboard Shortcuts, the invariants and CLAUDE.md.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -813,13 +813,13 @@ Further detail: with many sessions the horizontal strip stops being scannable, w
|
||||
|
||||
⚠️ **zh-CN.** Every string the grid puts on screen has its own entry in i18n.js's `ZH_CN` (or a `translateDynamic` pattern for counts, exit codes and the header tooltip's state plus duration, which requires the duration: bare state words stay out of the table, see mobile-overview.js), so nothing reaches the generic leading-verb fallback. `test/tile-grid-i18n.test.ts` drives the real tile code through every state that writes text, harvests each string and requires a full translation (and the same English back in `en`); a new tile string needs its entry or that test fails. Session and group names stay user text (`data-i18n-skip`). ⚠️ A refresh that skips unchanged text must compare with the last ENGLISH value it set (`entry.headerLabel`, `entry.overlayLabel`, `entry.zoomLabel`), never the DOM: in zh-CN the DOM holds the translation, so a DOM compare rewrites English on every `session:updated` for the observer to translate again.
|
||||
|
||||
⚠️ **Chrome.** The header is a fixed 28px sibling of the body (its buttons are 26px targets with 16 to 19px glyphs, the app header's own icon-button size), refreshed in place on every tab render (`_renderSessionTabsImmediate` wrapper), never by rewriting the tile (that would take its xterm along) and never growing (#464: the body holds the xterm). Header buttons stop pointerdown, so acting on an unfocused tile does not focus it. × removes the tile only; killing stays behind the session menu's Close session. The `needs` pulse animates `box-shadow` only. Dividers are their own 6px grid tracks (`gap: 0`), tiles placed explicitly; a drag uses pointer capture, reflows the affected tiles locally per animation frame and sends ONE `fit()` (one PTY resize) per affected tile at pointer-up; closing the grid or removing a tile mid-drag tears it down. A tab dragged onto a tile replaces it (or swaps two tiles); tiles and empty slots handle `dragover`/`drop` in the CAPTURE phase and stop it, because the drag carries the session id as text and xterm's helper textarea would type it into the PTY. ⚠️ **Moving tiles** (owner request): a tile's header is a native drag handle (`_installTileMoveDrag`; its free area only: where the press landed is noted in the capture phase, since `dragstart` targets the header whatever was pressed) through the same drop targets. It carries `application/x-codeman-tile`, never text, and sets `_draggedTileId`, never `draggedTabId` (the tab strip would reorder on it); `dragend` (Escape, a drop elsewhere) moves nothing and clears what the drag painted, and removing the tile or closing the grid mid-drag ends it. ⚠️ The header focuses its tile on CLICK, never on pointerdown (the tile's pointerdown skips presses in the header; the body keeps press-to-focus so focus moves before xterm sees the press), so a cancelled drag changes neither focus nor the idle alert; the rename input stops its own clicks. ⚠️ Each drop target says what it accepts (`_acceptTabDrops(el, onDrop, { accepts })`): a tile any session but its own, an empty slot only a session not tiled yet, so a tile is never moved to a slot; a refused drag is still held (`dropEffect: 'none'`, no highlight). Every move (that drag, a tiled tab's drop on a tile, Move Tile) goes through ONE `_reorderTiles`: no remount, reconnect or reload, divider sizes belong to the cells, and only a tile whose cell size changed fits, synchronously, once (never the debounced refit of every tile). Moving is off while a tile is zoomed (`draggable` off, a tiled tab's drop refused) and with a single tile.
|
||||
⚠️ **Chrome.** The header is a fixed 28px sibling of the body (its buttons are 26px targets with 16 to 19px glyphs, the app header's own icon-button size), refreshed in place on every tab render (`_renderSessionTabsImmediate` wrapper), never by rewriting the tile (that would take its xterm along) and never growing (#464: the body holds the xterm). Header buttons stop pointerdown, so acting on an unfocused tile does not focus it. × removes the tile only; killing stays behind the session menu's Close session. The `needs` pulse animates `box-shadow` only. Dividers are their own 6px grid tracks (`gap: 0`), each tile and each empty slot placed explicitly in its cell; a drag uses pointer capture, reflows the affected tiles locally per animation frame and sends ONE `fit()` (one PTY resize) per affected tile at pointer-up; closing the grid or removing a tile mid-drag tears it down. A tab dragged onto a tile replaces it (or swaps two tiles); tiles and empty slots handle `dragover`/`drop` in the CAPTURE phase and stop it, because the drag carries the session id as text and xterm's helper textarea would type it into the PTY. ⚠️ **Moving tiles** (owner request): a tile's header is a native drag handle (`_installTileMoveDrag`; its free area only: where the press landed is noted in the capture phase, since `dragstart` targets the header whatever was pressed) through the same drop targets. It carries `application/x-codeman-tile`, never text, and sets `_draggedTileId`, never `draggedTabId` (the tab strip would reorder on it); `dragend` (Escape, a drop elsewhere) moves nothing and clears what the drag painted, and removing the tile or closing the grid mid-drag ends it. ⚠️ The header focuses its tile on CLICK, never on pointerdown (the tile's pointerdown skips presses in the header; the body keeps press-to-focus so focus moves before xterm sees the press), so a cancelled drag changes neither focus nor the idle alert; the rename input stops its own clicks. ⚠️ Each drop target says what it accepts (`_acceptTabDrops(el, onDrop, { accepts })`): a tile any session but its own (its own is held but refused, `dropEffect: 'none'`, no highlight), an empty slot anything. ⚠️ **The grid is cells** (owner: an empty cell can be any cell): `grid.cells` (a session id or `null` per cell, cols x rows of them) is the ONE source of truth and `grid.ids` a getter deriving the tiles in reading order, so never write `ids` (a leftover write in the non-strict `Object.assign` methods would be silently ignored). The shape comes from the tile count; within it a tile moved onto an empty cell goes there and leaves its own cell empty (`_moveTileToCell`), a removed tile leaves a hole, an added one takes the first hole or the cell a tab was dropped on (`_placeTile`), and a shape change goes through the pure `fitTileCells` (keep row and column when all fit, else pack). Each slot carries its cell (`data-cell`). The focus chords run `tileInDirection` over the CELLS (never onto a hole; the nearest tile in the nearest row for up/down), the Move Tile chords `tileCellInDirection` (the adjacent cell: into it when empty, a swap when not). Every move (that drag, a tiled tab's drop, Move Tile) goes through ONE `_reorderTiles`: no remount, reconnect or reload, divider sizes belong to the cells, and only a tile whose cell size changed fits, synchronously, once (never the debounced refit of every tile). Moving is off while a tile is zoomed (`draggable` off, a tiled tab's drop refused) and with a single tile.
|
||||
|
||||
⚠️ **Harness and model.** Every session header (a tile's, both split panes') names the session's CLI with PR #532's `run-mode-dot <cliId>` logo slot and the model it runs as text, painted by ONE function, `_paintSessionHarness` (terminal-split.js), from the pure `describeSessionHarness` (constants.js). The id is data (the logo's class, the CLI catalog's label), never a branch: `test/frontend-cli-no-id-branching.test.ts` scans constants.js, terminal-split.js and tile-grid.js. The model is `SessionState.displayModel` (src/session-display-model.ts): the custom endpoint's `modelId`, else the newest report from the CLI itself (claude's statusline `model.display_name` via `POST /api/status-telemetry`, or the CLI's own footer read with `capabilities.modelDetect` off the capture the idle/working probe already takes), else what the CLI's own config pins (`capabilities.modelDetect.configResolver`, read at every pane start, attach and relaunch: for dsh, its TUI route under the session's `DSH_HOME`, src/deepseek-route-config.ts), else the launch model, else nothing: the logo alone, never a placeholder. ⚠️ A config reader is read-only and bounded (probe before read, a size cap, realpath inside the CLI's home), answers nothing on any doubt (a half-pinned dsh route, a file beyond its narrow YAML subset, no YAML dependency) and returns the model id alone; it skips remote and docker sessions, whose config is not local. ⚠️ A screen field equal to one of the CLI's declared `modelDetect.rejectWords` (what a footer shows there when its model field is off: dsh's effort ids and mode ids) or to the session's own folder name is never the model. ⚠️ The model is untrusted, pane-derived text: sanitized and capped at 64 characters on the server, rendered with `textContent` inside a `data-i18n-skip` span; the tooltip (on the logo and on the model's box, which the translator may reach) says where a model the CLI did not report came from ("set at launch", "custom endpoint"), and the logo's accessible name carries harness and model so the model box is `aria-hidden`. ⚠️ The painter diffs against what it last wrote (kept on the header's parts), never the DOM, so an unchanged session writes nothing on a tab render. ⚠️ On a narrow header the model gives way first, then the name: the name does not shrink at all and is capped at its box (`max-width: 100%`), because any shrink factor takes a subpixel from a name that fits and ellipsizes it. ⚠️ Pane A's strip exists only while the split is open, as the first child of `.terminal-wrap`; it takes height from the main terminal, so opening and closing fit through `sendResize` / `syncTerminalGeometry` (#464), never a bare `fitAddon.fit()`, and the partial-history banner moves below it.
|
||||
|
||||
⚠️ **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. ⚠️ 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`.
|
||||
⚠️ **Persistence and joining.** `codeman:tile-grid` holds `{ v: 1, open, ids, focused, zoomed, colFr, rowFr }`, ids only, written as the grid changes; its `ids` are the CELLS, `null` for an empty one, and `sanitizeTileGridState` returns them as `cells` (a dropped id a hole, never a shift) beside the packed `ids` every list consumer wants; a restore puts the holes back only when the cell count matches the current shape, else the tiles stay packed, and `_closeStoredTileGrid` writes the cells back, never the packed list. The old packed format reads as cells with no hole; 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
|
||||
|
||||
|
||||
+42
-24
@@ -21,8 +21,8 @@ or settled a question the spec left open. The invariants as built are in
|
||||
the setting off the toggle chord is inert. A grid opened another
|
||||
way (Ctrl/Cmd+click, a dropped tab, "Open group as tiles") keeps all its chords.
|
||||
- **Dividers are grid tracks.** Each gap between columns and rows is its own 6px track (the
|
||||
grid gap is 0) and tiles are placed explicitly in reading order, which is also what the
|
||||
empty-slot drop targets need. Fractions reset when the column or row count changes.
|
||||
grid gap is 0) and every tile and every empty slot is placed explicitly in its cell
|
||||
(`grid.cells`, see "Tiles move"). Fractions reset when the column or row count changes.
|
||||
- **Zoom follows tmux.** Moving focus to another tile restores the grid; an automatic zoom
|
||||
(window too small for the minimum tile) follows focus instead.
|
||||
- **Tile loads are bounded** (`boundedLoad`), carry a fetch deadline covering the body (Pane
|
||||
@@ -68,28 +68,46 @@ or settled a question the spec left open. The invariants as built are in
|
||||
batches, so lifecycle and hook events are unaffected, and leaving the grid
|
||||
re-subscribes the shown session.
|
||||
- **Tiles move** (owner request: "give me the option to move the tiles around"; not a
|
||||
numbered decision). A tile's header, its free area (not the buttons, not the rename
|
||||
input), drags it onto another tile and the two trade places. An empty slot refuses a tile,
|
||||
its header drag and its tab alike (owner's answer, "dont move the tile": a slot is always
|
||||
the last cell, so a move there shifted every tile after it); a session not tiled yet still
|
||||
joins there. It is a native drag through the tab drop targets (capture phase, stopped
|
||||
before xterm), carrying a type of its own and never text, and it is not `draggedTabId`, so
|
||||
neither a text field nor the tab strip takes it; Escape or a drop anywhere else cancels
|
||||
with nothing changed, focus included: the header focuses its tile on click, not on press
|
||||
(owner's answer: best practice; the body keeps press-to-focus, so focus moves before a
|
||||
press reaches xterm). `Ctrl+Shift+Arrows` (Move Tile Left/Right/Up/Down, registry,
|
||||
rebindable) swap the focused tile with the neighbour the focus chords pick, and focus
|
||||
stays on it. Every move, a tiled tab's drop included, goes through `_reorderTiles`: no
|
||||
remount, reconnect or reload; divider sizes belong to the cells, so only a tile whose cell
|
||||
size changed fits (one PTY resize, #464). Moving is off while a tile is zoomed (the chords
|
||||
still apply there, as a no-op, so their keys never reach the CLI; a tiled tab dropped on
|
||||
the zoomed tile is refused too, as the owner confirmed) and with a single tile. Both arrow
|
||||
chord families, focus and move, skip a text field, where shifted arrows select (owner's
|
||||
answer: best practice). Default keys: every other two-modifier arrow chord is taken
|
||||
(Ctrl+Alt switches workspaces, Ctrl+Alt+Shift moves a window to another workspace in
|
||||
GNOME, Alt is back/forward, Alt+Shift focuses tiles); Ctrl+Shift+Arrows is unclaimed by
|
||||
the browsers, GNOME, KDE, macOS and Claude Code, and costs only a terminal editor's word
|
||||
selection inside a tile while the grid is open.
|
||||
numbered decision). The grid is CELLS, not a packed list (owner: "the empty tab doesnt
|
||||
always have to be the last one ... it can also be tab nr 4 or 3"): `grid.cells` holds a
|
||||
session id or `null` per cell and is the one source of truth, `grid.ids` the tiles in
|
||||
reading order derived from it. The shape still comes from the tile count (the layout
|
||||
table), the cap counts tiles, never empty cells, and an empty cell can be any cell (in
|
||||
practice one at most: a 3x2 holds 5 or 6 tiles, a 2x2 3 or 4). A tile's header, its free
|
||||
area (not the buttons, not the rename input), drags it: onto another tile the two trade
|
||||
places, onto an empty cell it moves there and leaves its own cell empty, nothing else
|
||||
moving; a tiled session's tab does the same, and a tab of a session not tiled yet joins in
|
||||
the cell it is dropped on. It is a native drag through the tab drop targets (capture
|
||||
phase, stopped before xterm), carrying a type of its own and never text, and it is not
|
||||
`draggedTabId`, so neither a text field nor the tab strip takes it; Escape or a drop
|
||||
anywhere else cancels with nothing changed, focus included: the header focuses its tile on
|
||||
click, not on press (owner's answer: best practice; the body keeps press-to-focus, so
|
||||
focus moves before a press reaches xterm). `Ctrl+Shift+Arrows` (Move Tile
|
||||
Left/Right/Up/Down, registry, rebindable) move the focused tile to the adjacent cell: into
|
||||
it when empty, trading places when a tile is there, never jumping a cell; focus stays on
|
||||
it. Every move goes through `_reorderTiles`: no remount, reconnect or reload; divider
|
||||
sizes belong to the cells, so only a tile whose cell size changed fits (one PTY resize,
|
||||
#464). Removing a tile leaves its cell empty where it was, and adding one takes the first
|
||||
empty cell (or the one a tab was dropped on), while the shape stays; a shape change
|
||||
(`fitTileCells`, constants.js) keeps each tile's row and column when all fit and otherwise
|
||||
packs the tiles in reading order, which differs from plain packing only when a 2x2 grows
|
||||
to a 3x2 (the four tiles stay put). Focus never lands on an empty cell: Alt+Shift+Arrows
|
||||
go along the row past one, or to the nearest row with a tile (the same column, else the
|
||||
nearest), and Ctrl+Tab and Alt+[ / ] cycle the tiles only. `codeman:tile-grid` stays ids
|
||||
only: its `ids` are the cells, `null` for an empty one (a build before cells drops the
|
||||
nulls and reads them packed); a reload brings the holes back when the shape is the same, a
|
||||
session gone by then leaves its cell empty, another shape packs, and the old packed format
|
||||
reads unchanged. A fresh grid (the picker's Open, Ctrl/Cmd+click with the grid closed,
|
||||
"Open group as tiles") opens packed; only the toggle and the page-load restore bring holes
|
||||
back. Moving is off while a tile is zoomed (the chords still apply there, as a no-op, so
|
||||
their keys never reach the CLI; a tiled tab dropped on the zoomed tile is refused too, as
|
||||
the owner confirmed) and with a single tile. Both arrow chord families, focus and move,
|
||||
skip a text field, where shifted arrows select (owner's answer: best practice). Default
|
||||
keys: every other two-modifier arrow chord is taken (Ctrl+Alt switches workspaces,
|
||||
Ctrl+Alt+Shift moves a window to another workspace in GNOME, Alt is back/forward,
|
||||
Alt+Shift focuses tiles); Ctrl+Shift+Arrows is unclaimed by the browsers, GNOME, KDE,
|
||||
macOS and Claude Code, and costs only a terminal editor's word selection inside a tile
|
||||
while the grid is open.
|
||||
- **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
|
||||
|
||||
@@ -45,8 +45,8 @@ Anything you copy is cleaned on the way to the clipboard: each line loses the pa
|
||||
| --------------------------- | ------------------------------------------------------------ |
|
||||
| `Ctrl+Shift+G` | Open or close the tile grid (needs the Tiles setting on). |
|
||||
| `Alt+Shift+Arrows` | Focus the tile to the left, right, above or below. |
|
||||
| `Ctrl+Shift+Arrows` | Move the focused tile: it trades places with that neighbour. |
|
||||
| Drag a tile's header | Move the tile: dropped on another tile, the two swap. |
|
||||
| `Ctrl+Shift+Arrows` | Move the focused tile one place: into an empty slot, or swap. |
|
||||
| Drag a tile's header | Move the tile: onto another tile they swap, onto an empty slot it moves there. |
|
||||
| `Alt+Shift+Enter` | Zoom the focused tile, or restore the grid. |
|
||||
| `Ctrl`+click / `Cmd`+click a tab | Add that session to the grid. |
|
||||
| Right-click the Tiles button | Choose which sessions to show as tiles. |
|
||||
|
||||
+14
-9
@@ -65,18 +65,23 @@ whose agent exited inside its pane says so instead; close that session from `⋯
|
||||
## Moving tiles
|
||||
|
||||
Drag a tile by its header (anywhere but its buttons) onto another tile and the two trade
|
||||
places; the dropped tile takes the focus. Empty slots are for adding sessions (drop a tab
|
||||
there), so a tile cannot be dropped on one. Press `Escape` or let go anywhere else and nothing
|
||||
changes, not even which tile has the focus: a header focuses its tile when you click it, not
|
||||
when you press it.
|
||||
places. Drop it on an empty slot and it moves there, leaving its old place empty; nothing else
|
||||
moves, so the empty slot can be anywhere in the grid. The dropped tile takes the focus. Press
|
||||
`Escape` or let go anywhere else and nothing changes, not even which tile has the focus: a
|
||||
header focuses its tile when you click it, not when you press it.
|
||||
|
||||
With the keyboard, `Ctrl+Shift+Arrows` moves the focused tile left, right, up or down: it
|
||||
trades places with the tile next to it (the one `Alt+Shift+Arrows` would focus) and keeps the
|
||||
focus.
|
||||
With the keyboard, `Ctrl+Shift+Arrows` moves the focused tile one place left, right, up or
|
||||
down: into the empty slot if that is the place, else trading places with the tile there. It
|
||||
keeps the focus.
|
||||
|
||||
A moved tile takes the size of the place it lands in: column widths and row heights stay
|
||||
where you dragged the dividers. Tiles do not move while one is zoomed. The new order is
|
||||
saved with the grid.
|
||||
where you dragged the dividers. Tiles do not move while one is zoomed. Where everything is,
|
||||
the empty slot included, is saved with the grid and comes back on reload.
|
||||
|
||||
Closing a tile leaves its place empty when the grid keeps its shape (six tiles to five), and a
|
||||
new tile takes the first empty place. When the number of tiles changes the grid's shape (four
|
||||
tiles to five is two columns to three), the tiles keep their places if they still fit, or line
|
||||
up again from the top left. `Alt+Shift+Arrows` and `Ctrl+Tab` never stop on an empty slot.
|
||||
|
||||
## Keys
|
||||
|
||||
|
||||
Reference in New Issue
Block a user