Commit Graph
973 Commits
Author SHA1 Message Date
Aamer AkhterandSaqeb Akhter 0569f68b86 feat(terminal): tmux pane-buffer primitives and session/render reliability
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>
2026-06-09 15:39:03 -04:00
arkonandClaude Opus 4.8 b84438a0aa fix(security): push-endpoint SSRF guard + tmux name validation; document tail-file roots
- 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>
2026-06-09 20:02:17 +02:00
arkonandClaude Opus 4.8 d95b4c597c feat(self-update): live progress during install/build so it doesn't look hung
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>
2026-06-09 03:36:39 +02:00
arkonandClaude Opus 4.8 c669518ba0 fix(security): block DNS rebinding + cross-site CSRF + subagent-panel XSS
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>
2026-06-09 03:19:51 +02:00
arkonandClaude Opus 4.8 543be8a85b feat: add in-app self-updater (App Settings → Updates)
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>
2026-06-09 01:55:54 +02:00
arkonandClaude Opus 4.8 84b59567b1 fix(sse): sync frontend SSE_EVENTS registry with backend
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>
2026-06-09 00:50:34 +02:00
arkonandClaude Opus 4.8 09a142d14b feat: vendor gesture-control source into packages/gesture-control
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>
2026-06-08 20:48:20 +02:00
arkon 695e4047a1 chore: version packages 2026-06-08 19:59:40 +02:00
arkonandClaude Opus 4.8 f475caab87 fix(settings): stop App Settings modal overflowing horizontally
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>
2026-06-08 19:38:43 +02:00
arkonandClaude Opus 4.8 a8e0e2a343 chore: release 0.9.0 — security hardening + warn-don't-block network policy
Release 0.9.0 covering the merged security/reliability PRs (#106 deps/
supply-chain, #107 auth/network, #108 test stability, #110 tmux cwd) plus:

- Network policy: a non-loopback bind without CODEMAN_PASSWORD now STARTS
  with a loud warning (3 ways to secure) instead of refusing to start.
  Loopback stays the safe default. --allow-unauthenticated-network just
  acknowledges (terser note). (src/web/server.ts start())
- Post-install security note explaining the loopback default + safe exposure.
- New docs/security-architecture.md documenting the full model (binding,
  auth pipeline, tunnel req.ip caveat, file-serving, supply-chain, isolation,
  recommended setups). CLAUDE.md Security section + gotcha updated.
- Updated auth-security test: asserts warn-and-start (not throw).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:29:47 +02:00
Ark0N 4d0586a2aa Merge pull request #110 from aakhter/cod-31-tmux-session-reliability
fix: harden tmux launch cwd
2026-06-08 19:02:37 +02:00
arkon a721af4552 Merge remote-tracking branch 'origin/master' into cod-28-security-public-assets 2026-06-08 18:05:10 +02:00
Ark0N e6b18fd126 Merge pull request #107 from aakhter/cod-29-network-auth-downloads
fix: COD-29 harden network auth and downloads
2026-06-08 18:03:01 +02:00
Aamer Akhter 187ce653ae fix: COD-31 harden tmux launch cwd 2026-06-08 11:18:14 -04:00
Aamer Akhter da00fa6038 fix: COD-29 relax auth lockout recovery 2026-06-08 11:01:34 -04:00
Aamer Akhter a36543c1b9 fix: COD-29 harden downloads and extract auth policy 2026-06-08 11:01:34 -04:00
Aamer Akhter dea015dc91 COD-2 scope downloads to session workspace 2026-06-08 11:01:34 -04:00
Aamer Akhter 333dc047c3 fix: COD-29 fail closed for unauthenticated network binds 2026-06-08 11:01:34 -04:00
arkonandClaude Opus 4.8 a51c17170e feat(settings): relocate Gesture Control into Input section + release 0.8.2
- 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>
2026-06-08 17:01:17 +02:00
arkonandClaude Opus 4.8 20c01d5b11 feat(settings): label Gesture Control as (beta)
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>
2026-06-08 16:40:54 +02:00
Ark0N ce65d5f2ad Merge pull request #105 from Ark0N/beta/settings-toggles
feat(settings): toggle gesture control + multi-monitor button (off by default)
2026-06-08 16:37:17 +02:00
Ark0N cc45191c62 Merge pull request #103 from Ark0N/beta/session-detach
feat(web): session detach/undock + beta instance isolation (port 5000)
2026-06-08 16:36:46 +02:00
Aamer Akhter eb874339dd chore: COD-28 harden dependencies and public assets 2026-06-08 09:56:06 -04:00
arkonandClaude Opus 4.8 29d3fd48c1 fix(web): address self-review findings on #105 (settings cache + brittle reveal)
- 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>
2026-06-08 15:46:10 +02:00
arkonandClaude Opus 4.8 cf6fabc070 fix(web): address self-review findings on #103 (master-safe defaults + hardening)
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>
2026-06-08 15:41:46 +02:00
Ark0NandClaude Opus 4.8 94b26f7606 feat(settings): toggle gesture control + multi-monitor button (off by default)
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>
2026-06-08 06:01:15 +02:00
Ark0NandClaude Opus 4.8 b5ea7112a9 fix(web): cache-bust same-origin module scripts + stylesheets
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>
2026-06-08 05:03:59 +02:00
Ark0NandClaude Opus 4.8 95b00357b6 feat(multimonitor): header button to open Codeman spanned across all displays
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>
2026-06-08 04:53:33 +02:00
Ark0NandClaude Opus 4.8 59145c48fc build(gesture): fetch MediaPipe wasm + model at install/build instead of committing
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>
2026-06-08 02:05:28 +02:00
Ark0NandClaude Opus 4.8 8dc850f845 fix(csp): drop now-unused gesture CDN connect-src entries (self-hosted MediaPipe)
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>
2026-06-08 01:29:26 +02:00
Ark0NandClaude Opus 4.8 eea84db05e 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>
2026-06-08 01:20:23 +02:00
Ark0NandClaude Opus 4.8 ceca85365c style: format auth.ts to satisfy format:check (CI)
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>
2026-06-07 04:45:35 +02:00
Tenggan ZhangandTeigen b2f8b03b3c feat: inject effort as soft default via CLI flags instead of env var (#104)
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>
2026-06-07 04:33:11 +02:00
Ark0NandClaude Opus 4.8 afea6d6a1c feat(web): gesture-control overlay integration (Phase 5, opt-in via CODEMAN_GESTURE=1)
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>
2026-06-07 03:15:29 +02:00
Ark0NandClaude Opus 4.8 2e341e3897 fix(web): harden session detach edge cases (findings 2-4)
- 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>
2026-06-06 04:14:40 +02:00
Ark0NandClaude Opus 4.8 5459da5f9d fix(state-store): scope legacy ~/.claudeman migration to the default instance
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>
2026-06-06 04:06:29 +02:00
arkonandClaude Opus 4.8 b00a680d42 feat(web): session detach/undock + beta instance isolation (port 5000)
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>
2026-06-06 03:53:54 +02:00
arkonandClaude Opus 4.8 eb831487a0 feat(web): remove /compact button from mobile keyboard accessory bar
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>
2026-06-01 20:00:38 +02:00
68594ac395 feat(web): response-viewer transcript fallback + code-block rendering (#102)
* 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>
2026-06-01 19:43:52 +02:00
Tenggan ZhangandTeigen ec38fd11bf feat(web): mobile image upload to active session via paste dialog (#101)
- 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>
2026-06-01 19:38:48 +02:00
Tenggan ZhangandTeigen 06f9ff6d9c fix: avoid event-loop stalls from synchronous tmux/ps calls (#100)
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>
2026-06-01 19:32:24 +02:00
arkonandClaude Opus 4.7 257695ff8e chore: version packages
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 05:17:07 +02:00
arkonandClaude Opus 4.7 2cfccc745f docs: correct sendPendingCtrlL comment (it has no callers)
The prior wording claimed the no-op stub was kept so SSE idle/working
handlers could call it without guards, but there are no callers anywhere.
Reword to reflect that it's a vestigial, intentionally-retained guard
documenting why Ctrl+L must not be auto-sent. Comment-only; minified
build output is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 01:52:15 +02:00
Tenggan ZhangandTeigen 896dc5b177 fix(web): stop auto-sending Ctrl+L from session selection paths (#99)
Claude Code 2.x treats Ctrl+L (\x0c) as a two-step "clear conversation"
command (first press shows the confirmation prompt, second press
clears). The frontend previously fired \x0c from three places to force
Ink to redraw stale CUP-positioned frames in the tailed buffer; if a
page refresh or SSE reconnect ran the same path twice within Claude's
confirmation window the second \x0c silently nuked the user's
conversation.

Removed the \x0c sends from:
- selectSession() — main offender, runs on every tab switch & page reload
- restoreTerminalSize() — manual "restore size" button
- sendPendingCtrlL() — dead code path (pendingCtrlL was never populated)

Trade-off: occasional stale Ink frames immediately after refresh; the
user's first keypress causes Ink to redraw and the artifact vanishes.
Losing the conversation silently is far worse than a brief cosmetic
glitch.

Co-authored-by: Teigen <teigen@TeigendeMac-mini.local>
2026-05-26 00:12:43 +02:00
Tenggan ZhangandTeigen 196646a7ff feat(web): one-click copy button on response-viewer code blocks (#98)
Wrap every fenced code block in the response viewer with a positioned
.rv-code-wrap toolbar (outside the <pre> scroll container so buttons stay put
during horizontal scroll). All blocks get a copy button; ASCII diagrams keep
their existing line-wrap toggle alongside it.

_copyText() prefers the async Clipboard API and falls back to a hidden-textarea
+ execCommand path, so copy works over plain HTTP too. The button shows a 1.5s
✓ / ✕ feedback state after each attempt.

Co-authored-by: Teigen <teigen@TeigendeMac-mini.local>
2026-05-26 00:00:37 +02:00
Tenggan ZhangandTeigen 1b652ceb87 test: repair route harness error rendering + stop AI-checker spawning real processes in tests (#97)
* fix(test): share route error handler with test harness + fix stale assertions

The route test harness built a bare Fastify instance without the production
global error handler (server.ts), so structured errors thrown by route helpers
(findSessionOrFail → 404, parseBody → 400) fell through to Fastify's default
handler — yielding a `{statusCode,error,message}` body instead of the
`{success:false,...}` shape, and the tests asserted the old implicit-200
behavior. 51 route tests across 7 files were red.

- Extract the handler into src/web/route-error-handler.ts; server.ts and the
  test harness now install the identical handler (single source of truth).
- Correct stale assertions across route test files: throw-based error paths
  now assert 404 (unknown session) / 400 (invalid body); genuine in-handler
  `return createErrorResponse(...)` paths (200 + success:false) left untouched.
- Reformat a few test files prettier flagged (pre-existing non-compliance).

Route suite: 307/307 passing (was 256/307). No production behavior change.

* test(respawn): mock child_process so AI checker never spawns real processes

respawn-controller.test.ts drives the AI idle checker (ai-checker-base), whose
runCheck() spawns a real `tmux new-session` running `claude -p`. The AI-enabled
tests only assert the ai_checking state transition (then cancel/stop), so the
spawn produced stray real tmux sessions and claude processes on every run — the
reason `npm test` (full suite) was unsafe to run inside a managed session.

Mock node:child_process here (mirroring ai-idle-checker.test.ts), spreading the
real module so `exec` stays intact for transitively-imported modules
(tmux-manager calls promisify(exec) at load). With this, the full non-mobile
suite runs without spawning any real tmux/claude.

---------

Co-authored-by: Teigen <teigen@TeigendeMac-mini.local>
2026-05-25 23:55:57 +02:00
Tenggan ZhangandTeigen 1ff315a1e6 fix(tmux): isolate sessions on a dedicated socket + raise pane nofile limit (fixes new-session crash after tmux upgrade) (#96)
* fix: isolate codeman tmux sessions

* fix(tmux): unify all sessions onto a single dedicated socket

Remove the per-session `tmuxSocket` field that recorded which tmux server
each session lived on (default vs the `codeman` socket). That field was a
persisted cache of physical reality and could drift — causing live sessions
to be wrongly marked dead ("tab shows no session found") and spawning
duplicate "Restored:" tabs.

All Codeman sessions now live on one process-wide socket (`tmux -L codeman`,
overridable via CODEMAN_TMUX_SOCKET), exposed via TmuxManager.muxSocket on
the TerminalMultiplexer interface. reconcileSessions() collapses from a
multi-socket scan (locate / re-pin / cross-socket dedup) to a single
`list-panes` query. loadSessions() strips the obsolete field from on-disk
records so it stops being written back.

Also fix two sibling bare-`tmux` call sites the unification would otherwise
leave broken (same #80 regression class — bare tmux hits the user's default
server and never finds a session on the codeman socket):
- session.ts queryTmuxWindowSize(): add `-L <socket>` (was silently falling
  back to 120x40 on re-attach, losing scrollback)
- session-routes.ts send-key (Shift+Enter / Ctrl+Enter newline): route
  through ctx.mux.muxSocket

SSH chooser scripts (tmux-manager.sh, tmux-chooser.sh) route every tmux call
through `tmux -L $CODEMAN_TMUX_SOCKET`, matching the TS default.

---------

Co-authored-by: Teigen <teigen@TeigendeMac-mini.local>
2026-05-25 23:28:58 +02:00
Tenggan ZhangandTeigen d27f8e77f7 feat: add View all in folder modal for Resume Conversation (#94)
Drill into a single project's complete history when the homepage's
3-per-project dedup hides older conversations.

- Backend: /api/history/sessions accepts projectKey/offset/limit;
  single-folder mode bypasses the 50-cap and returns { sessions, total }.
  projectKey is validated against ^[A-Za-z0-9_-]+$ to prevent traversal.
- Frontend: detail panel adds "View all in this folder" button that
  opens a modal listing 20 sessions per page with Show more pagination.
- Modal items reuse _buildHistoryItem with showViewAll:false to avoid
  recursive entry points.

Co-authored-by: Teigen <teigen@TeigendeMac-mini.local>
2026-05-19 16:27:42 +02:00
Tenggan ZhangandTeigen e248cd8bcf fix: drop phantom ended-tab stubs, trust server as source of truth (#93)
Previously the client cached open session tabs in localStorage and resurrected
any that the server no longer knew about as grayed-out "ended" stubs. On
multi-device use (close tab on mobile, open desktop) this left stale phantom
tabs the user had to manually dismiss.

Remove _restoreEndedTabs / _saveTabMetadata, the session._ended branch in
selectSession, the data-ended render attribute, and the matching CSS rule.
Clear the legacy localStorage key on init to purge stale entries.

Co-authored-by: Teigen <teigen@TeigendeMac-mini.local>
2026-05-19 16:19:07 +02:00
Tenggan ZhangandTeigen 73d81afd4d fix: decode project keys with longest-match backtracking (#92)
When two sibling directories share a prefix (e.g. `diary/` and
`diary-app/`), the greedy shortest-match decoder picked the shorter
name and then failed to resolve the remainder, so the homepage Resume
Conversation list showed those workingDirs as $HOME and resume targeted
the wrong folder. Switch to recursive backtracking with longest-join-first
at each segment boundary; require every step to be a real directory.
Keep the greedy path as a fallback for deleted dirs.

Co-authored-by: Teigen <teigen@TeigendeMac-mini.local>
2026-05-19 16:16:46 +02:00