Fresh desktop installs slid the Monitor panel open at startup — the two
showMonitor fallbacks defaulted to true (mobile already defaulted false
via getDefaultSettings). Default it to false everywhere; users opt in
via App Settings -> Show Monitor.
Also fix toggleMonitorPanel(): applyMonitorVisibility() leaves inline
display:none when the setting is off, so the session-tab task badge
toggled the open class invisibly (already broken on mobile). Clear the
inline display when opening so transient opens work.
Local echo defaults audited, unchanged: off on desktop, on for touch
(?? MobileDetection.isTouchDevice()), stored per-device and never
server-synced.
Verified on a fresh isolated instance (desktop + iPhone 13 emulation):
14/14 checks — panel closed + checkbox unchecked on both device
classes, local echo desktop-off/mobile-on, separate storage keys,
badge open works.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The default (simple) accessory bar above the mobile keyboard had no way
to send Escape — the esc action only existed in the opt-in extended
layout. Add the Esc button next to the paste button; the send/refocus
handlers already covered the action.
Verified via Playwright (iPhone 13 emulation): renders next to paste,
sends \x1b to /api/sessions/:id/input, bar still fits 390px without
scrolling, mode round-trips intact.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adversarial post-rebase audit (11 agents) confirmed four real issues;
all fixed:
- Desktop tab clicks stopped focusing the terminal: handleSessionTabClick
passed preserveKeyboard:false on desktop (KeyboardHandler.keyboardVisible
is mobile-only state) and selectSession's ternary mapped explicit false
to 'never focus', skipping the gesture-stack focus master relies on.
Focus policy now lives solely in _shouldFocusTerminalForTabSwitch()
(desktop: always; touch: only while the keyboard is open).
- Desktop sizing claims were almost never registered: selectSession's
resizes run before _connectWs, so they went over HTTP (which never
claims), leaving the arbitration inert in the canonical desktop+phone
scenario. ws.onopen now sends a typed resize over the fresh socket —
registering the claim and syncing PTY dims after (re)connects.
- throttledResize (the main window-resize path) sent untyped HTTP
resizes: a rotating phone bypassed a desktop claim, and a desktop
narrowing past the tablet breakpoint never released its stale claim.
It now sends typed resizes, WS-first, like sendResize.
- The cjkInputEnabled App Settings toggle was silently ignored on touch
phones/tablets (composer only reachable via the server inputCjkForm
override, while the checkbox stayed visible and saveable). The user
setting is honored everywhere again; mobile keeps native-input-by-
default via the cjkInputEnabled:false mobile default.
- _handleCjkInput appended multi-byte ESC sequences (hardware-keyboard
arrows/Home/End on the composer) to local-echo pending text, typing
raw ESC bytes into the prompt on Enter; they are now forwarded to the
PTY like the onData path. Its backspace path also syncs the
per-session flushed Maps the way onData does, so tab-switch restore
no longer resurrects deleted characters.
Defensive: Session.stop() clears desktop sizing claims (a hung client's
socket close can lag teardown by a ping cycle), and the claims docblock
documents the WS-only tradeoff explicitly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review follow-ups on PR #111 (rebased onto master post-#112/#113):
Resize arbitration redesigned (review blocker 2): the previous
'cols < _ptyCols' guard froze a mobile-only session's PTY at the spawn
default — narrow phones rendered clipped and could never re-fit. The
guard now uses connection-scoped desktop sizing claims instead:
ws-routes registers a claim on a desktop-typed resize and releases it
on socket close (or when the same connection later reports a small
viewport), and Session.resize() ignores mobile/tablet resizes only
while at least one desktop connection holds a claim. A phone alone
fully controls its size (shrink, rows-only shrink, re-grow); a phone
glancing at a desktop-driven session can no longer reflow it.
mobile-handlers' keyboard open/close resize now declares its viewport
type so it participates in arbitration. Tests rewritten to cover
mobile-only shrink/rows-only/re-grow, claim/release lifecycle, multi-
claim behavior, and untyped legacy resizes; ws-routes test covers the
claim lifecycle over a real socket.
Solo/detached header restored (review blocker 3): index.html had
removed #soloSessionTitle and #soloRedockBtn, which _applySoloMode
still references — every detached window hit a null deref. Both are
back alongside the new mobile utility toggle.
Desktop leak fixed (review should-fix): .mobile-header-utility-toggle
had no rule outside the <=768px media queries, so the raw button
rendered on desktop. styles.css now hides it by default; the mobile/
tablet queries re-enable it.
Visual-regression baselines reverted to master (review should-fix):
the 18 contributor-machine PNGs are environment-specific (8 of the
behavioral tests already report environment-sensitive failures across
machines); re-baseline deliberately on the canonical machine instead.
The 24 behavioral keyboard/layout/tabs tests are kept as-is.
AGENTS.md trimmed to a pointer at CLAUDE.md (review should-fix) to
avoid drift between duplicated guidance.
Also dropped a dead getAttachmentHistoryForPersist stub (codex-branch
residue — no such method exists in src/).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mobile-focused fixes for the web UI: keyboard-accessory layout and
overlap, native input visibility above the keyboard, CJK input handling,
terminal touch scrolling, tab-menu tap targets, mic-recording glow
containment, and mobile resize/keyboard-state handling on tab switch,
plus mobile visual-regression test coverage and snapshots.
Co-Authored-By: Saqeb Akhter <saqeb.akhter@gmail.com>
A 15-agent audit of the merged tree confirmed 9 envelope/contract bugs;
all fixed here, with live-server contract tests added:
Blockers (fresh-install quick start broken):
- session-ui.js runClaude/runShell unwrapped .data from the /api/cases/:name
404 error envelope (which has no data key), so a not-yet-created case threw
TypeError instead of triggering the auto-create fallback. Now '?.data ?? {}'.
Contract violations on the new stable surface:
- Unknown /api routes returned HTTP 404 with {success:true,...} (Fastify's
default not-found payload was wrapped by the envelope hook). Added a
setNotFoundHandler returning the standard error envelope for /api paths.
- POST /api/events/subscribe 400 body became {success:true,data:{error}};
now createErrorResponse(INVALID_INPUT).
- POST /api/clipboard validation error lacked errorCode and shipped HTTP 200;
now createErrorResponse(INVALID_INPUT) -> 400.
- POST /api/run catch path returned bare {success:false,sessionId,error}
(HTTP 200, no errorCode); now OPERATION_FAILED envelope -> 422 with the
dead session id in the message.
- DELETE tail-file/:streamId returned {success: closed}, colliding with the
envelope discriminator; now returns {closed}.
Dead/regressed UI paths:
- Plan history modal could never open: route returned the bare history array
under data while the frontend read data.data.history/currentVersion. Route
now returns {history, currentVersion}; modal task count fixed to stats.total.
- Self-update error toast read j.error.message from the string-typed envelope
error, always falling back to the generic message; now reads the string.
Cleanup:
- Removed the stale QuickStartResponse type (unreferenced; documented the
pre-envelope shape and invited success-key collisions).
Tests: new test/http-contract.test.ts boots a real WebServer (port 3168) and
pins the envelope, /api/v1 alias, error statuses, and the /api 404 shape —
the route-test harness does not install the server-level hook, so these need
the live server. Updated file-routes/plan-routes/scheduled-runs tests to the
fixed shapes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
app.js resolves settingsPromise to the unwrapped settings object
(env?.data ?? null), matching loadAppSettingsFromServer. The
loadQuickStartCases consumer still read settings.data.lastUsedCase,
which silently dropped the last-used-case preselection; its fallback
fetch also missed the envelope unwrap. Align both with the unwrapped
shape.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Conflict in src/web/public/app.js selectSession: combined #112's
_clearTerminalLoadState cleanup on stale select with #113's
{success,data} envelope unwrap of the terminal fetch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the PR #112 re-review (all six prior blockers were already
resolved; these are new issues the rework introduced):
- app.js: define the missing `_scheduleTerminalRepaint()` helper. It was
called from both WebGL-fallback paths (onContextLoss + long-task trip)
but defined nowhere, so each fallback threw `TypeError` and lost the
post-fallback repaint, leaving a stale/blank terminal. Implemented as an
rAF-debounced full refresh (matches the old inline `terminal.refresh`).
- app.js: clear terminal load-state on the two post-write stale-select
early-returns (cached-buffer + rewrite branches), matching the other
four checks. Switching away from a mid-loading tab no longer leaks a
permanent `.tab-loading` spinner / `aria-busy=true`.
- terminal-ui.js + app.js: gate the post-resize TUI-redraw settle on an
actual dimension change. `sendResize` now returns whether dims changed;
a same-size tab switch sends no SIGWINCH, so the wait is skipped instead
of charging a flat tax on every non-shell switch. Literal hoisted to
`TUI_REDRAW_SETTLE_MS`.
- tmux-manager.ts: `resizeWindow()` uses a non-blocking `exec` instead of
`execSync` so the interactive WS/HTTP resize path can't stall the
Fastify event loop on a slow/hung tmux. Sole caller already fire-and-
forgets the result; test updated to assert the async dispatch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Point 1 of the v1.0 lock-in: commit to a stable HTTP API (the cleanest, fullest form).
Core (centralized):
- Every JSON /api response now uses ONE envelope via a Fastify preSerialization hook (src/web/server.ts): success -> { success:true, data:<payload> }; error -> { success:false, error, errorCode } with a conventional HTTP status. Non-JSON routes (file-raw, tail-file SSE, download, screenshots, /q redirect, WS) are skipped.
- Error-code -> HTTP status is a single source of truth (httpStatusForErrorCode in src/types/api.ts): 400/401/404/409/422/429/500. Expanded ApiErrorCode (added UNAUTHORIZED, CONFLICT, RATE_LIMITED). Errors are no longer HTTP 200.
- Versioned alias: /api/v1/* rewrites to /api/* (rewriteApiV1Url), so external clients pin to a stable surface while the bundled UI keeps using /api/*.
- Handlers stripped of manual 'success:true' (50 across 14 route files) so they return bare payloads the hook wraps uniformly; fixed the mux DELETE {success:<bool>} envelope collision (-> {killed}).
Frontend (48 call sites across 10 files):
- _apiJson() auto-unwraps { success:true, data } -> data (null on error), so most bare-shape readers are transparent. Raw-fetch sites relocate payload reads under .data; success/res.ok/error checks unchanged.
Docs: new docs/api-reference.md (envelope, status table, error codes, /api/v1, SSE); versioning-policy.md flipped — the HTTP/SSE API is now part of the stable, SemVer-covered surface.
Verification: full unit/route suite green (2680 passed) incl. ~166 updated assertions across 24 test files; typecheck/lint/format/frontend-syntax clean; a headless-chromium smoke loaded the migrated UI and drove the panels with 0 console/page errors; /api/status and /api/v1/status confirmed returning the uniform envelope live.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mode-agnostic terminal foundation extracted from the downstream branch:
- formatPaneSnapshot: SGR/grapheme-aware tmux pane capture + active-pane
resolution, with OSC/CSI redraw suppression and the buffer-load owner-token
race fix on the terminal fetch path
- socket-correct tmux lifecycle: dedicated -L socket and /tmp launch cwd in
createSession (restores the FUSE/getcwd hardening from #110), and a
socket-aware re-attach window-size query (avoids the 120x40 flicker)
- inline-rename: commit/cancel state handling clears _activeRename and skips
the API call on cancel
- selectSession: restored detached-window raise short-circuit
The codex-specific xterm snapshot/replay, the vendored serialize addon, and
the synchronous live pane-capture on the request path are intentionally
excluded: they depend on a 'codex' SessionMode that doesn't exist on master
and are deferred to COD-34 (which introduces that mode). The capture
primitives remain exported for COD-34 to build on.
Co-Authored-By: Saqeb Akhter <saqeb.akhter@gmail.com>
- M7 (SSRF): add isSafePushEndpoint (https-only; reject internal/loopback/link-local/metadata IPs incl. IPv4-mapped); enforce in PushSubscribeSchema and re-check before webpush.sendNotification. + unit test.
- M1 (command injection): validate tmux session names with isValidMuxName in sessionExists, killSession, and reconcileSessions before they reach a shell call site.
- M5: keep the intentional /var/log + ~/logs log-tail roots (a tested feature) and document the wider read scope in docs/security-architecture.md section 5 instead of dropping it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The updater wrote the status once per phase, so the minute-plus npm install and
build steps left the UI frozen on a single label. Add:
- a heartbeat in scripts/self-update.sh (run_step wrapper) that refreshes
update-status.json every ~3s during the install/build steps with the latest
output line; full output is still mirrored to the update log.
- a frontend (settings-ui.js) that, during non-terminal phases, shows the live
status message plus a ticking total-elapsed counter instead of only the static
phase label.
Takes effect when updating FROM a build that contains it — the detached runner
script (staged from scripts/self-update.sh) and the polling frontend are both
the from-version's copies.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds an always-on Host-header allowlist and a cross-site Origin/CSRF guard,
hardens the text/plain body parser, validates the WebSocket upgrade origin,
and escapes AI-derived fields in the subagent panel. Closes the two
CRITICALs and 5 HIGHs from the 2026-06-09 adversarial security review.
- C1: no Host allowlist -> DNS rebinding drove the full API (RCE) on the
default no-auth loopback install. New registerHostGuard rejects rebound
custom domains; allows loopback, any IP literal, the bind host,
*.ts.net / *.trycloudflare.com / *.cfargotunnel.com, the active managed
tunnel, and CODEMAN_ALLOWED_HOSTS.
- C2: a global text/plain parser JSON-parsed every body, enabling cross-site
simple-request CSRF. Parser now keeps the raw string; /api/crash-diag
self-parses; the global Origin guard rejects cross-site state changes.
- H1/H3/H6: self-update, session create/input, and settings/tunnel toggles
were CSRF-triggerable -> now covered by the Origin guard.
- H4: the subagent activity panel injected raw AI tool names/inputs into
innerHTML (executed under CSP 'unsafe-inline'). All sinks now escapeHtml'd.
- H5: the WebSocket upgrade had no Origin/Host check (CSWSH) -> now validated.
A missing Origin is allowed so curl/CLI and Claude Code hooks keep working;
custom reverse-proxy domains need CODEMAN_ALLOWED_HOSTS=host,.suffix.
Deferred: H2 (self-update tag signing, needs signing infra) and CSP
'unsafe-inline' removal (needs a nonce migration).
Tests: test/network-host-guard.test.ts (19), test/routes/ws-routes.test.ts
updated. Report: docs/reports/security-review-2026-06-09.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update Codeman from the web UI: a "Check for updates" button queries GitHub
for the latest tagged release (git ls-remote fallback) and shows release
notes; "Update now" runs git checkout <tag> → npm install → npm run build →
restart, streaming live progress that survives the service restart.
- Release-tag channel; dirty trees auto-stashed (left for manual git stash pop)
- Cross-platform restart: systemd / launchd / manual, detected at runtime
- Updater runs detached (systemd-run --scope on Linux, setsid on macOS) so the
restart it triggers can't kill the build mid-flight
- Build-failure rollback to the pre-update commit; boot reconcile with an
update-id/freshness guard; 409 concurrency lock; runner staged outside the
repo; strict tag validation; CODEMAN_DISABLE_SELF_UPDATE kill-switch
- Endpoints: GET /api/system/update/check, POST /api/system/update,
GET /api/system/update/status
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the 30 SSE event constants that existed in the backend
src/web/sse-events.ts but were missing from the frontend
SSE_EVENTS object in constants.js, bringing both registries to
an exact 120-event match:
- Session lifecycle: autoCompact, message, interactive, running
- Session: Plan (new): planTaskUpdate, planCheckpoint, planRollback,
planTaskAdded
- Respawn: cycleCompleted, stepSent, stepCompleted, aiCheck* (4),
planCheck* (3), log, configUpdated
- Scheduled: log, deleted
- Teams (new): created, updated, removed, taskUpdated
- Transcript (new): complete, plan_mode, tool_start, tool_end
Purely additive registry constants (none were referenced by raw
string in the frontend, so no behavior changes). Also refresh the
now-accurate event-count JSDoc on both files, and fix the files()
route handler count in CLAUDE.md (5 -> 6).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bring the Ark0N/codeman-gesture-control repo in-tree as the codeman-gesture-control
workspace package so the hand-tracking overlay can be developed in the Codeman repo.
New npm run build:gesture bundles src/codeman/entry.ts into the served
gesture-codeman.js; scripts/build.mjs reruns it on every production build.
Source formatted to Codeman's prettier style.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The App Settings toggle grid used grid-template-columns: 1fr 1fr, which
resolves to minmax(auto, 1fr): the auto minimum equals the items'
min-content (~550px), exceeding the available width and forcing a
horizontal scrollbar with the right-column switches clipped at the edge.
Switch the settings grids to minmax(0, 1fr) tracks so they can shrink
(labels ellipsis-truncate as a last resort instead of blowing out), and
widen the App Settings modal from 540 to 600px so the two-column layout
fits comfortably. Width bump is scoped to #appSettingsModal so the other
modal-lg modal (Add Case) is unaffected.
Verified with Playwright across all six tabs: 0 horizontal overflow,
0 truncated labels, clean single-column collapse at 390px.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Move the Gesture Control (beta) toggle into the existing Input section
(alongside Local Echo / CJK Input / Extended Keyboard Bar); remove the
duplicate "Input" section header. Hide only the toggle (not the whole
section) when CODEMAN_GESTURE=1 is unset.
- scripts/codeman-web.service: set CODEMAN_GESTURE=1 so the gesture feature
is available on the local install (still gated by the default-OFF toggle).
- CLAUDE.md: version sync to 0.8.2 + config/app.js structural-count fixes.
- Version packages -> 0.8.2 (changeset covers detach, gesture overlay,
multi-monitor, settings toggles, cache-busting).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The gesture overlay is an opt-in experimental feature; flag it as beta in the
App Settings → Input toggle label.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Fix the gesture enable-reload race: PUT /api/settings writes settings.json
without invalidating WebServer's 2s _settingsCache, and the toggle reloads
~400ms after save — within the TTL — so renderIndexHtml could render the
pre-toggle state (bundle not injected until a 2nd reload). renderIndexHtml
now reads settings via readSettings(true), a fresh read that bypasses the
cache; readSettings gains a forceFresh param.
- Replace the brittle multi-monitor reveal (string match on the button's
aria-label + inline style) with a stable `btn-multimonitor--hidden` class
marker: the template carries the class, the server strips it when the setting
is on, and applyHeaderVisibilitySettings()/solo-mode CSS toggle the same class.
Editing the button's copy no longer silently breaks the reveal.
- Test: test/render-index-html.test.ts (reveal, solo injection + escaping,
gesture availability vs. enablement, fresh-read wiring).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make the branch genuinely master-mergeable and fix several review findings:
- Defaults are now prod-safe: CODEMAN_INSTANCE defaults to '' (→ ~/.codeman,
-L codeman) and the web port back to 3000, so an existing install upgrades
cleanly. Port also honors a new CODEMAN_PORT env var. Run the beta isolated
alongside prod with scripts/run-beta.sh (CODEMAN_INSTANCE=beta + PORT 5000).
- .gitignore: anchor the root `public` symlink rule to `/public` (a bare
`public` also swallowed src/web/public, silently un-staging new web assets);
ignore the gesture wasm/model binaries explicitly instead.
- span-displays: add a macOS-only guard (400 elsewhere instead of spawning a
bash that fails invisibly); extract resolveSpanUrl() for unit testing.
- server.ts: memoize asset-version stat() calls (~1s TTL) so each index render
doesn't re-stat every script/link tag.
- styles.css: hide the multi-monitor button in solo (detached) windows.
- app.js: require two consecutive unanswered roll-calls before redocking, so a
timer-throttled background popup isn't wrongly un-marked.
- index.html: make the "skip to terminal" link base-href-safe (onclick scroll)
so it doesn't navigate to the dashboard from a /session/:id window.
- Tests: test/config/instance.test.ts, test/routes/system-span-displays.test.ts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
Static assets are served Cache-Control: max-age=1y, immutable, but the script
and link tags in index.html carried no version — so any edit to a frontend
module (panels-ui.js, styles.css, …) stayed cached until a manual hard refresh.
renderIndexHtml now appends ?v=<mtime> to every same-origin .js/.css ref
(generalizing the existing gesture-bundle cache-bust), re-stat'd per render so
a changed file is picked up with no server restart. External URLs, already-
versioned refs, and refs with no file on disk are left untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the header notification bell (now hidden by default; still reachable
via Settings → Notifications and the drawer) with a multi-monitor button.
Clicking it POSTs /api/system/span-displays, which spawns the bundled
scripts/span-codeman.sh — a fresh, maximized browser --app window sized to the
union of all displays — so in-page floating session panels can be dragged
across the physical monitor seam. macOS only; needs the one-time "Displays
have separate Spaces" OFF prerequisite (documented in the script). The route
pins the spanned window to localhost with a digits-only port from the Host
header so nothing attacker-controllable reaches the launched browser.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The self-hosted gesture assets (~27MB of wasm runtime + gesture_recognizer.task)
were committed to the repo. Replace that with scripts/fetch-gesture-assets.mjs,
which downloads them into src/web/public/gesture/ — idempotent (skips existing)
and non-fatal (the overlay is opt-in via CODEMAN_GESTURE=1, so a fetch failure
only warns). Wired into:
- postinstall.js (dev: populates src/web/public/gesture for `npm run dev`)
- build.mjs (before `cp -r src/web/public dist/web/`, so prod/dist gets them)
The files are already covered by the bare `public` .gitignore rule, so they
stay untracked. The overlay bundle (gesture-codeman.js) remains committed — it's
built from a separate repo and is small. Pin @mediapipe wasm to 0.10.21 to match
the bundled tasks-vision API.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MediaPipe's wasm runtime + model are served same-origin from /gesture/, so the
gesture CSP no longer needs https://cdn.jsdelivr.net / https://storage.googleapis
.com in connect-src ('self' covers same-origin). Kept 'wasm-unsafe-eval'
(script-src, WASM compile) and worker-src 'self' blob: (MediaPipe blob workers).
Codeman's base jsdelivr entries (script/style/font-src) are unchanged — those
aren't gesture's.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Wrap the two long CSP-builder lines in registerSecurityHeaders to the 120-col
Prettier limit. Formatting only — no behavior change. Fixes the failing
"Typecheck & Lint" check (prettier --check) on PR #103.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CLAUDE_CODE_EFFORT_LEVEL hard-locks effort for the whole session and makes
Claude reject in-session /effort switching (incl. ultracode). Carry effort
as a dedicated payload field instead, injected at spawn as a soft default:
- regular levels (incl. max) -> claude --effort <level>
(the settings effortLevel key is enum([low,medium,high,xhigh]) with
.catch(undefined), so max would be silently dropped there)
- ultracode -> claude --settings '{"ultracode":true}'
(dedicated boolean settings key, rejected by the --effort flag)
Changes:
- add effort enum field to create/quick-start/ralph-loop schemas and thread
it through Session -> CreateSessionOptions/RespawnPaneOptions -> spawn
- buildEffortCliArgs() in session-cli-builder, shared by tmux spawn command
and direct-PTY fallback args
- frontend: buildEnvOverrides() no longer emits CLAUDE_CODE_EFFORT_LEVEL;
validated effort goes into payloads via getEffortSetting()
- settings UI: add Ultracode option to the Thinking Effort dropdown
- legacy migration: Session constructor extracts CLAUDE_CODE_EFFORT_LEVEL
from persisted envOverrides; applyEnvOverrides() unsets the stale tmux
session var so respawned panes are no longer locked
- tests: test/effort-injection.test.ts (13 cases)
Co-authored-by: Teigen <teigenzhang@gmail.com>
Loads a hand-tracking overlay into the dashboard that detaches a session by
pinch-grabbing its tab and pulling it out — driving the existing
app.detachSession(id) hook. Bundle (src/web/public/gesture/gesture-codeman.js)
is built from the codeman-gesture-control project's src/codeman/entry.ts
(esbuild, MediaPipe included) and served same-origin.
OFF by default — guarded entirely by CODEMAN_GESTURE=1:
- server.ts: injects the module script into the dashboard HTML only (not solo
/session/:id popups, which have no tab strip).
- auth.ts: widens CSP only under the flag — adds 'wasm-unsafe-eval' (MediaPipe
WASM) and the pinned MediaPipe CDNs (cdn.jsdelivr.net wasm, storage.googleapis.com
model) to connect-src, plus worker-src 'self' blob:. Production CSP is unchanged
when the flag is off.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Finding 2: unify the pop-out icon and tab-click paths via _raiseDetached().
After a dashboard reload (no owned WindowProxy ref), clicking the pop-out icon
no longer re-runs window.open() — which reloaded the live popup's terminal —
and instead raises it via the channel, matching the tab-click behavior.
- Finding 3: debounce channel-driven redock. A popup *reload* emits
redocked->detached in quick succession; a 1.5s grace lets the re-announce
cancel the redock so the dashboard badge no longer blips on popup refresh.
- Finding 4: periodic liveness reconcile. A popup hard-killed without a
'pagehide' (crash / OS kill) while the dashboard holds no ref would leave its
tab stuck "detached". The dashboard now re-roll-calls every 5s and re-docks
any channel-only tab that stays silent.
Frontend-only; validated with node --check (app.js is outside the ts/lint/prettier gates).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The instance-isolation sweep routed every ~/.codeman write through dataPath()
except the legacy ~/.claudeman → ~/.codeman migration in the StateStore
constructor, which stayed hardcoded. Gate the whole legacy block on the default
(prod) instance so a named instance (e.g. CODEMAN_INSTANCE=beta) never reads or
renames into the shared ~/.codeman / ~/codeman-cases layout. Prod behavior is
unchanged (CODEMAN_INSTANCE empty → migration still runs).
Note: swapping newDir to getDataDir() was rejected — its mkdirSync side-effect
would make !existsSync(newDir) false and silently disable the migration.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Detach a session tab into its own browser window and back.
Detach/undock:
- GET /session/:id serves the SPA in "solo mode", reusing the existing
client (terminal, local-echo overlay, reconnect) so no terminal code is
duplicated. One PTY already fans out to N SSE/WS clients, so a detached
window is just another live client — no server fan-out work was needed.
- A pop-out icon per tab; detached tabs show a badge and focus the popup on
click; closing the popup re-docks. Cross-window state via BroadcastChannel
plus a WindowProxy poll, and survives a dashboard reload (roll-call).
app.detachSession(id) is a single idempotent entry point (future gesture
hook). <base href="/"> so relative assets resolve under /session/:id.
Beta-branch isolation (so it can run alongside a prod Codeman):
- Default port 3000 -> 5000.
- New src/config/instance.ts derives the data dir and tmux socket from
CODEMAN_INSTANCE (default "beta"): ~/.codeman-beta + tmux -L codeman-beta.
Every ~/.codeman path now goes through dataPath()/getDataDir() (state,
mux-sessions, settings, push keys, lifecycle log, screenshots, certs,
linked-cases, subagent window state). Overridable via CODEMAN_INSTANCE /
CODEMAN_DATA_DIR / CODEMAN_TMUX_SOCKET. Prevents a second instance from
discovering and attaching PTYs to the first instance's live tmux sessions.
Verified: tsc / eslint / prettier / lockfile clean; Playwright E2E (27 checks)
for detach/solo/redock; default isolation confirmed to see zero real sessions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drops /compact from both the simple and extended accessory-bar layouts,
the action handler (case folded back to clear-only), the refocus guard,
and the JSDoc. /clear retains its double-tap confirmation. Verified on a
touch-emulated viewport: neither layout renders a compact action.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): response-viewer transcript fallback + code-block rendering
- Add _cleanTerminalBuffer(): strip ANSI escapes and Claude CLI chrome
(status bar, spinner, progress bar, prompt glyphs) from the terminal
buffer so the response viewer renders clean text when the JSONL
transcript is missing.
- Add _preprocessAsciiArt(): wrap box-drawing/block-element diagrams in
fenced code blocks (narrow trigger that excludes arrows/geometric
shapes common in prose) so marked.js preserves their whitespace.
- Extend .rv-text rules to .response-viewer-body so fallback-rendered
content gets the same typography, code-block, and table styling.
* refactor(web): drop duplicate _cleanTerminalBuffer/_preprocessAsciiArt
These two methods already exist on master (added in #75). This branch
re-added byte-identical copies above _sanitizeHtml; in a JS class body the
later definition wins, so the duplicates were inert dead code. Remove them,
keeping only the genuinely new work: the _renderMarkdown null-safety fix and
the response-viewer CSS overhaul.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Teigen <teigen@TeigendeMac-mini.local>
Co-authored-by: arkon <arkon.85@hotmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Extend the keyboard accessory paste dialog with an image picker
(camera / photo library) plus best-effort image paste, routing
selected files through the existing _uploadAndInsertImages pipeline
- Re-encode images to standard JPEG/PNG in the browser before upload,
so mislabeled gallery images (e.g. MIUI WebP claiming image/jpeg)
pass the server magic-byte check; PNG keeps transparency, GIF passes
through untouched, decode failures fall back to the original file
- Log the real byte header on the paste-image magic-mismatch branch to
pin down any remaining format mismatches without a guessing loop
- Ignore the runtime .claude-images/ upload directory
Co-authored-by: Teigen <teigen@TeigendeMac-mini.local>