feat(gesture): port session improvements — direct detach, Run/Run Shell taps, self-hosted MediaPipe, cache-bust

Updates the opt-in gesture overlay (still gated by CODEMAN_GESTURE=1):

- Bundle (gesture-codeman.js) rebuilt from Ark0N/codeman-gesture-control:
  - Detach now calls window.app.detachSession(id) directly (the on-tab pop-out
    hook) instead of a separate /session/:id window.open reimplementation.
  - Pinch a session tab → ghost follows your hand → pull out to undock.
  - Pinch the Run (#runBtn → app.run()) or Run Shell (.btn-shell →
    app.runShell()) toolbar button to fire it; drift cancels the tap.
  - Camera shows fullscreen-dimmed by default (⛶ toggles a corner preview).
  - Robust start-error reporting; GPU→CPU MediaPipe delegate fallback.

- Self-hosted MediaPipe (no CDN): serves the wasm runtime + gesture_recognizer
  .task from /gesture/ so a browser content-blocker can't break startup. The
  overlay points wasmBase/modelUrl there. (~27MB of assets; could later be a
  build/postinstall fetch instead of committed blobs.)

- server.ts: cache-bust the injected bundle URL with its mtime (?v=), since
  static is served with a 1-year cache — a redeploy is now never stale.

format:check / lint scope (src/**/*.ts) clean; server.ts typechecks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ark0N
2026-06-08 01:20:23 +02:00
co-authored by Claude Opus 4.8
parent ceca85365c
commit eea84db05e
7 changed files with 224 additions and 45 deletions
+17 -2
View File
@@ -36,7 +36,7 @@ import fastifyMultipart from '@fastify/multipart';
import { startPasteImageGc } from './paste-image-gc.js';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { existsSync, mkdirSync, readFileSync, chmodSync, rmSync } from 'node:fs';
import { existsSync, mkdirSync, readFileSync, chmodSync, rmSync, statSync } from 'node:fs';
import fs from 'node:fs/promises';
import { execSync } from 'node:child_process';
import { hostname as getHostname } from 'node:os';
@@ -1012,11 +1012,26 @@ export class WebServer extends EventEmitter {
// same-origin from /gesture/ so 'self' covers it; CSP is widened to match in
// registerSecurityHeaders under the same flag.
if (!soloSessionId && process.env.CODEMAN_GESTURE === '1') {
html = html.replace('</head>', `<script type="module" src="/gesture/gesture-codeman.js"></script>\n</head>`);
const v = this.gestureBundleVersion();
html = html.replace('</head>', `<script type="module" src="/gesture/gesture-codeman.js${v}"></script>\n</head>`);
}
return html;
}
/** Cache-busting query for the gesture bundle: its mtime, re-read per render.
* The bundle is served from /gesture/ with a 1-year cache, so without a
* version that changes on redeploy the browser would keep running a stale
* bundle forever. Re-stat'ing each render means a freshly copied-in bundle is
* picked up with no server restart. Empty string if the file is missing. */
private gestureBundleVersion(): string {
try {
const p = join(__dirname, 'public', 'gesture', 'gesture-codeman.js');
return `?v=${Math.floor(statSync(p).mtimeMs)}`;
} catch {
return '';
}
}
private async setupSessionListeners(session: Session): Promise<void> {
// Create run summary tracker for this session
const summaryTracker = new RunSummaryTracker(session.id, session.name);