fix(tiles): file drops, focus handoffs, pop-out fallback, split merge, tablet default

- A file dragged onto the tile grid navigated the browser away: the single
  view's drop handler sits on #terminalContainer, hidden while tiles are
  open. The #tileGrid section now cancels every file dragover and drop
  (bubble phase, so tab and tile drags stay with _acceptTabDrops), and a
  drop on a tile uploads its images to THAT tile's session through
  _uploadAndInsertImages, with the same "Only image files are supported"
  toast as image-input.js (now in the zh-CN table).
- App-driven refocus no longer moves DOM focus into another session's
  xterm: a remote delete of the focused tile, _reconcileTileGrid and a
  socket closed with 4003/4004/4010 (_onTileExit) pass focus: false.
  removeTile gains a focus option; user-initiated removes keep focusing.
- Popping out the last tile left the parked terminal's stale content under
  the popped-out tab (and snapshotted it on the next switch).
  _selectAfterTileGrid treats a detached session as unusable for both the
  focused id and the fallback.
- "Open group as tiles" and Ctrl/Cmd+click with the grid closed pass
  mergeSplit: false, so an open split no longer adds its two sessions on
  top of a set already sized to the group, the count and the window.
- Touch-primary devices (primary pointer coarse: iPad, Android tablets)
  default the Tiles button OFF in getDefaultSettings(); touchscreen
  laptops (fine primary pointer) keep the desktop default ON. The button,
  the App Settings chip and the Ctrl+Shift+G gate all resolve an absent key
  through these defaults, so they agree. CLAUDE.md and the invariants doc
  say so.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-09 08:44:52 +02:00
parent f855b5d274
commit 18c8b5c280
12 changed files with 481 additions and 25 deletions
+56
View File
@@ -100,6 +100,62 @@ describe('opening the grid over an open split', () => {
expect(app._tileGrid.ids).toEqual(['s-a', 's-b', 's-c']);
expect(app.activeSessionId).toBe('s-a');
});
// The entry points that size their own set never get the split's two
// prepended on top of it (that went past the group, the count and the
// window's capacity, and auto-zoomed with a "too small" toast).
it("'Open group as tiles' over a split opens exactly the group, the split closed", () => {
const app = makeGridApp([...IDS, 's-d']);
app.tabLayout = {
groups: [
{
id: 'g',
name: 'G',
refs: [
{ kind: 'session', id: 's-c' },
{ kind: 'session', id: 's-d' },
],
},
],
};
const paneB = openSplit(app);
app.openGroupAsTiles('g');
expect(paneB.destroy).toHaveBeenCalledTimes(1);
expect(app._splitPane).toBeNull();
expect(app._tileGrid.ids).toEqual(['s-c', 's-d']);
expect(app.activeSessionId).toBe('s-c');
});
it('Ctrl/Cmd+click over a split opens the remembered count in total, never one more', () => {
const app = makeGridApp(IDS);
localStore.set('codeman:tile-count', '2');
openSplit(app);
app.addSessionToTiles('s-c');
expect(app._splitPane).toBeNull();
// The split's Pane A seeds the set (tileGridOpenSet), the clicked one joins: 2, not 3.
expect(app._tileGrid.ids).toEqual(['s-a', 's-c']);
expect(app.activeSessionId).toBe('s-c');
});
it('Ctrl/Cmd+click over a split with a remembered grid: the remembered grid wins, not the split', () => {
const app = makeGridApp(IDS);
app.selectSession = vi.fn();
app.openTileGrid(['s-other', 's-c']);
app.closeTileGrid({ reselect: false });
app.activeSessionId = 's-a';
localStore.set('codeman:tile-count', '2');
openSplit(app);
app.addSessionToTiles('s-b');
expect(app._splitPane).toBeNull();
expect(app._tileGrid.ids).toEqual(['s-other', 's-b']);
expect(app.activeSessionId).toBe('s-b');
});
});
describe('while the grid is open', () => {