install.sh now prints the loopback-bind security notice as the final block of
both the one-line fresh install and the update flow, so it stays visible. Also
documents that gesture control remains opt-in / default-off (changeset).
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>
Reconcile scripts/codeman-web.service with the installed
~/.config/systemd/user/codeman-web.service so they're identical: carry the
loopback + `tailscale serve` security note, keep NODE_COMPILE_CACHE, and a
concise CODEMAN_GESTURE comment. Points at docs/security-architecture.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Document the loopback-default bind and fail-closed non-loopback behavior
(COD-29) as a Common Gotcha, plus expanded Auth + new Network bind rows
in the Security table
- Add --host/CODEMAN_HOST bind and `npm run check:public-assets` to the
Additional Commands table
- Note the CI server boot smoke test step
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The PR adds an extended format:check / check-public-assets prettier pass
over src/web/public, but the hand-written public JS modules (and the
ported gesture bundle) have never been prettier-enforced and would turn
the new check red on master. Rather than reformat the entire frontend
(~2k lines of churn) inside a dependency-hardening PR, add those legacy
files + src/web/public/gesture/ to .prettierignore — matching the
author's existing pattern (app.js, styles.css, mobile.css, index.html).
The security-relevant checks are unaffected: check-public-assets.mjs
still validates NUL bytes and runs `node --check` on EVERY public .js
file regardless of .prettierignore.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The WebServer constructor now takes `host` as the 4th positional arg
(titleHostname shifted to 5th), and renderIndexHtml became async (it
reads settings.json for the gesture bundle) and cache-busts asset URLs.
Update the two title tests accordingly:
- pass '127.0.0.1' as the bind host so the title value lands in the
5th titleHostname slot (server-index-title + push-payload-host-title)
- await renderIndexHtml and make the cases async
- strip ?v=<mtime> cache-bust params before the byte-identical assertion
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>
Document that both header features are now opt-in (default OFF) via App Settings
→ Display (Input / Header Displays), how each is gated (renderIndexHtml reveal
+ async settings read), and that the notification bell stays hidden.
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>
- The 'static cached 1y → hard refresh after deploy' note is now stale:
renderIndexHtml runs cacheBustAssets() so a normal reload picks up edited
modules/styles. Update it.
- Note the multi-monitor header button (replaces notification bell) and its
/api/system/span-displays route in the Frontend + API Routes sections.
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>
The stats collector (~2s) and mouse-mode sync (5s) ran execSync (pgrep/ps/
list-panes, 5s timeout each) per session on the server's single thread,
blocking the event loop. With several sessions or a momentarily slow tmux this
froze port 3000 for seconds-to-tens-of-seconds while the process stayed alive
and other ports were unaffected — self-healing, so it never restarted and the
60s loopback healthcheck missed it. Convert these hot-path calls to execAsync.
Also add an always-on event-loop lag monitor (utils/event-loop-monitor.ts) that
logs stalls >=1s to the web log, so this otherwise-invisible class of incident
leaves a quantified, timestamped trace.
Co-authored-by: Teigen <teigen@TeigendeMac-mini.local>