Commit Graph
244 Commits
Author SHA1 Message Date
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
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
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
Teigen 7fb58648ba feat(web): forward wheel + guard clicks for desktop stripped-mouse sessions
Desktop click-to-position-cursor died under the server's mouse-DECSET strip
(same root cause as the mobile touchend tap regression): xterm's native mouse
encoder only emits SGR while mouseTrackingMode is ON, but the server strips the
enabling DECSETs from claude/codex/gemini output. Hand-encode the report for
plain left-clicks (_handleDesktopTerminalClick), skipping every click that
already means something else (synthetic/compat, modified, double/triple,
drag-selection, off-grid, xterm encoder live).

Also widen forwarding to the wheel: Claude Code 2.1.187+ scrolls its own
transcript on SGR wheel reports and no longer captures wheel as select-menu
navigation (verified against 2.1.202), so forward the wheel to the TUI for
strip-mode sessions at the buffer bottom (40ms-coalesced to avoid a tmux
send-keys storm). Shift+wheel and any scrolled-up viewport stay on xterm's
local scrollback. Guard synthetic taps/clicks on viewport-at-bottom so a
scrolled-up report can't hit-test the wrong row.

Tests: 12 cases in test/terminal-touch-tap.test.ts. Verified E2E via Playwright
against the live instance (wheel up/down forward, Shift+wheel local, click).
2026-07-07 17:52:32 +08:00
Teigen 9535edc367 fix(mobile): restore tap-to-position cursor after master merge — hand-encode SGR when server strips mouse DECSETs
v1.1.7 (3172bef, arrived via the master merge) strips mouse-tracking DECSET
sequences from claude/codex/gemini output so the wheel keeps scrolling
scrollback. Side effect: the browser xterm's mouseTrackingMode is permanently
'none' for those sessions, and the mobile touchend tap branch gates its
synthetic click on exactly that mode — so tap-to-position-cursor silently died.

Fix: when tracking reads 'none' but the session mode is one the server strips
(claude/codex/gemini — the PTY-side TUI still has tracking ON), encode the SGR
press+release report directly from the touch point and send it to the PTY,
bypassing xterm's mouse encoder. No DOM click is dispatched, so xterm's local
selection cannot trigger either.

Tests: 3 new cases in test/terminal-touch-tap.test.ts (SGR encoding, grid
clamping, shell-mode exclusion); verified E2E via Playwright iPhone emulation
against both a stripped-stream instance and the production bundle.
2026-07-07 17:52:32 +08:00
Teigen 443b85c18e fix(mobile): CJK input loss — IME state machine, focus routing, and Android InputConnection recovery
Three independent root causes of intermittent Chinese character loss
(English was unaffected because it bypasses the composition path):

1. input-cjk.js state machine: stuck _composing when compositionend never
   fires (WeChat/Sogou IMEs) silently swallowed all input; the deferred
   compositionend flush could reset the textarea mid-next-composition
   (cancels the live IME composition on iOS); the 100ms keydown-echo
   window discarded ANY input regardless of content.

2. Focus stealing: session-select / SSE-reconnect paths call
   terminal.focus() (15+ call sites), landing focus on xterm's hidden
   textarea; with the CJK onData gate active, everything typed there was
   swallowed. Fix: focus router in initTerminal routes ALL
   terminal.focus() calls to the CJK field while it is visible, plus a
   self-healing onData gate that reclaims focus when it swallows input.

3. Android InputConnection wedge (9-key IMEs + Chromium): the keyboard
   composes in its own UI but delivers zero DOM events. Fix: skip
   redundant textarea value/selection writes (they race IME session
   setup), and re-tapping the focused empty field forces a blur→focus
   cycle that restarts the input session.

Diagnostics: input-cjk.js now traces every IME event/flush decision into
the crash-diag breadcrumbs; /api/crash-diag stores beacons per page-load
id (iOS PWA reloads no longer wipe the trail, concurrent clients no
longer clobber each other) and flushes on visibilitychange.

Tests: test/input-cjk.test.ts (vm-sandbox, 9 cases incl. regression
guards for all three root causes).
2026-07-07 17:52:10 +08:00
Aamer Akhter ce4c5dd584 test(types): stop asserting Date.now() timestamps are deep-equal
createInitialRalphTrackerState() stamps lastActivity: Date.now(). The
'should create fresh instances each time' test deep-equaled two factory
results, so two calls straddling a millisecond boundary differed by 1ms
and failed intermittently (e.g. PR #139 CI: 1782927694581 vs ...580).

Exclude the dynamic lastActivity from the equality check and assert it
is a number separately, preserving the test's intent (distinct instances
with identical initial field values) without the timing race.
2026-07-05 21:49:31 -04:00
Kevin Crawley bf36eb0db4 feat(terminal): add WebGL renderer toggle in settings
Adds a 'WebGL Renderer' toggle to Settings > Appearance (desktop). WebGL
stays on by default; users can turn it off to force the DOM renderer when
they hit GPU glitches, without needing the ?nowebgl URL param. Explicit
opt-in (or ?webgl=force) clears a stale auto-fallback marker. Mobile skip
and the long-task auto-fallback safety net are unchanged.

The device/param/sticky/pref interaction is factored into a pure,
unit-tested shouldSkipWebGL() helper in constants.js.
2026-07-01 20:41:38 -05:00
Aamer Akhter 4dfdbcd100 COD-160 unified session list: backend service + endpoint
First increment of the read-only "complete + searchable session list".

- New src/services/unified-session-service.ts: mergeUnifiedSessions() combines
  live + persisted (state.json) + lifecycle + ~/.claude transcript history + mux
  stats into one list de-duped by sessionId, with precedence
  history < lifecycle < persisted < live, a meaningfulness floor that drops bare
  lifecycle/mux-only noise, and a stable newest-first sort. Plus
  filterAndPaginate() (case-insensitive q over name/firstPrompt/workingDir/
  sessionId; total before paging; limit clamped [1,500]). No IO — unit-testable.
- New GET /api/sessions/unified in session-routes.ts: gathers the five sources
  from ctx (sessions/store/lifecycle/scanProjectDir/mux, each try/caught), feeds
  the pure service, returns { sessions, total } (ApiResponse envelope). testMode
  short-circuits to empty.

Tests: unified-session-service.test.ts (12, pure) + unified-sessions-routes.test.ts
(4, app.inject).
2026-07-01 13:25:16 -04:00
Aamer Akhter ad71a92f29 COD-80 raise terminal history/scrollback/buffer defaults
Bump the centralized terminal-history defaults: tmux scrollback 50k->100k and
PTY buffer cap 2MB->32MB (trim 1.5MB->24MB). Both remain env/settings overridable
and bounds-clamped. Worst-case 20-session buffer budget rises 40MB->640MB.
Stacked on the terminal-history config commit.
2026-07-01 13:00:08 -04:00
Aamer Akhter 8c0c94540c COD-80 centralize terminal history/scrollback/buffer limits into config
Introduce src/config/terminal-history.ts: one place for terminal scrollback,
tmux history-limit, and PTY buffer byte caps, each overridable via env var or
the settings object and bounds-clamped via resolveTerminalHistoryConfig().
Defaults match the prior hardcoded values, so this is behavior-neutral. Wires
the resolver through buffer-limits, tmux-manager (incl. a setHistoryLimit so a
settings change applies live), session, server, system-routes, session-routes,
schemas, and the config port. Adds 4 optional settings keys (terminalScrollback
Lines, tmuxHistoryLimit, terminalBufferMaxBytes, terminalBufferTrimBytes) with
bounds + a trim<=max cross-check.
2026-06-30 19:52:49 -04:00
Codeman maintainer 368fc20fc2 fix: address PR review findings for Gemini run mode + Ralph todo-config
Gemini (PR #134) blockers:
- runGemini() now unwraps the {success,data} envelope: status check reads
  .data.available, quick-start reads data.data.sessionId (was reading the raw
  shape, so the Run-Gemini button could never start a session).
- setGeminiEnvVars() now uses the socket-scoped ${this.tmux()} setenv instead of
  bare tmux — Gemini/Google auth env vars were targeting the wrong tmux server
  and silently failing on every install.

Gemini parity polish:
- gemini tab-mode badge ('gm') + .tab-mode.gemini CSS; kill-dialog label
  'Kill Tmux & Gemini'; codeman doctor dependency-registry entry; export
  isGeminiAvailable from utils barrel; COLORTERM=truecolor + unset NO_COLOR;
  add gemini to isAltScreenStripMode (Ink TUI, repaints inline like Codex/Claude).
- Revert 4 system-routes.test.ts envelope assertions weakened to
  (body.message ?? body.error) back to (body.success === false).
- Add a runGemini() vm-sandbox test that drives the envelope path end-to-end.

Ralph todo-config (PR #135): maxTodos/todoExpirationMinutes are now persisted
and read back — surfaced via the loopState getter (RalphTrackerState) into
toState()/SSE broadcast and restored in restoreState(), mirroring maxIterations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 00:26:06 +02:00
Codeman maintainer 9cc310e843 Merge PR #134: Gemini run mode (third external-CLI mode alongside Codex/OpenCode) (COD-36) 2026-06-25 00:10:52 +02:00
Codeman maintainer aa991ece8f Merge PR #133: cross-session search (federated GET /api/search + history-panel search box) (COD-113) 2026-06-25 00:10:38 +02:00
Codeman maintainer 3b4106c349 Merge PR #136: Away digest feature (COD-41) 2026-06-25 00:10:38 +02:00
Aamer Akhter 509595b837 COD-52 fix: wire maxTodos + todoExpirationMinutes through ralph-config to the tracker
The Ralph settings modal sent maxTodos/todoExpirationMinutes but RalphConfigSchema
(zod) stripped them and the ralph-config route never applied them, so the inputs
were silent no-ops.

Fix: add both as optional positive-int fields to RalphConfigSchema; destructure
and apply them in the ralph-config route (matching the maxIterations pattern).
RalphTracker had no setters (the values were module constants) — added per-instance
_maxTodos/_todoExpiryMs (defaulting to the same constants, behavior unchanged),
switched the eviction + expiry sites to read them, and added
setMaxTodos/setTodoExpirationMinutes (minutes→ms) + getters.

Test: route test POSTs the two fields and asserts the route applies them to the
tracker. Verified RED (setters not called — fields stripped) → GREEN. 34/34
ralph-routes tests pass; tsc + eslint(src) + prettier + build clean. Frontend
already sent the fields (no change).
2026-06-19 18:00:29 -04:00
Aamer Akhter c95e94e4cb COD-8 add away digest 2026-06-19 17:58:06 -04:00
Aamer Akhter 19139837e4 feat: add Gemini run mode 2026-06-19 13:10:17 -04:00
Aamer Akhter 95df96e06a COD-9 add cross-session search backend (GET /api/search) v1
Bounded federated search over in-memory stores (sessions/cases, run-summary
events, file paths). Zod-validated query (q 1-200 chars, types csv, limit 1-60),
grouped session->event->file with exact-match-first + recency tiebreak, total
cap 60 + per-group cap 25, snippet cap 200, path-safety (relativePath only).
Frontend search box (history panel) deferred to next cycle; resume/history-prompt
text matching deferred to v1.1 (lives in large on-disk files, out of v1 bounded scope).

New: src/search-service.ts (pure core), src/types/search.ts, src/web/routes/search-routes.ts.
Tests: test/search-service.test.ts (14), test/routes/search-routes.test.ts (10).
2026-06-19 12:35:16 -04:00
Codeman maintainer 1255e28f6f fix(input): durable exactly-once input delivery so a dropped link can't lose a prompt
A "sent" prompt could vanish with no trace on a flaky connection (e.g. a train):
with local echo on, Enter cleared the overlay then sent over the WebSocket
fire-and-forget. On a half-open socket (readyState===OPEN, dead TCP) ws.send()
doesn't throw, so the frame was silently discarded, nothing was enqueued, and
navigator.onLine stayed true — the prompt was lost and never resent.

Replace the best-effort offline queue with a durable, acknowledged delivery layer:

- Client (app.js): every input frame is recorded with a stable clientId +
  monotonic per-session seq and persisted to localStorage BEFORE delivery, and
  only dropped on a server ACK. Delivered over WS (acked via {t:'ia',seq}) or,
  when the socket is down, POST in seq order (HTTP 2xx = ACK). A 2s sweep
  force-reconnects a WS whose oldest frame is unacked past 4s (half-open sockets
  never recover on their own); on reconnect/reload all pending frames re-deliver.
  Survives reconnects AND page reloads. Connection indicator shows pending count.
- Server: Session.shouldApplyInput(clientId, seq) applies each frame exactly once
  (bounded MRU map); ws-routes + POST /input dedup a redelivered seq but still ACK
  it (200 / {t:'ia'}), so an at-least-once resend can never type the prompt twice.
  Untagged input (curl/legacy) applies unconditionally — no behavior change.
- terminal-ui.js sendInput() (voice / keyboard-accessory / paste) now routes
  through the same durable layer.

Tests: test/reliable-input-dedup.test.ts (exactly-once semantics on the real
Session) + POST /input dedup route tests. Design: docs/reliable-input-delivery.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 16:58:40 +02:00
Codeman maintainer 98b2124d7e feat(ultracode): enrich live run tracking — real per-agent tokens/tools/state, readable title, blue connector line, click-to-open window
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 10:00:04 +02:00
Teigen 2c81bbc08b feat(terminal): add forced redraw resize 2026-06-17 23:41:47 +08:00
Teigen b374121c18 fix(mobile): prevent terminal tap selection 2026-06-17 23:40:21 +08:00
Codeman maintainer a0ac10a07c feat(tunnel,ui): purple tunnel button + opt-in unauthenticated tunnel with warning (v1.1.9)
- Daylight Blue: Cloudflare Tunnel welcome button is now purple (was orange),
  keeping Claude blue / Tunnel purple / OpenCode green distinct.
- Allow enabling the Cloudflare tunnel with no CODEMAN_PASSWORD via the UI: the
  toggle now pops a security confirm dialog and, on confirm, sends an explicit
  per-request acknowledgeUnauthTunnel:true (new action field, never persisted).
  Server logs a loud warning whenever a passwordless public tunnel starts.
  curl/API/CLI stay refused unless password/env/flag — no accidental exposure.

Tests: extend test/routes/system-routes-tunnel-guard.test.ts (ack allows + not
persisted; ack:false still refuses). Verified e2e on an isolated instance
(purple button, confirm dialog, retry carries the flag, no real tunnel opened).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 18:44:03 +02:00
Codeman maintainer 3172befd5d fix(terminal): keep Claude scrollback reachable — strip alt-screen/3J/mouse for claude mode (v1.1.7)
Terminal scroll-up intermittently broke for Claude sessions (most visible on
iPhone). Claude Code periodically emits alt-screen switches (?1049h/?47h/?1047h),
scrollback-erase (3J), and mouse-tracking enables for full-screen UIs, which move
xterm.js to the scrollback-less alt buffer / wipe saved lines / hijack the wheel.
Codeman stripped these but only for codex mode.

Share the strip via isAltScreenStripMode(mode) = codex || claude, applied at both
sites that were codex-only: the live PTY stream (Session._handleTerminalOutput,
incl. the chunk-boundary carry) and the /terminal buffer replay. shell stays
excluded (vim/less/htop need the alt screen); opencode unchanged.

Tests: test/claude-scrollback-strip.test.ts (8 new); codex strip tests unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 18:08:45 +02:00
Codeman maintainer b6531cbf79 fix(ultracode): floating windows pop for LIVE runs (watch transcript tree)
The Workflow runtime writes workflows/wf_<id>.json only at completion (always
terminal), so workflow-run-watcher never saw a run until it was already done and
the ACTIVE-gated floating window never popped. The watcher now also scans
subagents/workflows/wf_<id>/ and synthesizes a minimal running record (agentId
slots preserved for the transcript-click join, lastActivityAt from mtimes,
done/running from the journal), superseded by the real wf_<id>.json at
completion. Standalone (no subagent-watcher import). Verified e2e on a real
in-flight run; +6 unit tests. Bumps 1.1.3 -> 1.1.4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:20:52 +02:00
Codeman maintainer c15c19fab7 feat(ultracode): master-detail tab for Workflow/ultracode run visualization
Opt-in (showUltracodeAgents, default OFF) panel that visualizes ultracode /
Workflow-tool runs like Claude Code's "working agents" TUI: LEFT = runs + phases
(selectable tasks), RIGHT = each run's agents with model, live state, tokens
burned, and tool calls.

Standalone — ZERO edits to subagent-watcher.ts. A new workflow-run-watcher.ts
singleton globs the run-state tree (~/.claude/projects/*/*/workflows/wf_*.json,
disjoint from the transcript tree), strips the heavy script/scriptPath/result/logs
fields (174KB -> ~25KB/run), and emits workflow:run_* SSE events. The LEFT list
ships lightweight summaries (getLightState replay + SSE); the RIGHT pane fetches
the full run (with agents[]) via GET /api/workflows/:runId on selection.

Backend: workflow-run-watcher.ts, types/workflow-run.ts, config/workflow-config.ts,
3 SSE events, getLightState workflowRuns replay, GET /api/workflows[/:runId],
showUltracodeAgents schema key + boot-gate (default OFF) + live toggleService.
Frontend: ultracode-panel.js (debounced master-detail render, run/phase select),
header launcher (btn-ultracode-agents--hidden marker -> mobile-guard-exempt),
App Settings toggle (SYNCED, deliberately not in displayKeys).

Agent states on disk are start|progress|done (start=queued; done has
durationMs/resultPreview). Tests: workflow-run-watcher (9), workflow-routes (3).
Verified: tsc/lint/prettier/frontend-syntax/public-assets/mobile-header-guard
clean; full test:ci green (2986 passed); live server + Playwright e2e against 25
real runs (28-agent grid, phase filter, OFF hides launcher).

Design: docs/ultracode-agent-viz-plan.md (rev. 3).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 08:40:05 +02:00
Codeman maintainer f6a30d7335 fix(subagent-watcher): discover workflow-nested agents + harden meta→transcript upgrade
Two follow-ups to db93491 (the 2026-06 CC meta.json format change), after
reverse-engineering the new on-disk layout with a live current-CC subagent +
1Hz fs poller:

(1) Workflow recursion — the Workflow tool nests its agents at
    subagents/workflows/{wf}/agent-{id}.jsonl, one level below the flat
    subagents/ scan, so they were never tracked. Add watchWorkflowDirs()
    (driven from scanForSubagents) to descend and watch each workflow dir
    (idempotent; fs.watch recursive is unsupported on Linux, so the ~5s
    periodic scan re-drives it — same latency as new-session discovery).
    Require the `agent-` prefix in the flat readdir + watch callback so a
    workflow dir's sibling journal.jsonl can't register a bogus "journal" agent.
    E2E verified against real ~/.claude/projects: 32 workflow-nested agents
    discovered (wf_fa35c1d8-4a9), 0 bogus journal agents.

(2) Transcript timing — empirically the per-agent .jsonl IS written at the
    standard subagents/ path and grows incrementally (tailable); the
    /tmp/.../tasks/<id>.output the prior probe found is just a symlink back to
    it. meta.json lands at spawn, the .jsonl a beat later. Add a meta→transcript
    upgrade in registerAgentFile: when an agent registered meta-only gets its
    sibling .jsonl, re-point filePath, drop the stale sidecar context, start
    tailing, and emit subagent:updated (not a duplicate discovered). Corrects the
    now-inaccurate "no transcript to tail" doc comment on registerAgentMeta.

Tests: 2 new cases (workflow-nested discovery; journal.jsonl not registered).
All 56 pass; tsc/lint/format clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 01:43:15 +02:00
Codeman maintainer db93491dd1 fix(subagent-watcher): discover subagents via agent-*.meta.json (CC format change)
Claude Code changed its subagent on-disk format (~2026-06-14): TUI Task
subagents now write `agent-{id}.meta.json` ({agentType,description,toolUseId})
into the session's `subagents/` dir and no longer reliably write a per-agent
`agent-{id}.jsonl` transcript there. The watcher discovered agents ONLY by
`.jsonl`, so it tracked zero — subagent windows and the monitor's "N TRACKED"
showed nothing.

- Add `registerAgentMeta()`: discover from the meta sidecar (description from
  meta.description/agentType), prefer a sibling `.jsonl` transcript when present
  (richer), never tail a meta file.
- Initial scan + directory watcher now handle `.meta.json` alongside `.jsonl`.
- Tests: 2 new cases (meta-only discovery; prefer-.jsonl-when-present).
  Verified e2e against a real ~/.claude/projects fixture.

Known follow-ups (not in scope): meta-only agents have no per-agent transcript
to tail (no live tool-call feed, status stays 'active'); workflow agents under
`subagents/workflows/{wf}/agent-*.jsonl` are still missed by the flat scan.

Also adds the README screenshot tooling used to surface this:
- capture-real-overview.mjs: DSF=2 + ?nowebgl crisp path (DOM renderer avoids
  the WebGL glyph-doubling at deviceScaleFactor>1).
- capture-readme-real.mjs: real-instance desktop-scene capture (dashboard/
  monitor/subagent) for an isolated beta seeded from prod settings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 01:24:30 +02:00
Codeman maintainer b7ff54b2ec fix: file viewer opens audio/svg/binary like the attachments viewer
The File Browser preview and Attachments preview share openFilePreview(),
but the workspace branch (via /file-content) misclassified several types the
attachments viewer handled fine:

- SVG was reported as type:image, but file-raw serves SVG as octet-stream +
  attachment (XSS hardening), so the <img> broke. Now fetched and rendered via
  a same-origin image/svg+xml blob <img> (safe; <img> never runs SVG scripts).
  file-raw's SVG hardening is unchanged.
- Audio (mp3/wav/ogg/m4a/aac/flac/opus) was type:binary -> "Cannot preview".
  Now classified as audio and rendered with <audio controls>; file-raw gained
  the matching audio/video MIME types so playback works.
- Binary formats not in the hardcoded list (xlsx/doc/zip/...) were decoded as
  UTF-8 and dumped as mojibake. Replaced the static list with a NUL-byte
  content sniff that flags arbitrary binaries; the binary fallback now offers a
  Download link instead of dead-ending.

Adds route tests for audio, known-binary (xlsx), and NUL-sniff classification.
Verified end-to-end on an isolated instance + headless browser.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 00:42:12 +02:00
Ark0N e1e670594b Merge PR #128: auto-wrap desktop session tabs on overflow + resize re-eval
Auto-wrap desktop session tabs to a second row on overflow
2026-06-14 22:43:43 +02:00
Ark0N 2e28e17834 Merge PR #123: hide CJK textarea on welcome screen + mobile test update
fix(cjk): hide CJK textarea on welcome screen and fix vertical centering
2026-06-14 22:43:17 +02:00
Ark0N 90f18438ff Merge PR #127: require hook-event secret unconditionally + stale-config self-heal
Require the hook-event secret unconditionally (drop managed-tunnel gating)
2026-06-14 22:43:13 +02:00
Ark0N 5b62f397ec Merge PR #129: macOS Option/physical-key session shortcuts + terminal-ui ESC-leak fix
Make Option/Alt session shortcuts work on macOS (physical key codes)
2026-06-14 22:43:08 +02:00
Ark0N 1e54ebcdf4 Merge PR #125: add codeman doctor dependency checker + accuracy review fixes
Add `codeman doctor` tool-dependency checker
2026-06-14 22:43:04 +02:00
Ark0N 0364bea166 Merge PR #126: harden markdown sanitizer with DOMPurify (mXSS) + allowlist/test review fixes
Harden markdown HTML sanitizer with vendored DOMPurify (mXSS)
2026-06-14 22:42:59 +02:00
Claude (Codeman maintainer) c7e8ff616f fix(tabs): re-evaluate auto-wrap on resize and on every full tab rebuild
Review polish on the desktop tab auto-wrap:

- Auto-wrap is purely width-driven, but updateTabOverflowMode() was only called at the
  tail of _renderSessionTabsImmediate (SSE content renders). Window resize — the primary
  trigger for tabs crossing the one-row overflow threshold — never re-evaluated it, so
  narrowing/widening the window left the wrap state stale until an unrelated status event
  fired a render. Call it from the debounced window-resize handler (no-op on
  mobile/tablet, where the method bails).

- Move the re-evaluation into _fullRenderSessionTabs() as well, so the incremental
  branch's two early `_fullRenderSessionTabs(); return;` paths (badge add/remove, which
  change tab width) and the manual two-rows toggle (applyTabWrapSettings → _fullRender…)
  re-evaluate too. The latter also fixes a transient where enabling manual two-rows while
  auto-wrap was on left both classes set (clipping folder tabs to 96px) until the next
  render.

- Add boundary cases to the policy test: exact fit and the +1 sub-pixel tolerance (no
  wrap), 2px over (wrap), and a single overflowing tab (no wrap).

Verified: tab-overflow test passes; tsc, check:frontend-syntax, check:public-assets,
prettier all clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 22:38:19 +02:00
Claude (Codeman maintainer) 21fbff4d8a fix(hooks): self-heal stale pre-secret hook configs so COD-91 doesn't 401 them
Making the hook-event secret unconditionally required closes the own-loopback-proxy gap,
but it would also silently 401 the hook curls baked into cases created BEFORE the secret
header existed (COD-54, 2026-06-10): writeHooksConfig only runs at case CREATION, so an
existing/linked case on a password-protected install keeps secret-less curls that the new
gate rejects (degrading idle/stop/teammate/task signalling with no error surfaced).
No-password installs are unaffected — the gate isn't registered without CODEMAN_PASSWORD.

Add `refreshStaleHookSecret(casePath)` and call it on Claude-mode spawns in
POST /api/sessions and POST /api/quick-start (existing-case branch). It regenerates the
hooks block ONLY when settings.local.json already holds Codeman's own hook curls (they
target /api/hook-event) that lack the X-Codeman-Hook-Secret header — a no-op when the
hooks are absent, not ours, or already current, so it never clobbers user customizations
and is cheap on every spawn. Fresh cases are unaffected (writeHooksConfig already wrote
the secret). withSettingsLock serializes it with the model/statusLine writers.

Verified: new test/hook-secret-selfheal.test.ts 5/5 (heal + key-preservation + no-op on
current/foreign/absent/malformed); the PR's cod54 + auth-security suites still pass
(36); tsc, lint, format:check, and npm run build all clean (symbol present in dist).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 22:35:29 +02:00
Claude (Codeman maintainer) 8ffb2b0644 test(cjk): update the mobile server-override test for the welcome-screen gate
The PR gates CJK textarea visibility on an active session
(`showCjk = cjkUserEnabled && !!activeSessionId`) so the fixed-position textarea no
longer floats over the welcome overlay. That intentionally changes the behavior the
existing `shows the CJK textarea on mobile only for server override` test asserted —
it set `_serverCjkOverride = true` on a fresh page (no active session) and expected the
textarea visible, which now (correctly) resolves to hidden. The test lives in
test/mobile/** (excluded from CI), so it wasn't caught by the PR's green CI.

Update the test to verify the new, intended behavior: with the server override on it
stays hidden on the welcome screen (no active session) and is revealed once a session
is active. This is a co-authored review fix; the original change is TeigenZhang's.

Verified: tsc, check:frontend-syntax, check:public-assets, prettier all clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 22:30:09 +02:00
Claude (Codeman maintainer) 80ebf8b549 fix(shortcuts): stop Alt/Option nav keys leaking ESC sequences into the terminal
The PR migrated the app.js tab-nav handler to physical e.code but left xterm's
pass-through gate (terminal-ui.js) matching ev.key digits. Consequences:

- Alt+[ / Alt+] (the new bindings) were never in the gate, so xterm sent ESC[ / ESC]
  to the PTY on every platform AS WELL AS switching the session.
- Alt+digit on a remapped macOS Option layout (Option+1 -> "¡") didn't match the
  ev.key '0'-'9' gate either, so xterm injected ESC<char> — on exactly the layouts
  this PR exists to fix.

Update the xterm gate to mirror app.js exactly: suppress when
`ev.altKey && !ctrl && !shift && /^(Digit[1-9]|BracketLeft|BracketRight)$/.test(ev.code)`.
Returning false there tells xterm not to write to the PTY, so the shortcut switches
the tab with no stray escape sequence.

Also: relabel the docs Alt/Option (the mechanism is layout/OS-independent, so the
shortcut works for Linux/Windows Alt users too — "Option" alone was Mac-only wording),
and add a keyboard-shortcuts test asserting terminal-ui.js gates on the same physical
codes so this desync can't regress (a grep the original test missed).

Verified: keyboard-shortcuts test 4/4, check:frontend-syntax, check:public-assets,
format:check all clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 22:27:36 +02:00
Claude (Codeman maintainer) c101cc8716 fix(doctor): correct Node minimum, drop phantom gemini, add pdftoppm, validate --category
Review fixes on top of the `codeman doctor` checker:

- Node minVersion 18.0.0 -> 22.0.0. package.json engines is ">=22.0.0" and the docs/CI
  require Node 22+, so doctor was green-lighting Node 18-21 (a false pass).
- Remove the phantom `gemini` registry entry. Codeman has no Gemini backend
  (SessionMode = 'claude' | 'shell' | 'opencode' | 'codex'); the entry advertised a
  dependency that nothing uses.
- Add `pdftoppm` (poppler) to the office group. document-thumbnailer.ts calls pdftoppm
  with no fallback as the sole PDF/Office first-page thumbnail renderer, yet it was
  absent from the registry, so doctor never reported it missing.
- Fix the `--category` mismatch: the help advertised `documents|media` categories that
  the ToolCategory type/registry never defined, and an unknown category silently
  produced an empty "all healthy" table. Introduce TOOL_CATEGORIES as the single source
  of truth (type + help + validation); an invalid `--category` now errors with the
  valid list and exits 2.

Verified: tsc, lint, format:check all clean; both dependency tests pass (20);
`doctor` runs correctly (Node 22.22 ok, pdftoppm detected, no gemini), `--category media`
errors with exit 2, `--category office` lists libreoffice/pdftoppm/msoffice.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 22:24:46 +02:00
Claude (Codeman maintainer) cceb24ed8f fix(sanitizer): enforce the curated allowlist + make the test run order-independently
Review fixes on top of the DOMPurify mXSS hardening:

- Remove `USE_PROFILES: { html: true }` from the sanitize-html.js config. DOMPurify
  treats USE_PROFILES and ALLOWED_TAGS/ALLOWED_ATTR as mutually exclusive — with a
  profile set it resets the allow-lists to the full HTML profile and silently ignores
  the curated lists, so the tight markdown-only allowlist was dead config (still
  XSS-safe via FORBID + core, but far broader than intended: <button>/<input>/
  <details>/<audio>/<select>/<label> all survived). Dropping USE_PROFILES puts the
  curated ALLOWED_TAGS/ALLOWED_ATTR back in force; FORBID_TAGS/FORBID_ATTR stay as
  defense-in-depth and DOMPurify keeps its default safe-URI handling.

- Rewrite test/markdown-sanitizer.test.ts to run in the default node environment with
  an in-test jsdom window instead of a per-file jsdom environment. That environment
  externalizes node:fs/node:path under vite, so the suite failed to load in isolation
  ("No such built-in module: node:") and only survived the full CI run because an
  earlier node-env test happened to pre-cache node:fs — order-dependent and fragile.
  The rewrite is order-robust and adds an "allowlist is actually enforced" block
  (non-markdown tags must be dropped) that fails if USE_PROFILES is reintroduced.

Verified: 25/25 tests pass standalone under config/vitest.ci.config.ts; tsc, lint,
format:check, check:frontend-syntax, check:public-assets, and npm run build all clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 22:21:13 +02:00
Aamer Akhter 60dab7ce3f Make Option/Alt session shortcuts work on macOS (physical key codes)
Tab-switch shortcuts matched e.key, so on macOS Option+1 emits a special
character ('¡', not '1') and the shortcut silently failed. Switch to physical
e.code (Digit1-9), which is layout-independent. Also adds Option+[ / Option+]
for previous / next session. Help modal + README updated.

Test: test/keyboard-shortcuts.test.ts.
2026-06-14 15:54:50 -04:00
Aamer Akhter a5263b3252 Auto-wrap desktop session tabs to a second row on overflow
When desktop session tabs overflow one row, wrap them to a second row instead
of horizontal scroll — unless the user has pinned the manual two-row layout
(tabTwoRows). Mobile/tablet keep horizontal scroll. The wrap policy
(shouldAutoWrapTabs) lives in constants.js as a pure, unit-testable function;
updateTabOverflowMode() measures overflow after each tab render and toggles
.tabs-auto-wrap.

Test: test/tab-overflow.test.ts (vm-loads constants.js, asserts the policy).
2026-06-14 15:49:12 -04:00