Commit Graph
1456 Commits
Author SHA1 Message Date
Codeman maintainer f89bc420ba Merge PR #147 from aakhter/cod-168-pty-exit-breaker: scrub TMUX vars + PTY-exit circuit breaker (COD-115/COD-118)
Includes review fixes: breaker reset only on explicit clearBreaker restarts (auto-reattach never clears), trip observability survives listener detach, push notification wired into PUSH_EVENT_MAP.

# Conflicts:
#	src/session.ts
2026-07-12 20:07:19 +02:00
Codeman maintainer 5a4e60dc8e fix(merge): reconcile cross-PR test seams after #141/#145/#146 merges
- help-modal extractor bounds at the next HTML comment (cron modal's 'Run At'
  text false-positived the stale-shortcut regex)
- remote-shell run test expects the wired /api/quick-start path (#145) — POST
  /api/sessions has no caseName in its schema

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 20:06:32 +02:00
Codeman maintainer 460972a50e Merge PR #146 from aakhter/cod-163-command-palette: searchable case picker + Command-K session palette
Includes review fixes: Session Manager aligned to the merged /api/sessions/unified contract with error states, Ctrl+K no longer leaks 0x0B into the PTY, shortcut registry finished (dispatch/persistence/rendering), shortcutOverrides preserved across settings saves, help modal kept reachable.

# Conflicts:
#	README.md
#	src/web/public/index.html
#	src/web/public/session-ui.js
2026-07-12 20:03:53 +02:00
Codeman maintainer 8a971c3935 Merge PR #145 from aakhter/cod-94-remote-host-ssh: remote host SSH cases
Includes review fixes: reachable Remote tab UI, remote metadata restore on recovery, quick-start routing for remote run flows, ssh-arg injection guards, dedicated remote socket/name (no cross-instance adoption), remote tmux kill on delete, wired tmux probe + ConnectTimeout, --dangerously-skip-permissions default.
2026-07-12 20:01:47 +02:00
Codeman maintainer 83779cab4d Merge PR #141 from chatgptkrylor/feat/scheduler: recurring cron-style scheduled jobs
Includes review fixes: multi-line prompt rejection, prompt-file confinement hardening (realpath + attachment-guard blocklist), per-job autoClosePreviousSession lifecycle, live-session-only concurrency counting, wired launchCommand.
2026-07-12 20:01:16 +02:00
Codeman maintainer a8e7669f5a fix(review): preserve shortcutOverrides across settings saves + keep help modal reachable (PR #146)
- saveAppSettings() rebuilds settings from the DOM; carry over shortcutOverrides
  like showTokenCount/showCost so rebinding survives unrelated saves
- shortcut overlay footer links to the full help modal (its only opener was the
  legacy Ctrl+? route this PR replaced)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:58:32 +02:00
Codeman maintainer 5deb0d4a4c fix(review): harden + wire remote-host SSH cases end-to-end (PR #145)
- UI: add the missing data-tab="case-remote" tab button; dispatch it through
  submitCaseModal()/switchCaseModalTab() to linkRemoteCase() (was dead code).
- Restore: restoreMuxSessions() now passes remote (muxSession.remote ??
  savedState.remote) into the Session constructor, so remote metadata round-trips
  on restart instead of reattaching from a local cwd / respawning LOCAL / being
  erased from state.json. Recovery tests added.
- Run flows: runClaude()/runShell() route remote cases through /api/quick-start
  (POST /api/sessions stat-validates workingDir locally); run*() skip the
  /api/*/status pre-check and omit inert config/env for remote cases.
- Quick-start: resolve the remote case BEFORE the local CLI availability gates and
  skip isCodex/Gemini/OpenCodeAvailable() when remote; REJECT
  envOverrides/effort/codex/gemini/openCode config for remote (they don't cross
  ssh) instead of silently dropping them.
- Injection: reject $, backtick, $( in remotePath + identityFile at the schema
  layer (they survive shellescape into the bash -c launch double-quote layer).
  Regression tests for $(...) and backtick payloads added.
- Remote socket/name: launch on a DEDICATED -L codeman-remote socket under a
  codeman-ssh-<id> name that fails a remote Codeman's SAFE_MUX_NAME_PATTERN, so a
  remote instance can't adopt the session; scope tmux set-options per-session
  (never -g) so they don't mutate other sessions.
- Kill: best-effort ssh 'tmux -L codeman-remote kill-session' on remote session
  kill (fire-and-forget, never blocks/throws the local kill) so the remote agent
  isn't orphaned forever.
- Probe: wire checkRemoteTmuxAvailable() into POST /api/quick-start (structured
  OPERATION_FAILED) and as courtesy validation in remote-link; add a default
  -o ConnectTimeout=10 to buildSshConnectionArgs (overridable via extraSshOptions).
- Command default: remote claude default is now
  'exec claude --dangerously-skip-permissions' (per-host override stays the escape
  hatch), mirroring local non-interactive semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:49:58 +02:00
Codeman maintainer 84ab4ff07b fix(review): harden cron security, session lifecycle, skip policy (PR #141)
- Reject multi-line prompts end-to-end: schema refines on promptText/
  launchCommand, runtime check in resolvePrompt (prompt-file content;
  trailing newlines tolerated), matching cron-ui form validation — delivery
  is single-line only, so multi-line was silently corrupted (typed mode
  fused lines, paste mode submitted partials)
- Close the workingDir confinement bypass (arbitrary server-side file read,
  e.g. workingDir=/proc + /proc/self/environ): realpath-resolve workingDir
  before the containment check, reject '/' and blocked/pseudo-fs trees
  (/proc, /sys, /dev + the attachment-guard blocklist) at fire time AND at
  job create/update (workingDir must exist and be a directory)
- Session lifecycle: new per-job autoClosePreviousSession (default true,
  recurring schedules only; ignored for 'once') — the previous run's
  still-open session is closed via the normal cleanupSession path when the
  next run fires; UI switch added; 50-session cap math documented in
  docs/cron-guide.md §8
- skip_if_same_agent_running: count only live sessions (exclude
  stopped/error dead tabs), exclude sessions created by this job's own runs
  (fixes the fire-once-then-skip-forever self-deadlock), and a skipped
  'once' job stays armed and retries next tick instead of being consumed;
  liveness filter mirrored in cron-ui _countActiveAgents
- Wire launchCommand (was accepted+documented but dead): shell mode sends
  it via writeViaMux as the first input line after startShell readiness
  (single-line, schema-enforced); form field shown for shell agent type
- Record delivery failures: a false writeViaMux result now fails the run
  instead of recording a false 'prompt_sent'
- Cap saved jobs at MAX_CRON_JOBS (100) to bound state.json growth
- Surface field-specific schema messages (drop parseBody custom
  errorMessage on cron create/update)
- Tests: workingDir create/update validation, /proc bypass regression,
  single-line enforcement (schema+runtime+trailing-newline tolerance),
  live/own-session skip filtering, once-skip re-arm, auto-close on/off/once,
  job-count cap

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:25:42 +02:00
Codeman maintainer 88f47754ad fix(review): wire Session Manager to /api/sessions/unified contract, stop Ctrl+K PTY leak, finish shortcut registry (PR #146)
- Session Manager (COD-121/192): align _loadSessionManagerList() with the
  merged #139 endpoint — map UnifiedSessionItem fields (lastActivityAt
  epoch-ms → lastModified, optional sizeBytes/firstPrompt/name) to the
  history-record shape _buildHistoryItem renders; surface non-2xx /
  error-envelope responses as a visible message instead of a silent
  "No sessions found"; route clicks by liveness (live row → selectSession,
  history row → resumeHistorySession by conversation UUID) via a new
  onActivate option so a live session is never duplicate-resumed
- Ctrl+K double-dispatch: gate the palette chord in
  attachCustomKeyEventHandler (return false on keydown) so xterm never
  writes 0x0b kill-line into the PTY while the palette opens; gate is
  registry-aware so a rebound/disabled palette shortcut restores normal
  terminal Ctrl+K
- Shortcut registry (COD-157) finished per maintainer decision: document
  keydown now dispatches through getShortcutRegistry() +
  matchesShortcutEvent() (legacy SHORTCUTS table removed), honoring
  per-shortcut disable and rebinds incl. the palette chord; overrides
  persist via saveAppSettingsToStorage() (correct device key + cache
  coherence, was orphaned 'codeman:settings'); Shortcuts tab renders on
  open via switchSettingsTab hook; capture uses a persistent listener that
  ignores bare modifier keydowns (combos now capturable) and requires a
  Ctrl/Cmd/Alt chord; settings rows use delegated listeners instead of
  inline onclick (JS-string injection sink) and overrides can no longer
  clobber id/label/action; added the missing row + overlay CSS
- matchesShortcutEvent: reject undeclared extra modifiers (Ctrl+Shift+K
  no longer hijacked from Firefox devtools) while keeping Ctrl/Cmd
  interchangeable; match physical code OR produced key for layout parity
- Registry/dispatch gaps: added restore-terminal-size entry, documented
  Ctrl+Shift+R again in the help modal (test flipped to assert presence),
  Ctrl+?/Alt+? now really open the registry-driven shortcut overlay, and
  Escape closes it
- Palette new-session pick routes through selectQuickStartCase() so the
  searchable combobox, dir display, and lastUsedCase stay in sync
- Removed fork cherry-pick debris: dead _onSessionListMaybeChanged(),
  orphaned .session-row-menu CSS, nonexistent closeMobileHeaderUtilities
  calls
- Tests: functional vm-harness coverage for the unified-list field
  mapping + error state + liveness routing, palette chord shift/disable/
  rebind handling, override persistence round-trip, capture flow, tab
  render hook, and source guards for the PTY gate + registry dispatch

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:06:12 +02:00
Codeman maintainer 6e417d69dc fix(review): WS state machine, per-tab supersede key, backoff, dot CSS (PR #149)
- _wsState now transitions through the full lifecycle: _connectWs() sets
  'connecting', ws.onopen (inside the this._ws === ws guard) sets 'connected',
  _disconnectWs() resets to 'disconnected' — the connection chip's "WS" state
  was previously unreachable (stuck on "WS…"/"HTTP" forever).
- WS registry supersede is now keyed per TAB: the upgrade URL sends
  cid = clientId + ':' + per-page nonce (reusing the constructor's page UUID),
  while input frames keep the bare browser clientId for seq dedup — two
  tabs/windows on one session coexist instead of 4010-evicting each other in a
  perpetual 5s ping-pong; a genuine same-tab reconnect still supersedes.
- Exponential backoff engages: _disconnectWs() no longer zeroes
  _wsReconnectAttempts (it's called at the top of _connectWs, so every retry
  replanned at attempt 0 → ~0ms tight reconnect loop during outages); onopen
  resets the counter on success.
- styles.css: add .connection-dot.connected (green) and .connection-dot.fallback
  (yellow) — both states rendered an invisible dot (no rule existed).
- Remove smuggled dead code: resolveMonitorRowLabels/CodemanMonitorLabels
  (COD-122, no consumer, referenced test doesn't exist) and the never-written
  _wsLastClose/_wsInputSendCount/_httpFallbackSendCount diagnostics.
- Tests: new test/ws-state-lifecycle.test.ts drives the REAL
  _connectWs/onopen/onclose/timer cycle (state transitions, escalating backoff
  delays, composite cid on the upgrade URL); registry two-tab coexistence test;
  static check that every emitted connection-dot class has a styles.css rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 18:42:36 +02:00
Codeman maintainer f98d29b323 fix(review): wire full-scrollback replay to an explicit ?full=1, dedup + bound the capture (PR #148)
- Replace the 'missing ?tail means reload' overload with an explicit ?full=1
  query param: the frontend's first buffer load after a page load (selectSession)
  now requests full=1, tab switches keep ?tail=, and the legacy no-param callers
  (response-viewer fallback, clearTerminal refresh) keep the cheap visible-frame
  path — the COD-47 feature was previously unreachable from a real reload.
- When the full-history capture succeeds, return it ALONE instead of prepending
  the byte buffer + \x1b[H\x1b[2J: the capture is the rendered superset of the
  byte history, and ED2 clears only the viewport so the concat replayed the whole
  conversation twice in xterm scrollback. The history+clear+frame concat stays
  for the visible-frame/tab-switch path.
- Pass an explicit execSync maxBuffer for the full-history capture (configured
  terminalBufferMaxBytes + slack) — the 1MB Node default ENOBUFS-killed exactly
  the multi-MB captures the feature exists for; log ENOBUFS concisely instead of
  dumping the truncated stdout.
- Bound the capture itself via -S -<N> derived from the configured tmux
  history limit (was unbounded -S -), and add -J so lines hard-wrapped at the
  capture-time pane width reflow in the browser xterm.
- Cap the concatenated buffer to terminalBufferMaxBytes EARLY (before the
  regex normalization passes) so multi-MB captures don't stall the event loop
  normalizing bytes that get sliced away.
- Tests: route tests updated for ?full=1 semantics (capture-alone response,
  config-forwarded capture bounds, byte-history fallback, no-param requests
  stay on the visible-frame path); source-scan tests cover the bounded -J -S -<N>
  flags and explicit maxBuffer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 18:33:18 +02:00
Codeman maintainer 360d58ca4f fix(review): breaker reset semantics, trip observability, push template (PR #147)
- Breaker reset is now explicit-only: POST /api/sessions/:id/interactive no
  longer unconditionally resets the PTY-exit breaker (that endpoint IS the
  frontend's automatic re-attach path, so the breaker could never trip on the
  COD-115 crash loop and any tab click silently re-armed it). The route accepts
  a schema-validated optional body flag {clearBreaker:true}
  (InteractiveStartSchema) and resets only when it is sent.
- Frontend restart control: app.js selectSession keeps the bare auto-attach
  (no body, never clears); when the selected session has respawnBlocked it asks
  for explicit user confirmation and only then re-POSTs with clearBreaker:true.
  respawnBlocked is surfaced via SessionState/toState() (runtime-only, not
  restored on boot so recovery can re-attach).
- Trip observability: WebServer.setupSessionListeners() is now idempotent
  (skips while refs are attached) and the re-attach routes (/interactive,
  /interactive-respawn, /shell) re-run it, restoring the wiring that the exit
  handler detaches on every PTY exit — without this the 5th-exit trip had
  guaranteed zero listeners (no SSE, no push, no persist, no run-summary).
- Push notification: added SessionRespawnBreakerTripped to PUSH_EVENT_MAP
  ('Session crash loop stopped', urgency critical) with an exit-count body
  branch; previously sendPushNotifications silently no-oped.
- Minor: buildMuxAttachEnv() truecolor param is now actually passed
  (codex/gemini, mirrors buildEnvExports); buildClaudeEnv() uses delete for
  COLORTERM/CLAUDECODE (same node-pty "KEY=undefined" quirk as COD-115).
- Tests: route tests assert auto-reattach does NOT reset, clearBreaker resets,
  invalid flag rejected, and listener re-wiring on /interactive + /shell;
  real-wiring lifecycle tests (createSessionListeners/attach/detach) prove the
  exit-detach gap and that re-setup keeps the 5th-exit trip observable;
  PUSH_EVENT_MAP regression guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 18:21:42 +02:00
Aamer Akhter 6cac517fa6 COD-130 session-row ⋯ becomes a context menu
The per-row ⋯ in the session list was a details toggle that did nothing in
the Session Manager modal (swallowed by the modal's capture-phase
close-on-click). Replace it with a real kebab context menu.

- terminal-ui.js: ⋯ now opens _openSessionRowMenu() — a body-anchored popup
  (fixed-positioned, flips/clamps to viewport, z-index above the modal) with:
  Resume/Switch-to (live→select tab, closed→resume), Open folder in the file
  browser (live sessions only — the browser is session-scoped), Copy path
  (_copyText + toast, when workingDir present), and Show details (the old
  inline prompt/path panel). Closes on outside-click / Escape / scroll / resize.
- panels-ui.js: _loadSessionManagerList scopes its modal-close to the
  .history-item-main (resume) click, so the ⋯/menu no longer closes the modal.
- styles.css: .session-row-menu + .session-row-menu-item.

Verified in Chromium on an isolated beta: ⋯ opens the menu with the modal
still open; closed rows show Resume/Copy path/Show details, live rows add
Switch-to + Open folder; Show details expands inline (modal stays open),
Copy path copies the path, Resume closes the modal, Escape closes only the
menu. Gates: tsc 0, lint 0, frontend-syntax + public-asset format clean.
2026-07-12 12:19:10 -04:00
Aamer Akhter 2235f06ea5 COD-121 unified session list: live SSE refresh (slice A, unit 4)
The complete session list now updates live as sessions change, instead of
only on open/welcome-load.

- app.js: extra SSE listeners (session:created/updated/deleted) on the same
  EventSource (multiple listeners per event; existing handlers untouched;
  registered via addListener so they tear down on reconnect) call
  _onSessionListMaybeChanged().
- panels-ui.js: _onSessionListMaybeChanged() debounced-refreshes the Session
  Manager modal when it's open and the welcome list when its overlay is
  visible (no work when neither is showing). _loadSessionManagerList stores the
  active query so refreshes preserve the user's search.

Verified on an isolated beta instance (Playwright): dispatching a session
event refreshes the modal while open, does NOT while closed (gated), and
refreshes the welcome list while visible. Gates: tsc 0, frontend-syntax +
public-asset format clean, build clean.
2026-07-12 12:19:10 -04:00
Aamer Akhter 65b609b6db COD-121 unified session list: persistent Session Manager modal (slice A, unit 3)
Adds a header-reachable Session Manager so the complete session list is
available mid-session, not only on the welcome screen.

- index.html: always-on header button (.btn-session-manager) + #sessionManagerModal
  (mirrors the Away Digest modal) with a search box + results list.
- panels-ui.js: openSessionManager()/closeSessionManager()/_loadSessionManagerList()
  — loads GET /api/sessions/unified (limit 200), renders via the unit-2
  _buildHistoryItem (rich items, mode/LIVE badges, open->select / closed->resume),
  debounced search wired to the endpoint's q= param, empty/error states. A
  modal-scoped Escape listener closes it even when focus is in the search input;
  backdrop click and item click also close it.
- app.js: closeSessionManager() added to the global Escape chain.
- styles.css: modal + list styling (items reuse .history-item).

Verified on an isolated beta instance (Playwright): the header button opens the
modal, it lists 200 sessions from /api/sessions/unified, a no-match query issues
?q= to the server and yields 0 items, clearing restores the list, clicking an
item closes the modal and routes resume/select, and Escape closes it. Gates:
tsc 0, lint 0, frontend-syntax + public-asset format clean, 17 tests pass.
2026-07-12 12:19:10 -04:00
Aamer Akhter c9f37f2628 COD-121 unified session list: welcome list frontend (slice A, unit 2)
Backs the welcome-screen "Resume Conversation" list with the new
GET /api/sessions/unified endpoint instead of /api/history/sessions, so it
shows the COMPLETE set (live + persisted + non-Claude + closed history)
newest-first with richer context, rather than only Claude transcripts.

- terminal-ui.js: new _fetchUnifiedSessions(); loadHistorySessions() now uses
  it. _buildHistoryItem upgraded to the unified shape (kept backward-compatible
  with the folder-modal's old shape): title = name || firstPrompt || dir; a
  mode badge + a LIVE badge (sources includes 'live'); timestamp from
  lastActivityAt (falls back to lastModified); size only when present; detail
  panel + "View all in this folder" preserved (gated on projectKey). Resume
  branches: an open live session selects its tab, a closed one resumes.
- unified-session-service.ts + endpoint: pass projectKey through the history
  source so the folder drill-down survives.
- styles.css: .history-item-badges / -badge / -badge-live pills.

Verified: tsc 0, lint 0, frontend-syntax + public-asset format clean, service
tests 13/13 (+projectKey), route tests 4/4. Playwright on an isolated beta:
the welcome list renders real items from /api/sessions/unified, and the
renderer produces the tab-name title + codex mode badge + visible LIVE badge,
omits LIVE on closed items, keeps "View all in folder", and routes resume
correctly (open->select tab, closed->resume). Persistent panel + live SSE
status are later units.
2026-07-12 12:19:10 -04:00
Codeman maintainer 309959be27 fix(review): worker-thread HEIC conversion with bomb guard, concurrency cap, and magic-byte routing (PR #151)
- Event-loop blockage: HEIC decode/encode (CPU-synchronous libheif WASM +
  jpeg-js) now runs in a per-conversion worker_threads Worker
  (src/web/heic-jpeg-worker.ts, spawned by heic-jpeg-converter.ts) with
  resourceLimits and a 30s hard timeout that terminates the worker —
  verified end-to-end under tsx and against compiled dist/ output with a
  real iPhone HEIC (event-loop max stall 52ms during conversion).
- No server-side concurrency cap: conversions now acquire a slot from the
  existing global runWithConversionLimit() pool (document-conversion-limiter),
  bounding peak decode memory/CPU across simultaneous uploads.
- Decompression bomb: header-declared dimensions are read via heic-decode's
  allocation-free `.all` path and rejected above 64MP BEFORE decode() can
  allocate width*height*4 bytes (a <300-byte crafted file can declare
  30000x30000 = 3.6GB). Regression-tested with a crafted ISOBMFF fixture
  against the real heic-decode WASM (test/heic-jpeg-core.test.ts).
- Mislabeled HEIC (documented Android/MIUI case): conversion now routes on
  ftyp magic-byte sniff of the raw buffer regardless of declared
  ext/Content-Type, so a HEIF uploaded as image/jpeg converts instead of
  415ing; the magic-mismatch 415 only fires for genuinely unrecognized bytes.
- Brand allowlist narrowed to what heic-decode's isHeic() accepts
  (heim/heis/hevm/hevs dropped — they could only ever fail conversion).
- Converted-output size: the JPEG result is checked against
  MAX_PASTE_IMAGE_BYTES (jpeg-js can inflate a within-limit HEIC past the cap).
- Deps: heic-convert replaced with its underlying heic-decode + jpeg-js
  (the wrapper could not expose the pre-decode dimension check); lockfile
  synced, drops pngjs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 18:06:59 +02:00
Codeman maintainer 13c877f938 fix(review): harden Codex generated-artifact attachment pipeline (PR #150)
- Pass the attachment request `source` through the server deps lambda and make
  it a required param on SessionListenerDeps.registerAttachment + the wiring
  event type (the 2-arg lambda silently dropped `source`, force-confining every
  codex-generated artifact — the feature never worked outside the workspace);
  new test/session-listener-wiring.test.ts asserts the pass-through
- Gate the Codex `Saved to: file://` scanner on mode === 'codex' via a
  codexArtifacts option threaded from the session call site; magic links stay
  mode-agnostic; tests assert claude/shell sessions never emit codex-generated
  requests
- Decide the generated-artifact trust policy on the realpath-RESOLVED path
  (unresolvable → force-confined) and anchor the ~/.codex marker dirs to
  os.homedir() prefixes with startsWith instead of substring matching; symlink
  escape + unanchored-marker regression tests added
- Run the Codex scanner on stripAnsi'd data so trailing SGR sequences don't
  ride into the captured URL; styled 'Saved to:' test added
- Extend generateFirstPageThumbnail with jpg/jpeg/gif/webp passthrough and
  per-extension content types (mirrors the png passthrough) so the PR's new
  image formats render real thumbnails instead of 204 letter-tiles

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 17:50:21 +02:00
Codeman maintainer 895edfedb0 fix(review): add Codex last-response test coverage + minor hardening (PR #152)
- Add test/routes/session-routes-codex-last-response.test.ts (app.inject +
  temp CODEX_HOME fixture rollouts): originator match beats cwd fallback when
  two panes share a dir, cwd fallback excludes sibling-claimed/foreign-cwd
  rollouts, resume-uuid filename match, history.jsonl pin outranks originator,
  event_msg/legacy user-turn dedup keeps old-codex turns, injected-context
  filtering, image placeholder, envelope shape ({success:true,data:{text,
  timestamp[,messages]}}), and a Claude-mode regression guard (codex reader
  never consulted for claude sessions)
- Replace clear-at-cap Map caches (codexHistoryPinCache, codexRolloutMetaCache)
  with the repo-standard LRUMap so a full cache wipe can't thrash hot entries
  on large rollout collections
- Join multi-block assistant/user text with a blank line instead of no
  separator (extractCodexBlockText)
- Re-enable the terminal-buffer eye fallback for shell sessions (they have no
  transcript source at all); TUI modes keep the clear placeholder

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 17:42:39 +02:00
Ark0N 3f23621f8d Merge pull request #144 from TeigenZhang/pr/mouse-restore-decset-strip
feat(web): restore tap/click/wheel mouse interaction when server strips mouse DECSETs
2026-07-12 13:23:22 +02:00
Ark0N 5cca965aa4 Merge pull request #143 from TeigenZhang/pr/cjk-input-loss
fix(mobile): CJK input loss — IME state machine, focus routing, and Android InputConnection recovery
2026-07-12 13:23:19 +02:00
Ark0N b74a904b41 Merge pull request #142 from TeigenZhang/fix/mobile-response-viewer-typography
fix(mobile): improve response-viewer readability on phones
2026-07-12 13:23:17 +02:00
Codeman maintainer 05d366e405 Merge PR #140 from crawlsys/feat/webgl-renderer-toggle: WebGL renderer toggle in settings
Includes review fixes (per-device setting + sticky-marker semantics); merged locally because the org-owned fork rejects maintainer pushes.
2026-07-12 13:22:53 +02:00
Ark0N 9204e42812 Merge pull request #139 from aakhter/cod-160-unified-session-service
Unified session list: backend service + endpoint
2026-07-12 13:22:39 +02:00
Ark0N a9749ead6a Merge pull request #138 from aakhter/cod-80-raise-terminal-defaults
Raise terminal history/scrollback/buffer defaults (50k→100k, 2MB→32MB)
2026-07-12 13:22:37 +02:00
Codeman maintainer e510ab74ca fix(review): use dvh fallback pair so the response-viewer header stays on-screen on iOS (PR #142)
- 92vh on iOS Safari measures the large viewport; with browser chrome visible the
  panel top (header + close button) clipped off-screen. 88vh fallback + 92dvh
  matches the repo's established dvh idiom.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 13:20:16 +02:00
Codeman maintainer 7fa52cdcd6 fix(review): make WebGL toggle per-device and fix sticky-marker semantics (PR #140)
- saveAppSettings no longer sends webglRendererEnabled on the settings PUT:
  the key is absent from the .strict() SettingsUpdateSchema, so every save
  400'd with INVALID_INPUT, silently killing all server-side settings
  persistence. Stripped in the per-device destructure alongside
  localEchoEnabled/skin/etc.
- shouldSkipWebGL now treats a stored true like the untouched default w.r.t.
  the sticky marker: the checkbox defaults checked on desktop, so any
  unrelated save stored true and every page load then cleared the
  'codeman-webgl-disabled' marker, permanently defeating the GPU-stall
  auto-fallback. Only ?webgl=force clears the marker at init.
- The marker is instead retired on a real OFF->ON toggle flip detected at
  save time (mirrors the _prevGestureEnabled pattern in settings-ui.js).
- webglRendererEnabled added to the displayKeys per-device set in
  loadAppSettingsFromServer (renderer choice is device/GPU-specific; syncing
  would leak mobile's hidden-checkbox false onto desktop).
- Tests: stored true + sticky marker -> still skips WebGL; OFF->ON save
  clears the marker and keeps the key off the wire; default-checked save
  leaves the marker alone; ?webgl=force / ?nowebgl behavior unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 12:42:36 +02:00
Codeman maintainer 5ea424565d fix(review): content-free IME traces, guarded onData self-heal, Android-only retap recovery (PR #143)
- BLOCKER (privacy): the CJK diagnostic trace logged typed CONTENT — _esc(e.key)
  per keystroke, up to 24 chars of textarea value on focus/blur/compstart/
  compend/input, and the flushed text — mirrored into _crashDiag, which
  persists to localStorage and beacons to POST /api/crash-diag. Traces are now
  content-free: key CLASS via _kdesc (any single code point → 'printable',
  named keys pass through), value lengths + phantom presence via _vdesc
  (len=N[+ph]), and 'flush send len=N'. _esc removed.
- MAJOR: the onData self-heal refocused the CJK field whenever gated data
  arrived with focus elsewhere — but onData also fires for xterm's
  SELF-GENERATED query replies (DA/DSR/CPR/OSC during Ink redraws), so it
  stole focus from rename/search/settings inputs while output streamed. Now
  requires document.activeElement === this.terminal.textarea (genuine typed
  input) and bails when shouldSuppressTerminalQueryResponse(data) matches.
- MAJOR: the pointerdown blur→setTimeout(focus,0) wedged-IME recovery ran on
  ALL platforms; on iOS tapping the focused empty field is normal and the
  async refocus is outside the user-gesture stack. The listener is now only
  registered when /Android/i.test(navigator.userAgent).
- tests: trace-privacy test (no typed character or textarea value ever appears
  in the trace; lengths/key classes still recorded), iOS harness asserts the
  pointerdown recovery never cycles, self-heal source guard asserts both new
  conditions; vm harness gained a ua option (navigator injected, Android UA
  default so the existing wedged-IME test still exercises the recovery).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 12:42:14 +02:00
Codeman maintainer 0ad673794f fix(review): dedupe resumed-session rows + newest-wins lifecycle name/mode (PR #139)
- Duplicate rows: transcript-history rows are keyed by the Claude
  conversation UUID (.jsonl filename stem), which diverges from the Codeman
  session id for resumed (claudeSessionId = resumeSessionId != id) and
  /clear-respawned sessions, so one conversation surfaced as both a live row
  and a history-only row. mergeUnifiedSessions now builds an alias map
  (claudeSessionId -> Codeman id) from the live + persisted views and
  resolves history/lifecycle keys through it; the route feeds
  SessionState.resumeSessionId as the persisted alias.
- Inverted precedence: SessionLifecycleLog.query() returns entries
  NEWEST-first, but the merge loop unconditionally overwrote name/mode so
  the OLDEST entry in the window won (stale rename/mode). First-seen now
  wins, mirroring the existing lastActivityAt guard.
- Tests: resumed session yields ONE row (service unit + route end-to-end
  with a real transcript fixture); renamed-then-deleted session surfaces
  the NEWEST name/mode. All 4 new tests fail against the pre-fix code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 12:41:48 +02:00
Codeman maintainer 4d3080aacc fix(review): gate wheel forwarding by CLI mode/version, stop link click double-fire (PR #144)
- _shouldForwardWheelToApp: claude sessions forward wheel to the TUI only
  when the banner-parsed cliVersion is known AND >= 2.1.187 (older/unknown
  Claude Code captures wheel as select-menu navigation → keep local
  scrollLines); new dependency-free _cliVersionAtLeast semver-ish compare
- gemini excluded from wheel forwarding entirely (TUI wheel behavior
  unverified); codex keeps forwarding (verified); taps/clicks still
  forwarded for all strip modes
- link double-fire: registerFilePathLinkProvider links now track hover
  state via ILink hover/leave callbacks (_linkHovered) and
  _handleDesktopTerminalClick bails while a link is hovered, so a link
  click no longer also sends a synthetic SGR press/release to the TUI
- help modal: document Shift+Wheel (scroll local history when mouse
  passthrough is active)
- tests: version gate (2.1.186/unknown/garbage no forward, 2.1.187+
  forwards), codex/gemini split, link-hover click suppression

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 12:40:25 +02:00
Codeman maintainer 246f7b532d fix(review): clamp env-path trim below max; revert unwired scrollback raise (PR #138)
- UNBOUNDED-MEMORY: DEFAULT_TERMINAL_BUFFER_TRIM_BYTES from CODEMAN_TRIM_TERMINAL_TO
  had no relation to DEFAULT_TERMINAL_BUFFER_MAX_BYTES — setting only
  CODEMAN_MAX_TERMINAL_BUFFER=2097152 left the 24MB trim default in force, making
  BufferAccumulator.trim() (slice(-trimSize)) a no-op: unbounded growth past the cap
  plus a full string re-join on every append (O(n²)). Trim default is now clamped to
  75% of the resolved max (the 24MB/32MB default ratio, preserved as hysteresis);
  regression test re-evaluates the module under the env via vi.resetModules.
- OVERCLAIM: reverted DEFAULT_TERMINAL_SCROLLBACK_LINES 100k -> 50k — it has zero
  consumers; browser xterm scrollback is the separate hardcoded DEFAULT_SCROLLBACK
  (50k) in constants.js and deliberately stays 50k (mobile-memory hazard). The tmux
  history-limit raise (50k -> 100k) and PTY 32MB/24MB raise remain (those are wired).
  Module docstring now claims only what is wired; fixed the stale tmux-manager.ts
  comment saying the tmux limit "matches the xterm-side default in constants.js".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 12:39:46 +02:00
codeman-localandClaude Fable 5 116db81002 feat: response-viewer support for Codex sessions
The response-viewer (eye) currently reads only ~/.claude/projects — for
Codex panes it falls back to a raw terminal-buffer dump. This adds a
Codex-aware reader with exact per-pane rollout attribution.

Locating THIS pane's rollout (~/.codex/sessions/**), in confidence order:

1. history match — Session tracks the pane's last Enter
   (codexLastSubmitAt); correlating it against ~/.codex/history.jsonl
   {session_id, ts} entries identifies the thread the pane is ACTUALLY
   on, surviving /resume, /new and /fork typed inside the codex TUI.
   An entry is credited to the pane whose Enter is closest, so menu
   keystrokes in other panes can't steal attribution.
2. originator match — codex panes are spawned with
   CODEX_INTERNAL_ORIGINATOR_OVERRIDE=codeman_<sessionId>, which codex
   (verified on 0.144.1) writes into session_meta.originator of every
   rollout it creates.
3. resume-id match — resumed rollouts keep their original session_meta
   (codex appends without rewriting), but the uuid is in the filename.
4. cwd+mtime heuristic — case-blind compare (codex records launch-time
   path case) and rollouts claimed by other panes are excluded.

Reader details: user turns come from event_msg/user_message (real input
only — AGENTS.md / environment_context injections never appear there),
deduped against legacy response_item rows per-text so mixed-version
rollouts keep full history; image inputs render an [image xN]
placeholder; session_meta identity is cached per path (write-once).

Frontend: thread role label follows session mode (Codex/Gemini/
OpenCode); the terminal-buffer fallback is Claude-only — TUI modes show
a clear placeholder instead of a repaint dump.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 10:39:42 +08:00
Aamer AkhterandSaqeb Akhter bb1d16e230 feat(image): convert HEIC paste uploads to JPEG
When a browser pastes an HEIC file without normalising it first, the
paste-image route now converts it to JPEG server-side via heic-convert
before writing to .claude-images/. Magic-byte validation confirms the
output is valid JPEG. Adds type declarations for the heic-convert package.

Co-authored-by: Saqeb Akhter <saqeb.akhter@gmail.com>
2026-07-11 16:32:49 -04:00
Saqeb Akhter 978ca57343 fix: COD-152 preserve generated artifact filenames 2026-07-10 20:58:47 -04:00
Saqeb Akhter f8aa93969b fix: COD-152 surface Codex generated artifacts 2026-07-10 20:54:41 -04:00
Aamer Akhter 584910f645 COD-144 flush queued SSE output on empty buffer-load so new shells paint immediately
A freshly created shell session rendered blank until a tab-switch. selectSession()
fetches the terminal buffer, but for a just-started shell that fetch resolves before
the PTY emits its prompt, so the buffer is empty; the prompt then arrives as a live
SSE event queued during the load and _finishBufferLoad() discarded it. The discard is
correct for an established session (its fetched buffer already contains that output),
but harmful when the load painted nothing.

_finishBufferLoad(owner, { flushQueued }) now REPLAYS the queued events through
batchTerminalWrite (after _isLoadingBuffer is cleared, so they write through, not
re-queue) instead of discarding. selectSession passes flushQueued only in the empty
branch (no fresh buffer + no cache), so the established-session de-dup path is
unchanged. TDD: test/terminal-buffer-flush.test.ts exercises the real begin/finish
mixin (vm-harness, no jsdom).
2026-07-10 14:39:19 -04:00
Aamer Akhter b86b132af5 COD-136 skip redundant connection-indicator DOM writes on hot input path
_updateConnectionIndicator() ran on every keystroke (_reliableSend) and
every ACK (_ackDelivery), unconditionally writing display/className/
textContent/title. During fast typing the rendered output is usually
identical between calls, so those were wasted main-thread DOM writes.

Extracted the branch logic into a pure DOM-free _computeConnectionDescriptor()
returning { display, dotClass, text, title } (every branch/string preserved
verbatim; hidden state normalizes the three non-display fields to '' so the
compare is well-defined). _updateConnectionIndicator() now computes the
descriptor, compares all four fields against a cached _lastIndicatorDescriptor,
and early-returns when unchanged — otherwise caches and writes the DOM exactly
as before (display always; dotClass/text/title only when shown). First call
renders (cache starts null). Perf only, no behavior change.

Tests: test/connection-indicator.test.ts — 9 descriptor cases pinning the
exact strings per state + 4 skip cases (first call writes; two identical calls
write DOM once via counting setters; state change and hidden->shown re-render).
31/31 with input-send-order regression; build, frontend-syntax, prettier clean.
2026-07-10 14:39:09 -04:00
Aamer Akhter 4ab89f9a4e COD-137 scope WS per-session limit by clientId (fix spurious 4008 on reconnect)
MAX_WS_PER_SESSION was gated by a bare Map<sessionId,number> counter,
incremented on upgrade and decremented only on the old socket's async
close. A client that dropped and immediately reconnected could land its
new upgrade before the old socket's close fired, briefly over-counting and
tripping a spurious 4008 (-> HTTP fallback). The limit also counted raw
sockets, so a reconnecting client consumed a new slot instead of its own.

Replace the counter with WsConnectionRegistry (new pure, unit-tested module)
that tracks live sockets per session keyed by clientId. A same-cid upgrade
SUPERSEDES its own socket (evicts the stale one with close 4010, reuses the
slot, no net count change) -> a reconnect can never be rejected by the cap.
The reliable-input protocol (shouldApplyInput(cid,seq)) already assumes one
logical client per cid per session, so same-cid eviction is principled, not
a regression of multi-tab (which already collides on seq). Slots are freed
EAGERLY on error/terminate, not just async close; close is identity-matched
so a superseded socket's late close is a no-op. cid-less upgrades are
admitted anonymously up to the cap and never evict (backward-compat).
Client sends cid on the WS upgrade URL (?cid=, encoded, omitted if absent).

Tests: ws-connection-registry.test.ts (reconnect-reclaim at cap, rejects
N+1th distinct, eager-terminate frees slot, cid-less up-to-limit + no-evict,
late-close-no-evict, per-session isolation) + route integration in
ws-routes.test.ts (real upgrade through the cap). 45/45 across registry +
ws-routes + input-send-order + ws-reconnect-plan; tsc 0, build, prettier,
frontend-syntax clean.
2026-07-10 14:39:01 -04:00
Aamer Akhter 20cb42d202 COD-135 re-drive lost input ACK on a live WebSocket (durable-delivery gap)
A reliable-input frame could be stranded forever if its server ACK
({t:'ia',seq}) was lost while the WebSocket kept delivering other output.
_drainSession's WS fast path skips records with sentAt!==0, and after
COD-134 the sweep only force-closes a *silent* socket -- so a lost ACK on
an otherwise-live socket (stale && !silent) was never re-sent.

_redeliverSweep now, for an active-WS session whose oldest unacked frame
is stale but the socket is NOT silent, resets sentAt=0 on every stale
unacked frame and lets the existing _drainSession re-drive them over the
live socket (server dedups by seq). The stale && silent force-close
remains the fallback for a genuinely half-open socket. Restores the
exactly-once recovery guarantee without reintroducing the flap.

Tests: new failing-first COD-135 cases in test/input-send-order.test.ts
(re-drive on live socket; leave not-yet-stale alone; keep stale+silent
force-close). 18/18 across input-send-order + reliable-input-dedup +
ws-reconnect-plan; tsc 0, frontend-syntax, build all clean.
2026-07-10 14:38:52 -04:00
Aamer Akhter 68fd6e8962 COD-134 fix WS flap loop (undefined onopen call) + reconnect resilience + logging
Root cause of the WS->HTTP->WS flapping: the v1.1.15 input-delivery merge left a
call to the now-undefined _flushHttpFallbackQueuesViaWs() in ws.onopen, so every
(re)connect threw a TypeError BEFORE _onWsReady() ran -- durable input was never
re-flushed over the fresh socket, the 2s redeliver sweep then saw stale unacked
frames and force-closed the socket, reconnect, throw again: a self-sustaining
flap loop. Remove the dead call (_onWsReady, 10 lines below, is its replacement).

Resilience + observability:
- Pure CodemanWsReconnect.plan(code, attempt) (constants.js, TDD, 6 tests):
  <4004 -> fast reconnect (immediate jittered first retry, faster backoff);
  4008/unknown->=4004 -> bounded retry-fallback (HTTP no longer sticks until a
  tab switch); 4004/4009 -> give up (session gone). Wired into onclose.
- Redeliver sweep force-closes only a SILENT socket (no recent recv), not one
  actively delivering output/ACKs -- stops self-inflicted flaps while typing.
- Client logs WS close code/reason to crash-diag; server logs [ws]
  open/close/terminate/4008 (console -> journald; Fastify runs logger:false).

Verified: 6/6 unit, tsc 0, frontend-syntax + prettier clean, build; beta WS
reaches connected with zero console errors (onopen TypeError gone),
_wsLastRecvAt tracked, server [ws] lines emit.
2026-07-10 14:38:48 -04:00
Aamer Akhter be4fecdad5 COD-133 fix header WS status indicator + typing lag from v1.1.15 merge
The upstream v1.1.15 merge spliced upstream's transport-object indicator
body onto local's _connectionStatus-based _updateConnectionIndicator()
without defining `transport`, so every transport.* reference threw
ReferenceError on any queued state. That hid the "WS" status and, because
_reliableSend() updates the indicator before _drainSession(), made every
keystroke skip immediate delivery (input flushed only on the 2s sweep =
typing lag).

- Rewrite _updateConnectionIndicator() to show the terminal WebSocket
  transport from _wsState (WS / HTTP / WS… / Offline), falling back to the
  SSE _connectionStatus only on the idle dashboard.
- Only annotate a backlog (· N queued) above 4 bytes so normal typing no
  longer flickers "sending 1B" on each key press.
- test/connection-indicator.test.ts (new): transport labels, the >4B
  threshold, an exhaustive never-throws guard for the ReferenceError, and
  the _reliableSend -> _drainSession invariant (typing-lag guard).
- test/input-send-order.test.ts: reconcile to local's durable input layer
  (the prior coalescing-fallback tests had been failing since 1255e28).
2026-07-10 14:36:21 -04:00
Aamer Akhter c7967d4b55 COD-138 normalize shell scrollback to CRLF so replay doesn't staircase
A shell terminal could render output diagonally (each line shifted one
column right) after a full page reload or a cursor-query-failure replay.

Root cause: capturePaneBuffer's full-history path (capture-pane -p -e -S -)
and its cursor-query-failure fallback returned raw scrollback, which tmux
joins with a BARE \n. The browser xterm uses convertEol:false (correct for
the live PTY stream, which carries real \r\n), so each bare \n dropped a
row without returning the cursor to column 0 -> staircase. The visible /
tab-switch path (formatPaneSnapshot) was immune because it repaints each
row with an absolute cursor CSI.

Fix: new pure helper normalizeScrollbackEol() (\r?\n -> \r\n, idempotent
on CRLF, leaves lone \r overwrites untouched, adds/removes no rows) applied
at both raw-return seams. The absolute-positioned snapshot path is unchanged.

Tests: test/tmux-scrollback-eol.test.ts pins the invariant (no LF without a
preceding CR) + CRLF idempotency + lone-CR preservation. 136/136 across
tmux-scrollback-eol + tmux-capture-full-history + tmux-manager +
routes/session-routes; build, tsc, prettier, frontend-syntax clean.
2026-07-10 11:14:26 -04:00
Aamer Akhter 8be83cd585 COD-47 replay full tmux scrollback on terminal reload
A full page reload (GET /api/sessions/:id/terminal with no ?tail=) now captures
the ENTIRE tmux scrollback via capture-pane -p -e -S -, so users get back history
that scrolled off Codeman's byte buffer. Tab switches (?tail=N) keep the fast
visible-frame capture.

- tmux-manager capturePaneBuffer/captureActivePaneBuffer take { fullHistory }:
  full-history returns raw linear scrollback (skips the single-screen
  formatPaneSnapshot repaint, which would clip multi-screen history).
- /terminal selects full-history on full reload, visible on tail; caps the
  payload at the configured terminalBufferMaxBytes (keeps most-recent bytes,
  line-aligned) and returns source/fullSize/truncated metadata.

Verified: tsc 0, tmux-capture-full-history 5/5, session-routes 68/68.
Caveat: lines tmux already evicted past its history-limit can't be recovered.
2026-07-10 11:08:57 -04:00
Aamer Akhter 09fd1e495f COD-118 fix(test): stub resetRespawnBreaker in MockSession
POST /api/sessions/:id/interactive calls session.resetRespawnBreaker()
before startInteractive(); mock missing the stub → route threw → 422.
2026-07-09 16:00:06 -04:00
Aamer Akhter 7efc6cd5a8 COD-94 fix(lint): remove useless escapes in jump-host regex character class
\[ inside [...] doesn't need backslash — ESLint no-useless-escape.
2026-07-09 12:18:21 -04:00
Aamer AkhterandClaude Opus 4.8 286cf0768d COD-118 feat: circuit breaker bounding repeated non-zero interactive-PTY exits
Defense-in-depth after COD-115. If the interactive PTY exits non-zero
repeatedly within a short window, recovery/reconnect paths recreate it
indefinitely (COD-115 saw 114 'exited with code: 1' events + orphans).

- New pure InteractivePtyExitBreaker (session-pty-exit-breaker.ts):
  injectable time, sliding window, clean-exit resets counter, stays
  tripped until reset(). Defaults: threshold 5, window 10s.
- Session records each interactive PTY exit in the breaker; on trip it
  flips _status to 'error', sets _respawnBlocked, emits
  respawnBreakerTripped. startInteractive() refuses to respawn while
  blocked, so all recovery/reconnect callers stop looping uniformly.
- Explicit user restart (POST /api/sessions/:id/interactive) calls
  resetRespawnBreaker() so intentional restarts are never blocked.
- New SSE event session:respawnBreakerTripped wired in sse-events.ts +
  constants.js (registries in sync) + session-listener-wiring.ts;
  minimal diagnostic toast in app.js.
- Tests: test/respawn-pty-breaker.test.ts (pure trip/reset/window +
  MockSession session-level trip/reset).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 11:49:23 -04:00
Aamer AkhterandClaude Opus 4.8 3c0e6286f6 COD-115 fix: scrub TMUX/TMUX_PANE so tmux-backed sessions don't crash-loop
When the web server is launched from inside a tmux pane it inherits TMUX/
TMUX_PANE. tmux's nesting guard then makes every new attach-bridge PTY
(`tmux attach-session`, used by codex/opencode/gemini and mux-wrapped claude)
exit code 1; the respawn controller recreates the dead bridge → infinite loop.

The existing guard in buildMuxAttachEnv() used `TMUX: undefined` on a
{...process.env} spread, which leaves the KEY present with value undefined —
node-pty serializes it as the literal string "TMUX=undefined", still tripping
the guard. (The working create path in tmux-manager.ts uses `delete`.)

Fix:
- Primary: delete process.env.TMUX / TMUX_PANE at web bootstrap (src/index.ts)
  so every downstream {...process.env} spread is clean regardless of launch
  context. `delete`, not `= undefined`.
- buildMuxAttachEnv(): build a copy and `delete` TMUX/TMUX_PANE/CLAUDECODE
  (and COLORTERM when not truecolor) instead of `: undefined` — same node-pty
  quirk affected all of them.
- Test: assert the keys are genuinely ABSENT (`'TMUX' in env === false`), not
  merely undefined — the prior test only checked `toBeUndefined()`, which is
  why the bug slipped through. Red→green confirmed.

Verified on isolated beta launched from inside tmux (inherited the poisonous
TMUX=codeman,980,7): created a codex session + triggered interactive attach —
the bridge `tmux -L codeman-beta attach-session` spawned with NO TMUX in its
env, attached successfully, zero "exited with code: 1", server healthy.

Circuit-breaker for repeated non-zero bridge exits (AC bullet 4, optional)
split to a follow-up. Deploy-pending (substrate): never auto-deployed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 11:47:38 -04:00
Aamer AkhterandClaude Sonnet 4.6 8a133d083b fix: COD-163 implementation gaps — shortcut overlay, settings tab, remote-case shell
- app.js: getShortcutRegistry()/matchesShortcutEvent()/showShortcutOverlay()/
  renderShortcutOverlay()/closeShortcutOverlay() (needed for DEFAULT_SHORTCUTS
  action dispatch + shortcut-registry-overlay tests)
- settings-ui.js: renderShortcutSettingsList()/startShortcutCapture()/
  onShortcutCaptureKeydown()/resetShortcutOverride()/toggleShortcutEnabled()
  (Settings → Shortcuts tab, needed for shortcut-registry-overlay tests)
- index.html: Shortcuts modal tab + shortcut overlay modal; remove Ctrl+Enter
  hint text (help-modal-shortcuts test asserts absence)
- session-ui.js: remote-case detection in runShell() (caseName vs workingDir);
  saveLastUsedCase after deleting selected case
- test/command-palette-ui.test.ts: expect browse-sessions item (COD-192 adds it)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-09 11:26:24 -04:00
Aamer AkhterandClaude Sonnet 4.6 a0e26db1dc feat: COD-192 add "Browse all sessions" escape hatch to command palette
Pins a "Browse all sessions…" item at the bottom of the command palette
list (after "New session"). Activating it closes the palette and opens
the Session Manager modal, bridging the gap between the fast in-memory
switcher and the full server-side history browser.

The item gets a distinct visual treatment (≡ icon, muted title/icon
color, 4px top gap) so it reads as a secondary action separate from the
primary session rows.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-09 11:11:48 -04:00
Saqeb Akhter 596899e19b fix: COD-157 refine shortcut palette labels 2026-07-09 11:11:33 -04:00