From 7debebb7b38bde79fd63b291fc5c4c118d3a678e Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Thu, 8 Oct 2026 04:10:55 +0200 Subject: [PATCH] feat(tiles): right-click Tiles is a 2 / 4 / 6 count menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner decision 10 ("give me then the option to choose only HOW many tiles, 2,4,6 default is 6 so the menu is easier"; asked where it lives: "Click opens 6"). The click still opens the grid at once; right-click, Shift+F10 or the Menu key on the button opens a small menu of three counts, each drawn as the grid's own layout (2x1, 2x2, 3x2), the remembered one checked. It replaces the session picker, which is gone (method, markup hook, CSS, zh-CN entries). - A pick is remembered per device in codeman:tile-count (default 6; codeman:tile-grid stays ids only) and opens that many tiles. The click and Ctrl+Shift+G then open with it, at most what the window fits. - Which sessions: the rule the click already had (the grid this tab last had, else an open split's two, else tab order, the active one included and focused), trimmed from the end (the focused one kept) or filled from tab order to the count. A remembered grid comes back with its tiles first, in their cells, holes filled first, then tab order (owner answer, superseding decision 8's "exactly the stored set"). A page-load restore still brings back exactly what was stored. - With the grid open a pick re-forms it: a count change is a shape change under the cell model's rule (reformTileCells), the focused tile always kept, every joining tile mounted and laid out before any of them connects, so each fits once and sends one PTY resize. - Ctrl/Cmd+click on a tab with the grid closed opens the count in total, that session among them and focused (owner answer: N, not N+1). - A count the window cannot fit is greyed out with the reason; a remembered one stays checked, the keyboard starts on the largest that fits. Arrows, Home/End, Enter or Space; Escape closes the menu alone (the global handler gives it the key first, like the tab-group menu) and puts the keyboard back on the Tiles button; Tab and a click elsewhere close it. - zh-CN for every new string (N 个窗格, 窗格数量, the titles, the Help modal row); the i18n test harvests the menu now, with a session named "6 tiles" as the user-text trap. Tests: tile-grid-picker.test.ts becomes tile-grid-count-menu.test.ts (the button checks kept, every picker check carried over to the menu, plus keyboard, remembered count, re-form, Ctrl/Cmd+click and the batch connect); the open-set, cap, restore, shortcuts, split-coexistence and i18n expectations follow the count; the Help modal test escapes its label (the new one has parentheses). The vm harness tracks document.activeElement and makes SVG elements. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/web/public/app.js | 7 +- src/web/public/constants.js | 8 +- src/web/public/i18n.js | 14 +- src/web/public/index.html | 4 +- src/web/public/styles.css | 118 +++--- src/web/public/tile-grid.js | 419 ++++++++++++++------- test/help-modal-shortcuts.test.ts | 4 +- test/mocks/tile-grid-vm.ts | 14 +- test/tile-grid-cap.test.ts | 35 +- test/tile-grid-count-menu.test.ts | 453 +++++++++++++++++++++++ test/tile-grid-i18n.test.ts | 71 ++-- test/tile-grid-open-set.test.ts | 54 +-- test/tile-grid-picker.test.ts | 198 ---------- test/tile-grid-restore.test.ts | 30 ++ test/tile-grid-shortcuts.test.ts | 9 +- test/tile-grid-split-coexistence.test.ts | 18 +- 16 files changed, 973 insertions(+), 483 deletions(-) create mode 100644 test/tile-grid-count-menu.test.ts delete mode 100644 test/tile-grid-picker.test.ts diff --git a/src/web/public/app.js b/src/web/public/app.js index 390f64b4..f7669682 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -1417,12 +1417,17 @@ class CodemanApp { this._tabLayoutDragKeydown(e); return; } + // So does the Tiles count menu: it closes alone and gives the keyboard + // back to the Tiles button (tile-grid.js). + if (this._tileCountMenu) { + this.closeTileCountMenu({ refocus: true }); + return; + } this.closeAllPanels(); this.closeHelp(); if (this.attachmentHistoryDrawerOpen) this.closeAttachmentHistory(); this.closeSessionManager(); this.closeCommandPalette?.(); - this.closeTilePicker?.(); this.closeShortcutOverlay?.(); // Overlay layouts only: below 1024px the sidebar is a modal off-canvas // drawer over the terminal, so Escape must close it. The docked desktop diff --git a/src/web/public/constants.js b/src/web/public/constants.js index b170c453..773c0127 100644 --- a/src/web/public/constants.js +++ b/src/web/public/constants.js @@ -1793,8 +1793,8 @@ function dragTrackFractions(fr, index, deltaPx, totalPx, minPx) { } /** - * The sessions the Tiles picker offers (and the Tiles button opens, case c of - * tileGridOpenSet), in tab order: live ones only, never a session popped out to + * The sessions the Tiles button can open (case c of tileGridOpenSet, and the + * ones a count fills a grid with), in tab order: live ones only, never a session popped out to * its own window (that window owns its PTY size). A session with no PTY * attached IS offered: its tile shows the Attach overlay. * @@ -1820,8 +1820,8 @@ function buildTilePickerSessions(sessions, sessionOrder, detachedIds) { * a. the grid this tab last had (`stored`, already sanitized: live, not * detached, at most the cap), if any of its sessions survive; * b. else an open split's two sessions, Pane A focused; - * c. else the open sessions in tab order, the picker's list (no detached - * ones), up to `limit`, the active session always among them and focused + * c. else the open sessions in tab order (buildTilePickerSessions: no + * detached ones), up to `limit`, the active session always among them and focused * (when it sits past the limit, the first `limit - 1` others come with it). * Null when there is nothing to open. * diff --git a/src/web/public/i18n.js b/src/web/public/i18n.js index e0cb2f48..ff3ad3f0 100644 --- a/src/web/public/i18n.js +++ b/src/web/public/i18n.js @@ -103,10 +103,10 @@ Tiles: '平铺', Split: '分屏', 'Tiled sessions': '平铺的会话', - 'Tiles: show several sessions side by side (right-click to choose which)': - '平铺:并排显示多个会话(右键单击可选择会话)', - 'Tiles: back to a single session (right-click to choose which sessions)': - '平铺:返回单个会话(右键单击可选择会话)', + 'Tiles: show several sessions side by side (right-click for how many)': + '平铺:并排显示多个会话(右键单击可选择窗格数量)', + 'Tiles: back to a single session (right-click for how many tiles)': '平铺:返回单个会话(右键单击可选择窗格数量)', + 'How many tiles': '窗格数量', 'Split: unavailable while tiles are open': '分屏:平铺打开时不可用', 'Toggle Tile Grid': '切换平铺网格', 'Focus Tile Left': '聚焦左侧窗格', @@ -125,7 +125,7 @@ 'Zoom Focused Tile': '放大聚焦的窗格', 'Remove Focused Tile': '移除聚焦的窗格', 'Add the Session to the Tile Grid': '将该会话加入平铺网格', - 'Choose Which Sessions to Tile': '选择要平铺的会话', + 'Choose How Many Tiles (2, 4 or 6)': '选择窗格数量(2、4 或 6)', 'a tab': '标签页', 'the Tiles button': '平铺按钮', Click: '单击', @@ -133,8 +133,6 @@ Arrows: '方向键', 'not bound': '未绑定', 'Open group as tiles': '以平铺方式打开分组', - 'Show sessions as tiles': '以平铺方式显示会话', - 'Open tiles': '打开平铺', 'No sessions to show as tiles': '没有可平铺显示的会话', 'This group has no session to show as tiles': '此分组没有可平铺显示的会话', 'Zoom this tile': '放大此窗格', @@ -1058,7 +1056,7 @@ (_m, group) => `删除分组“${group}”?其中的标签将移到未分组。`, ], // Tile grid: counts, exit codes and durations pass through. - [/^Up to (\d+) tiles$/, (_m, n) => `最多 ${n} 个窗格`], + [/^(\d+) tiles$/, (_m, n) => `${n} 个窗格`], [/^This window fits (\d+) tiles?$/, (_m, n) => `此窗口可容纳 ${n} 个窗格`], [/^The grid holds at most (\d+) tiles$/, (_m, n) => `平铺网格最多容纳 ${n} 个窗格`], [ diff --git a/src/web/public/index.html b/src/web/public/index.html index 3fbe4530..d868c437 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -192,7 +192,7 @@ - +
—