mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 00:49:41 +02:00
feat(tiles): the per-device Tiles setting, and Ctrl+Shift+G follows it
showTileGridButton gets the full per-device treatment Split has: a header chip in App Settings beside Split, its load and save lines, OFF by default (and in the handheld defaults), a member of the displayKeys merge policy, stripped from the settings PUT and never declared in the .strict() SettingsUpdateSchema (sending it would 400 the whole save). The setting also gates the Ctrl+Shift+G toggle (the applied default while the owner's answer is pending; one line in tileShortcutFor to change): OFF, the chord is inert and reaches the terminal like any unbound key; ON, it opens and closes the grid where one can open. A grid that is open however it was opened (Ctrl/Cmd+click, a dropped tab, "Open group as tiles") keeps all its chords, the toggle that closes it included. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1921,6 +1921,7 @@
|
||||
<label class="set-chip" data-preview="header" data-preview-order="10"><input type="checkbox" id="appSettingsShowFileViewerButton"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg><span>File Viewer</span></label>
|
||||
<label class="set-chip" data-preview="header" data-preview-order="11"><input type="checkbox" id="appSettingsShowMultiMonitorButton"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="4" width="13" height="9" rx="1.5"/><rect x="11" y="9" width="11" height="8" rx="1.5"/></svg><span>Multi-monitor</span></label>
|
||||
<label class="set-chip" data-preview="header" data-preview-order="11.5"><input type="checkbox" id="appSettingsShowSplitButton"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="3" width="20" height="18" rx="2"/><line x1="12" y1="3" x2="12" y2="21"/></svg><span>Split</span></label>
|
||||
<label class="set-chip" data-preview="header" data-preview-order="11.6"><input type="checkbox" id="appSettingsShowTileGridButton"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="8" height="8" rx="1"/><rect x="13" y="3" width="8" height="8" rx="1"/><rect x="3" y="13" width="8" height="8" rx="1"/><rect x="13" y="13" width="8" height="8" rx="1"/></svg><span>Tiles</span></label>
|
||||
<label class="set-chip" data-preview="header" data-preview-order="13" data-preview-text="42%"><input type="checkbox" id="appSettingsShowPlanUsageLimits"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 18a8 8 0 1 1 16 0"/><path d="M12 18l4.5-5"/></svg><span>Plan Usage</span></label>
|
||||
<label class="set-chip" data-preview="header" data-preview-order="14"><input type="checkbox" id="appSettingsShowLifecycleLog"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg><span>Lifecycle Log</span></label>
|
||||
</div>
|
||||
|
||||
@@ -430,6 +430,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
settings.ultracodeFloatingWindows ?? defaults.ultracodeFloatingWindows ?? false;
|
||||
document.getElementById('appSettingsShowMultiMonitorButton').checked = settings.showMultiMonitorButton ?? defaults.showMultiMonitorButton ?? false;
|
||||
document.getElementById('appSettingsShowSplitButton').checked = settings.showSplitButton ?? defaults.showSplitButton ?? false;
|
||||
document.getElementById('appSettingsShowTileGridButton').checked = settings.showTileGridButton ?? defaults.showTileGridButton ?? false;
|
||||
document.getElementById('appSettingsShowPlanUsageLimits').checked = this.planUsageChipEnabled(settings);
|
||||
document.getElementById('appSettingsShowRedrawButton').checked = settings.showRedrawButton ?? defaults.showRedrawButton ?? false;
|
||||
// Phone overview home screen: only meaningful under 600px, so the row is
|
||||
@@ -2492,6 +2493,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
ultracodeFloatingWindows: document.getElementById('appSettingsUltracodeFloatingWindows').checked,
|
||||
showMultiMonitorButton: document.getElementById('appSettingsShowMultiMonitorButton').checked,
|
||||
showSplitButton: document.getElementById('appSettingsShowSplitButton').checked,
|
||||
showTileGridButton: document.getElementById('appSettingsShowTileGridButton').checked,
|
||||
showPlanUsageLimits: document.getElementById('appSettingsShowPlanUsageLimits').checked,
|
||||
showRedrawButton: document.getElementById('appSettingsShowRedrawButton').checked,
|
||||
mobileOverviewEnabled: document.getElementById('appSettingsMobileOverview').checked,
|
||||
@@ -2730,6 +2732,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
// SettingsUpdateSchema (.strict()) — sending it 400s the whole PUT
|
||||
// (moving it into displayKeys alone is not the strip; this is).
|
||||
showSplitButton: _ssp,
|
||||
// Same as Split: a per-device header button (and the Tiles chord), absent
|
||||
// from SettingsUpdateSchema (.strict()), so sending it 400s the whole PUT.
|
||||
showTileGridButton: _stg,
|
||||
webglRendererEnabled: _wgl,
|
||||
terminalWheelLocalScrollback: _twls,
|
||||
// Copy-on-select. Per-device (clipboard access differs by device and by
|
||||
@@ -3420,6 +3425,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
ultracodeFloatingWindows: false,
|
||||
showMultiMonitorButton: false,
|
||||
showSplitButton: false,
|
||||
showTileGridButton: false,
|
||||
// Desktop defaults this ON (see planUsageChipEnabled); handhelds keep it
|
||||
// OFF so the phone header stays minimal and the mobile-header-buttons
|
||||
// policy guard keeps passing.
|
||||
@@ -4057,6 +4063,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
'mobileOverviewEnabled',
|
||||
'sessionLineageLines',
|
||||
'showSplitButton',
|
||||
'showTileGridButton',
|
||||
]);
|
||||
// The plan-usage chip is a PER-DEVICE display setting (desktop default ON,
|
||||
// handheld default OFF): desktop can show it while mobile stays hidden. Drop
|
||||
|
||||
@@ -552,11 +552,14 @@ Object.assign(CodemanApp.prototype, {
|
||||
},
|
||||
|
||||
/**
|
||||
* The tile chord `e` asks for, if it applies right now, else null: the toggle
|
||||
* wherever a grid could open (or is open), the focus and remove chords only
|
||||
* while it is open, so outside the grid they reach the terminal untouched.
|
||||
* Registry-aware (rebinds and disables in App Settings, Shortcuts). The
|
||||
* capture handler (app.js) dispatches it; every xterm key handler returns
|
||||
* The tile chord `e` asks for, if it applies right now, else null. The
|
||||
* toggle applies while the grid is open, or where one could open AND the
|
||||
* per-device `showTileGridButton` setting is on: with it off (the default)
|
||||
* the chord is inert and reaches the terminal like any unbound key. (The
|
||||
* applied default pending the owner's answer; one line to change.) The focus,
|
||||
* zoom and remove chords apply only while the grid is open, however it was
|
||||
* opened. Registry-aware (rebinds and disables in App Settings, Shortcuts).
|
||||
* The capture handler (app.js) dispatches it; every xterm key handler returns
|
||||
* false for it, so a chord that applies never reaches a PTY.
|
||||
*
|
||||
* @returns {string|null} the registry id
|
||||
@@ -568,8 +571,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
for (const shortcut of this.getShortcutRegistry()) {
|
||||
const spec = TILE_SHORTCUTS[shortcut.id];
|
||||
if (!spec || shortcut.disabled || !this.matchesShortcutEvent(e, shortcut)) continue;
|
||||
if (spec.needsOpen ? open : open || this.canOpenTileGrid()) return shortcut.id;
|
||||
return null;
|
||||
if (spec.needsOpen) return open ? shortcut.id : null;
|
||||
const enabled = this.loadAppSettingsFromStorage?.()?.showTileGridButton === true;
|
||||
return open || (enabled && this.canOpenTileGrid()) ? shortcut.id : null;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// test/tile-grid-per-device-setting.test.ts
|
||||
// Port: none (pure static analysis, runs in CI, no browser/server).
|
||||
//
|
||||
// `showTileGridButton` (the header Tiles button, and the Ctrl+Shift+G chord) is
|
||||
// a PER-DEVICE setting, default OFF, with the same three-way rule as
|
||||
// showSplitButton (test/split-pane-per-device-setting.test.ts, read it for the
|
||||
// history): in settings-ui.js's displayKeys merge policy, stripped out of the
|
||||
// object saveAppSettings() PUTs (SettingsUpdateSchema is .strict(), so sending
|
||||
// it would 400 the whole save), and never declared in the schema. Plus the
|
||||
// parts that make the setting reachable: the App Settings chip, its load and
|
||||
// save lines, the mobile default, and the `--hidden` marker's display:none
|
||||
// rule (also covered generically by split-pane-hidden-button-css.test.ts).
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const HERE = fileURLToPath(new URL('.', import.meta.url));
|
||||
const PUBLIC = join(HERE, '../src/web/public');
|
||||
const read = (file: string) => readFileSync(join(PUBLIC, file), 'utf8');
|
||||
|
||||
describe('showTileGridButton stays per-device: display key, stripped from the PUT, absent from the schema', () => {
|
||||
const settingsUi = read('settings-ui.js');
|
||||
const schemas = readFileSync(join(HERE, '../src/web/schemas.ts'), 'utf8');
|
||||
|
||||
it('is in the client-side displayKeys merge policy', () => {
|
||||
const start = settingsUi.indexOf('const displayKeys = new Set([');
|
||||
const displayKeys = settingsUi.slice(start, settingsUi.indexOf('])', start));
|
||||
expect(displayKeys).toContain("'showTileGridButton'");
|
||||
});
|
||||
|
||||
it('is stripped out of the object saveAppSettings() PUTs to the server', () => {
|
||||
expect(settingsUi).toContain('showTileGridButton: _stg,');
|
||||
});
|
||||
|
||||
it('is never declared in the .strict() SettingsUpdateSchema', () => {
|
||||
expect(schemas).not.toContain('showTileGridButton');
|
||||
});
|
||||
|
||||
it('defaults OFF on handhelds (and reads as OFF when absent everywhere)', () => {
|
||||
expect(settingsUi).toMatch(/showSplitButton: false,\s*showTileGridButton: false,/);
|
||||
expect(settingsUi).toContain(
|
||||
"document.getElementById('appSettingsShowTileGridButton').checked = settings.showTileGridButton ?? defaults.showTileGridButton ?? false;"
|
||||
);
|
||||
});
|
||||
|
||||
it('is saved from its App Settings chip and applied to the header', () => {
|
||||
expect(settingsUi).toContain(
|
||||
"showTileGridButton: document.getElementById('appSettingsShowTileGridButton').checked,"
|
||||
);
|
||||
expect(settingsUi).toContain('this._applyTileGridButtonVisibility?.(showTileGridButton);');
|
||||
});
|
||||
});
|
||||
|
||||
describe('markup and styles', () => {
|
||||
const html = read('index.html');
|
||||
const css = read('styles.css');
|
||||
|
||||
it('has a header chip in App Settings, beside Split', () => {
|
||||
expect(html).toMatch(
|
||||
/data-preview-order="11\.6"><input type="checkbox" id="appSettingsShowTileGridButton">[\s\S]*?<span>Tiles<\/span>/
|
||||
);
|
||||
});
|
||||
|
||||
it('ships the header button hidden by its marker class, which has a display:none rule', () => {
|
||||
expect(html).toContain('class="btn-icon-header btn-tile-grid btn-tile-grid--hidden"');
|
||||
expect(css).toMatch(/\.btn-tile-grid--hidden\s*\{\s*display: none !important;/);
|
||||
});
|
||||
});
|
||||
@@ -46,6 +46,11 @@ const chord = (overrides: Record<string, unknown>) => ({
|
||||
...overrides,
|
||||
});
|
||||
const TOGGLE = { key: 'G', code: 'KeyG', ctrlKey: true, shiftKey: true };
|
||||
/** The per-device Tiles setting on (it enables the Tiles button and the toggle chord). */
|
||||
const withTilesSetting = (app: GridApp) => {
|
||||
app.loadAppSettingsFromStorage = () => ({ showTileGridButton: true });
|
||||
return app;
|
||||
};
|
||||
const RIGHT = { key: 'ArrowRight', code: 'ArrowRight', altKey: true, shiftKey: true };
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -71,15 +76,27 @@ describe('registry', () => {
|
||||
});
|
||||
|
||||
describe('when a chord applies', () => {
|
||||
it('the toggle applies wherever a grid could open, and while one is open', () => {
|
||||
const app = makeGridApp(IDS);
|
||||
it('with the Tiles setting on, the toggle applies wherever a grid could open, and while one is open', () => {
|
||||
const app = withTilesSetting(makeGridApp(IDS));
|
||||
expect(app.tileShortcutFor(chord(TOGGLE))).toBe('toggle-tile-grid');
|
||||
app.openTileGrid(IDS);
|
||||
expect(app.tileShortcutFor(chord(TOGGLE))).toBe('toggle-tile-grid');
|
||||
});
|
||||
|
||||
it('the toggle does not apply in a narrow window or a solo window', () => {
|
||||
it('with the Tiles setting off (the default) the toggle is inert, like an unbound key', () => {
|
||||
const app = makeGridApp(IDS);
|
||||
expect(app.tileShortcutFor(chord(TOGGLE))).toBeNull();
|
||||
});
|
||||
|
||||
it('with the setting off, a grid opened another way (Ctrl+click, a drop) still has its chords, toggle included', () => {
|
||||
const app = makeGridApp(IDS);
|
||||
app.openTileGrid(IDS);
|
||||
expect(app.tileShortcutFor(chord(RIGHT))).toBe('focus-tile-right');
|
||||
expect(app.tileShortcutFor(chord(TOGGLE))).toBe('toggle-tile-grid');
|
||||
});
|
||||
|
||||
it('the toggle does not apply in a narrow window or a solo window', () => {
|
||||
const app = withTilesSetting(makeGridApp(IDS));
|
||||
gridWindow.innerWidth = 1000;
|
||||
expect(app.tileShortcutFor(chord(TOGGLE))).toBeNull();
|
||||
gridWindow.innerWidth = 2400;
|
||||
@@ -131,8 +148,17 @@ describe('the capture-phase handler', () => {
|
||||
return keydown[1];
|
||||
}
|
||||
|
||||
it('Ctrl+Shift+G opens the grid on the active session, then closes it, overriding the browser', () => {
|
||||
it('setting off: Ctrl+Shift+G is left alone (no preventDefault), the grid stays closed', () => {
|
||||
const app = makeGridApp(IDS);
|
||||
const onKeydown = handlerFor(app);
|
||||
const e = chord(TOGGLE);
|
||||
onKeydown(e);
|
||||
expect(e.preventDefault).not.toHaveBeenCalled();
|
||||
expect(app._tilesOwnTerminal()).toBe(false);
|
||||
});
|
||||
|
||||
it('Ctrl+Shift+G opens the grid on the active session, then closes it, overriding the browser', () => {
|
||||
const app = withTilesSetting(makeGridApp(IDS));
|
||||
app.selectSession = vi.fn();
|
||||
const onKeydown = handlerFor(app);
|
||||
const open = chord(TOGGLE);
|
||||
|
||||
Reference in New Issue
Block a user