From 94b26f760612fe7b8dd8850688e33d588f88ec67 Mon Sep 17 00:00:00 2001 From: Ark0N Date: Mon, 8 Jun 2026 06:01:15 +0200 Subject: [PATCH 1/3] feat(settings): toggle gesture control + multi-monitor button (off by default) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the two experimental header features opt-in via App Settings instead of forced on. Both default OFF. - App Settings → Display → 'Header Displays' gains a 'Multi-monitor Button' toggle (setting: showMultiMonitorButton). The button is hidden in the template by default; the server reveals it at render when enabled, and applyHeaderVisibilitySettings handles live toggles from a save. - App Settings → Display → new 'Input' section gains a 'Gesture Control' toggle (setting: gestureControlEnabled). The gesture overlay is injected at page render, so renderIndexHtml (now async) reads settings.json and injects the bundle only when enabled; toggling reloads the page. CODEMAN_GESTURE=1 stays the instance-level 'feature available' gate (CSP + assets) and exposes window.__codemanGestureAvailable so the Input section only shows when usable. - The retired notification bell stays hidden regardless of notification state. Both settings added to SettingsUpdateSchema and the mobile defaults. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/web/public/index.html | 21 ++++++++++++++- src/web/public/settings-ui.js | 43 ++++++++++++++++++++++++++++--- src/web/schemas.ts | 3 +++ src/web/server.ts | 48 ++++++++++++++++++++++++++++------- 4 files changed, 102 insertions(+), 13 deletions(-) diff --git a/src/web/public/index.html b/src/web/public/index.html index b5473817..405804e3 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -105,7 +105,7 @@ - + - + ', + '', +].join('\n'); + +function makeServer(settings: Record = {}) { + const server = new WebServer(0, false, true); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (server as any).indexHtmlTemplate = TEMPLATE; + const readSettings = vi.fn(async () => settings); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (server as any).readSettings = readSettings; + return { server, readSettings }; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const render = (server: WebServer, solo?: string): Promise => (server as any).renderIndexHtml(solo); + +const ORIG_GESTURE = process.env.CODEMAN_GESTURE; +afterEach(() => { + if (ORIG_GESTURE === undefined) delete process.env.CODEMAN_GESTURE; + else process.env.CODEMAN_GESTURE = ORIG_GESTURE; +}); + +describe('WebServer.renderIndexHtml', () => { + it('keeps the multi-monitor button hidden by default and reads settings FRESH', async () => { + const { server, readSettings } = makeServer({}); + const html = await render(server); + expect(html).toContain('btn-multimonitor--hidden'); + // forceFresh=true — fixes the post-save reload race against the 2s cache. + expect(readSettings).toHaveBeenCalledWith(true); + }); + + it('reveals the multi-monitor button when showMultiMonitorButton is set', async () => { + const { server } = makeServer({ showMultiMonitorButton: true }); + const html = await render(server); + expect(html).not.toContain('btn-multimonitor--hidden'); + expect(html).toContain('btn-multimonitor"'); // class list still present, only the marker stripped + }); + + it('injects the solo global and skips settings for a /session/:id window', async () => { + const { server, readSettings } = makeServer({ showMultiMonitorButton: true }); + const html = await render(server, 'sess-123'); + expect(html).toContain('window.__CODEMAN_SOLO__="sess-123"'); + expect(readSettings).not.toHaveBeenCalled(); + // Solo skips settings, so the button is NOT revealed even though the setting is on. + expect(html).toContain('btn-multimonitor--hidden'); + }); + + it('escapes the solo id so it cannot break out of the inline '); + expect(html).not.toContain(''); + expect(html).toContain('\\u003c'); + }); + + it('exposes gesture availability but injects the bundle only when enabled', async () => { + process.env.CODEMAN_GESTURE = '1'; + let { server } = makeServer({ gestureControlEnabled: false }); + let html = await render(server); + expect(html).toContain('window.__codemanGestureAvailable=true'); + expect(html).not.toContain('gesture-codeman.js'); + + ({ server } = makeServer({ gestureControlEnabled: true })); + html = await render(server); + expect(html).toContain('window.__codemanGestureAvailable=true'); + expect(html).toContain('gesture-codeman.js'); + }); + + it('does not expose gesture at all when CODEMAN_GESTURE is unset', async () => { + delete process.env.CODEMAN_GESTURE; + const { server } = makeServer({ gestureControlEnabled: true }); + const html = await render(server); + expect(html).not.toContain('__codemanGestureAvailable'); + expect(html).not.toContain('gesture-codeman.js'); + }); +});