Commit Graph
634 Commits
Author SHA1 Message Date
Codeman maintainer 98ba1fd49c fix(input): stop the connection indicator flashing "Sending 1B…" while typing
The reliable-delivery layer marks every keystroke as briefly pending until its
ACK lands a few ms later, which made the connection indicator flash
"Sending 1B…" on every character during normal typing. Hide the indicator
entirely while the connection is healthy (connected/connecting) — it now only
appears for an actual problem (reconnecting/offline), where the queued-byte
count reassures the user their input is safely buffered.

Verified in a real browser: hidden throughout connected typing, shows
"Offline (NB queued)" when offline, hides again after reconnect+delivery.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 23:24:49 +02:00
Codeman maintainer 50b8f1d9a0 feat(mobile): large + multi-image uploads from the camera-roll picker
The mobile copy/paste overlay's "🖼 Image" button (and drag-drop / paste)
now handles real-world photo batches:

- Up to 20 images per batch, uploaded with bounded concurrency (3) and a
  live "Uploading N/M…" progress toast; a final summary reports successes,
  any failures, and whether the 20-cap trimmed the selection (no silent
  truncation).
- Per-file upload limit raised 10MB → 50MB (MAX_PASTE_IMAGE_BYTES in
  buffer-limits.ts, env-overridable) so full-resolution phone photos and
  large screenshots aren't rejected.
- Very large images are downscaled to <=4096px longest edge before upload:
  fixes iOS Safari's ~16.7M-px <canvas> limit (which made huge photos fail
  to re-encode and fall back to an original that tripped the magic-byte
  check), and keeps batch uploads fast and small.
- Fix a latent concurrency bug the batch path exposed: the first parallel
  uploads to a session raced on `mkdir(.claude-images)` and the EEXIST
  losers 500'd. mkdir now treats an existing real directory as success
  (re-verifying it isn't a planted symlink), so concurrent uploads succeed.

Verified end-to-end in a real browser (Playwright): downscale, >10MB
server acceptance, 20-cap, 20/20 concurrent uploads landing on disk.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 23:12:41 +02: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 9d12fc7f94 feat(gesture): hand-drag subagent & ultracode windows in the gesture beta
Pinch any floating subagent or ultracode run/transcript window with the
camera hand-tracking overlay and move it anywhere. Adds a 'window' grab
kind to entry.ts, slotted into the pinch priority chain
(cg-float panel → agent window → session tab → toolbar button). It moves
the window via its own style.left/top (matching app.js's mouse drag,
incl. bottom:'auto') and calls window.app.updateConnectionLines() so the
glowing connector line to the session tab tracks live — app.js redraws
from fresh rects, so no reach into its internals.

Hardening: el.isConnected guard (ultracode windows tear down mid-grab on
SSE reconnect / auto-close), all window.app calls optional-chained +
try/caught so the standalone playground still works, bring-to-front via
app.js's own z-counters, rAF-coalesced redraws cleared on drop so the
final placement always redraws.

Rebuilt the committed gesture-codeman.js bundle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 16:01:04 +02:00
Codeman maintainer a8782b364f fix(security): harden remaining inline onclick handlers against XSS double-context
Extends PR #132 (ultracode handlers) to the rest of the frontend. The same
JS-string-in-HTML-attribute pattern — '${escapeHtml(value)}' — remained in 32
more inline handlers across app.js, panels-ui.js, session-ui.js,
subagent-windows.js, and notification-manager.js. The browser HTML-decodes the
attribute value before parsing the handler source, so escapeHtml's &#39; reverts
to ' and a quote-bearing id/path/name breaks out of the JS string literal into
executable code.

Switch all to escapeHtml(JSON.stringify(value)): JSON.stringify JS-encodes and
quote-wraps first, then escapeHtml handles the HTML-attribute layer, so the
value round-trips as one inert string argument.

Also fixes two non-escapeHtml variants of the same class:
- panels-ui.js: mux-session `sid` was pre-escaped with escapeHtml() then dropped
  into a single-quoted JS string (selectSession / killMuxSession). Now
  JSON.stringify'd at the source.
- orchestrator-panel.js: phase.id was interpolated raw (no escaping at all) into
  orchestratorSkipPhase / orchestratorRetryPhase. Now escapeHtml(JSON.stringify()).

The most realistic vector here is file paths (panels-ui openLogViewerWindow) —
filenames can legally contain a single quote.

Numeric interpolations (${i+1}, ${index}, ${item.version}) and the
developer-literal ${onclick} in orchestrator-panel are not user data and are
left as-is. Verified: 0 vulnerable patterns remain, all 22 frontend files parse
(check:frontend-syntax + node --check), and a runtime round-trip confirms the
injection that fired under the old pattern is now an inert string argument.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 15:26:42 +02:00
Aamer Akhter 06871eb7e3 Harden ultracode inline onclick handlers against XSS
The ultracode run/agent cards and minimized-tab badges built inline onclick
handlers by interpolating escapeHtml(value) inside single-quoted JavaScript
strings within an HTML attribute:

    onclick="app.openUltracodeAgentWindow('${escapeHtml(agentId)}', ...)"

escapeHtml maps ' -> &#39;, but the browser HTML-decodes the attribute value
before the handler source is parsed, so &#39; becomes a literal ' again and a
quote in a run/agent/session id breaks out of the string literal into
executable JS. escapeHtml alone is insufficient for the JS-string-within-HTML-
attribute double context.

Switch each handler to escapeHtml(JSON.stringify(value)): JSON.stringify
JS-encodes and quote-wraps the value, then escapeHtml handles the HTML
attribute layer, so the value round-trips as an inert string argument. This
matches the encoding already used by other handlers in these files.

Affected:
- ultracode-panel.js: selectWorkflowRun, openUltracodeAgentWindow
- ultracode-windows.js: restore/dismiss for minimized run and agent tabs
2026-06-19 08:55:24 -04:00
Codeman maintainer 5d59c1764d feat(ultracode): in-page agent transcript windows + minimize-to-tab (1.1.14)
Clicking an agent card opens its live transcript as an in-page connected
floating window instead of a detached browser popup. The "−" button on both
run and agent windows now minimizes into the originating session tab as a
restorable ULTRA badge (🧬 runs, 📄 transcripts). Removes the old
collapse-to-header behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 21:26:33 +02:00
Codeman maintainer cfcd9d288b fix(mobile): keep /compact in extended accessory bar, only drop it from simple
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 10:29:49 +02:00
Codeman maintainer 9c22114b5a fix(mobile): remove /compact button from keyboard accessory bar (reintroduced in 1.1.10)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 10:13:23 +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
Codeman maintainer 57eefd7aa5 fix(terminal): don't scroll/fling on a sub-threshold tap
The touchmove handler accumulated pixelAccum/velocity and could scrollLines
on every move — including micro-drift below the 8px tap threshold. A jittery
tap (<8px) stayed classified as a tap (didScroll=false, so tap-to-position
fired) yet still left a non-zero velocity, which touchend turned into a
momentum fling. Result: one tap both positioned the cursor and scrolled.

Gate the scroll/velocity accumulation behind didScroll so sub-threshold
movement is inert, matching the handler's stated tap-vs-scroll intent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 18:02:21 +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
Teigen b1c4330680 fix(mobile): add tap threshold to terminal touch handler
touchmove fires on any 1px finger drift, marking didScroll=true and
skipping the tap handler (which refocuses terminal/CJK input). On
iPad's large touch surface and phones with imprecise taps, this makes
terminal tap unreliable — cjkActive gets stuck true, blocking all
input (CJK and paste).

Add 8px TAP_THRESHOLD: finger movement under 8px is still a tap.
Also add touch-action:none on .touch-device .terminal-container
so the browser doesn't consume touch events before our JS handler.
2026-06-17 23:40:21 +08:00
Teigen 47359e4002 fix(iPad): enable terminal touch interaction on all touch devices
touch-action: none was only set inside @media (max-width: 430px),
so iPad's browser consumed touch events before the JS scroll/tap
handler could preventDefault. Move to .touch-device class in
styles.css so it applies at any screen width.
2026-06-17 23:40:21 +08:00
Teigen a8e7d60db4 fix(iPad): show stop button on touch devices 2026-06-17 23:40:21 +08:00
Teigen 8dc70a5f1d fix(mobile): restore /compact button to keyboard accessory bar
Reverts eb83148 which removed the /compact button from both simple
and extended accessory bar modes. Restores double-tap confirmation
and refocus guard for the compact action.
2026-06-17 23:40:04 +08:00
Teigen 4d129086d1 fix(iPad): raise toolbar z-index when case settings popover is open
backdrop-filter on the toolbar creates a stacking context that traps
the popover's z-index (1000) inside the toolbar. CJK input (z-index 52)
in the root stacking context always wins. Use :has() to raise the
toolbar above CJK only while the popover is visible.
2026-06-17 23:40:04 +08:00
Teigen 566c65c3c9 fix(iPad): accessory bar styling, positioning, and paste dialog
Move keyboard accessory bar and paste dialog CSS from mobile.css
(gated behind max-width: 1023px) to styles.css (always loaded).
iPad landscape (≥1024px) was getting unstyled white buttons.

- Add position:fixed via .touch-device class for accessory bar
- Fix dismiss button: gray-blue → blue, matching phone styling
- JS: position accessory bar above keyboard on iPad via direct bottom
- JS: position CJK above accessory bar (bottom: keyboardHeight + 44)
- Clear accessory bar bottom in resetLayout()
2026-06-17 23:40:04 +08:00
Teigen cd7d8c7329 fix(mobile): split CJK keyboard positioning by device size
Phones use translateY(-keyboardOffset) — CSS bottom is relative to layout
viewport and keyboardOffset reliably lifts it above the keyboard (iOS
doesn't auto-scroll the visual viewport for the CJK textarea on phones).

iPad uses direct bottom positioning from keyboard height — translateY
broke because iOS auto-scrolls the visual viewport when the CJK textarea
receives focus, making keyboardOffset approach 0.
2026-06-17 23:40:04 +08:00
Teigen c55af9ec39 fix(iPad): CJK input positioning, paste dialog, and voice dictation duplication
Three iPad-specific issues fixed:

1. CJK input hidden behind keyboard: updateLayoutForKeyboard() gate changed
   from screen-size to touch-device detection. On iPad, CJK textarea (always
   position:fixed) gets bottom offset computed from keyboard HEIGHT directly
   instead of keyboardOffset (which depends on visualViewport.offsetTop that
   iOS adjusts when the CJK textarea receives focus). Toolbar/accessory bar
   transforms remain phone-only (they're normal-flow on iPad).

2. Paste dialog invisible on iPad: paste overlay CSS was inside
   @media (max-width: 430px) phone breakpoint — iPad (≥768px) had no styling.
   Extracted to universal section alongside keyboard accessory bar styles.

3. Voice dictation character duplication (Doubao/third-party IME):
   iOS voice dictation does NOT fire composition events (WebKit Bug 261764).
   Text arrives as bare input events; refinement is a delete→reinsert cycle.
   Rewrote CJK input handler with two-tier debounce:
   - Keyboard typing (no delete/replacement events): 150ms debounce
   - Dictation mode (deleteContentBackward or insertReplacementText detected):
     1500ms debounce, persists 3s to cover multi-word dictation
   - Composition path (compositionend): immediate flush, unchanged
   - Keydown singles/Enter/Esc/Ctrl: immediate, unchanged
   Also: keep cjkActive=true on blur while CJK is visible (prevents xterm
   from processing duplicate input when iOS dictation UI steals focus);
   keydown single-char sends tracked via timestamp to suppress the echo
   input event that third-party IMEs fire despite preventDefault.
2026-06-17 23:40:04 +08:00
Teigen 1a54217bfb fix(mobile): don't clear textarea during compositionstart
Programmatic _textarea.value = '' during compositionstart cancels the
active IME composition on iOS Safari, breaking Chinese character input.
The phantom (U+200B) is invisible and _strip() already removes it
before sending to PTY — no need to clear it manually.
2026-06-17 23:40:04 +08:00
Teigen 70742d400a fix(mobile): restore real-time CJK input and terminal tap interaction
Root cause: the mobile-composer mode (02fa3f3) routed CJK text through
local-echo buffering, which accumulated characters until Enter instead
of sending each composed word to the PTY immediately. Additionally,
xtermFocusRedirect hijacked all terminal taps, preventing cursor
positioning and scroll interaction.

Changes:
- Remove mobile-composer accumulation mode from input-cjk.js — all
  platforms now use the same immediate-flush path (compositionend →
  flush → PTY)
- Bypass local-echo buffering in _handleCjkInput (terminal-ui.js) —
  the CJK textarea already provides visual feedback
- Remove xtermFocusRedirect so terminal taps work normally again
- Reduce CJK textarea height (34px min, 6px padding) for less
  screen intrusion
- Paste dialog now sends Enter after text so pasted content submits
- Hide CJK textarea on welcome screen (no active session)
- Add Opus 4.6 model options to selector
2026-06-17 23:40:04 +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 f7814ad364 feat(ui): distinct colors for welcome action buttons on Daylight Blue (v1.1.8)
On the default daylight-blue skin the three welcome buttons all read blue.
Give each its own identity: Run Claude Code keeps the blue accent, Cloudflare
Tunnel takes Cloudflare brand orange, Run OpenCode takes emerald green (with
matching hover/active states + dark ink for contrast). Scoped to daylight-blue
only; daylight-green and OG unchanged. Verified in-browser (blue/orange/green).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 18:22:33 +02:00
Codeman maintainer 29ffc62536 fix(ultracode): pop floating windows on fresh devices loading mid-run (v1.1.6)
Re-run syncAllUltracodeFloatingWindows() after server settings load so a
first-time device whose getLightState run snapshot arrives before the async
settings fetch resolves still pops an already-active run's window immediately,
instead of waiting for the next ~10s watcher tick. Also fixes a stale
@fileoverview comment that named the wrong gating setting.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 14:27:10 +02:00
Codeman maintainer 4cb3a4aac8 fix(ultracode): (x) Close fully hides the Ultracode Agents panel
closeUltracodeAgentsPanel() only removed `open`, leaving the drawer in its
collapsed peek state (header strip still visible) — so (x) looked like a no-op.
Now also adds `hidden` (display:none), mirroring closeSubagentsPanel; does NOT
flip showUltracodeAgents (that gates the watcher + floating windows). Verified in
a real browser (post-close computed display:none). Bumps 1.1.4 -> 1.1.5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:46:53 +02:00
Codeman maintainer d16bf34e34 feat(ultracode): floating run windows with tab connector lines + dedicated toggle
Auto-popping draggable window per active ultracode/Workflow run, connected by a
glowing line to its originating session tab (resolved via claudeSessionId ===
sessionUuid). Mirrors the live agent grid; auto-closes after a run finishes;
dismissals are remembered. Additional to the existing docked panel.

New "Ultracode Floating Windows" setting (default OFF), independent of the
"Ultracode Agents" panel toggle; either toggle starts the workflow-run watcher.

Also bumps version to 1.1.3 and brings CLAUDE.md up to date for the ultracode
subsystem.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:33:54 +02:00
Codeman maintainer 6ab6bbbbd4 feat(ultracode): Phase 4 — click an agent card to open its live transcript
Each workflow agent card with an agentId is now clickable and opens that agent's
live transcript in a popup, reusing the existing GET /api/subagents/:agentId/
transcript route. The workflow agent's agentId is byte-identical to the
agent-<id>.jsonl stem that subagent-watcher already tracks (via w16's
watchWorkflowDirs), so this is a pure client-side join — ZERO subagent-watcher
edits.

Graceful degradation: 'start' (queued) agents have no agentId yet and stay
non-clickable; an aged-out/untracked agent (subagent-watcher's 4h startup window,
or tracking disabled) returns an empty transcript and shows a friendly note
instead of an empty popup.

Verified on a live isolated server: the subagent transcript route serves a
workflow agent's transcript (150 entries) and the runId's agents[] carries the
matching agentId; Playwright confirmed clicking a card opens the transcript popup
with no console errors. frontend-syntax / public-assets / CSS-parse clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 10:49:22 +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 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 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 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) 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) 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
Claude (Codeman maintainer) 787e5e2a03 feat(attachments): make the header attachments button opt-in (default OFF)
The COD-39 attachments button was hard-visible in the header — first on
mobile, then (after the mobile-only hide) still on desktop. Make it a
proper opt-in App Settings → Display toggle ("Attachments Button"),
default OFF everywhere, mirroring the Response Viewer button:

- index.html: button ships with the `btn-attachments-history--hidden`
  marker; new settings checkbox #appSettingsShowAttachmentsButton.
- styles.css: base `display:inline-flex !important` + a more-specific
  `--hidden` rule (same pattern as the response viewer).
- settings-ui.js: load/save/getDefaultSettings(false) + a live toggle in
  applyHeaderVisibilitySettings. Per-device and NON-leaking — added to
  displayKeys AND stripped from the server payload, so enabling it on
  desktop never makes it appear on mobile (or any other device). No
  server-side render step (purely client display, like the eye button).
- mobile.css: dropped the now-redundant phone-only hide — the opt-in
  marker hides it everywhere by default; the per-device toggle governs
  both desktop and phone.

Tests updated: the CI static guard drops btn-attachments-history from the
phone-hidden lock (it's opt-in now, excluded from the default-visible
enumeration — the guard still gates any NEW default-visible button); the
real-browser E2E now asserts default-hidden on a desktop-class viewport
and visible after enabling the setting.

Verified on a real desktop browser: hidden by default, the settings
toggle exists, enabling it shows the button. tsc + frontend-syntax +
prettier + public-asset checks + both test suites green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 21:21:06 +02:00
Claude (Codeman maintainer) e738c776c1 fix(settings): slim the Skin picker select to match its row
The skin picker inherited .form-select's 0.8rem font + 0.5rem vertical
padding, rendering bigger and taller than the settings row it sits in
(0.75rem / 0.45rem). The daylight skins' Manrope font exaggerated it,
so "Daylight Blue" looked oversized and the field too thick. Scope a
0.75rem font + 0.3rem vertical padding to .settings-item-skin .form-select
so the field text matches the row label.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 21:08:18 +02:00
Claude (Codeman maintainer) e10f0dabdb fix(mobile): hide attachments-history button on phones + regression guards
The COD-39 attachment-history header button was visible on the cramped
phone header. Hide it on phones alongside the settings gear and lifecycle
log (the mobile header is intentionally minimal — those controls live in
the toolbar). One-line addition to the existing @media (max-width: 430px)
display:none block in mobile.css.

This is the second time a header control leaked onto mobile (the
plan-usage chip was the first), so add two regression guards:

- test/mobile-header-buttons-policy.test.ts — a pure static analysis of
  index.html + mobile.css (no browser), so it runs in the normal CI sweep
  (the test/mobile/** Playwright suite is EXCLUDED from CI and never gated
  this). It enumerates every default-visible header button and fails when
  one has no phone-visibility decision — either a mobile.css hide rule or
  an explicit MOBILE_VISIBLE_ALLOWLIST entry. A new header button now
  forces that decision. Verified it fails on the pre-fix state and passes
  after.
- test/mobile/header-buttons.test.ts — real-browser E2E in the mobile
  suite: asserts the attachments/settings/lifecycle buttons are hidden on
  an emulated iPhone 14 Pro and the attachments button is visible on a
  desktop-class tablet.

tsc + lint + prettier + both new tests green. Only CSS + tests changed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 20:45:23 +02:00
Claude (Codeman maintainer) 661c89cefd fix(plan-usage): make the usage chip per-device, not synced
The plan-usage header chip (5h/7d %) was a SYNCED setting, so enabling
it on desktop turned it on for mobile too — even though the user never
enabled it there. Make the chip's DISPLAY purely per-device (default
OFF) like the response viewer / skin, while keeping telemetry COLLECTION
server-side.

Three leak sources fixed:
- server.ts renderIndexHtml force-revealed the chip from the synced
  value (pre-paint), pushing the desktop choice onto every device.
  Removed — the chip now ships hidden and the client reveals it
  per-device via applyHeaderVisibilitySettings.
- settings-ui.js load-merge let the server value win, writing desktop's
  `true` into the (separate) mobile settings blob. showPlanUsageLimits
  is now a displayKey AND is dropped from the server payload on load, so
  a stale server value is never seeded into a device that didn't enable
  it. It's also stripped from the save payload so a mobile "off" can't
  clobber the server.
- Collection was gated on the same synced flag. Decoupled via a new
  `statusLineTelemetry` ACTION field (schema + system-routes): sent on
  ENABLE only and never persisted, so the exporter is injected when a
  device turns the chip on but is never yanked when another device has
  it off (it's shared across sibling sessions). Session-create already
  reads the per-device blob, so that path was already correct.

One-time migration clears a stale synced `true` from the mobile blob so
existing mobile installs default to OFF without a manual toggle.

Verified end-to-end on an isolated server: with showPlanUsageLimits=true
persisted, the rendered HTML ships the chip hidden; a fresh browser
context (mobile case) keeps it hidden while a context that explicitly
enabled it shows it; the PUT accepts statusLineTelemetry and does not
persist it. tsc + frontend-syntax + system-routes/index tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 20:15:39 +02:00
Ark0N a122e867ef Merge PR #122: restore response-viewer eye button on mobile
Remove the dead mobile-collapsed header tray that hid the entire header-right cluster (incl. the opt-in response-viewer eye) on phones/tablets, and update the mobile test to assert inline reachability. Eye stays hidden by default (showResponseViewer).
2026-06-14 19:46:11 +02:00
Aamer Akhter ea53916adc Replace markdown denylist sanitizer with vendored DOMPurify (mXSS hardening)
The previous _sanitizeHtml was a denylist over agent/transcript markdown
rendered via innerHTML; it missed style attributes and the svg/math mXSS
namespaces — e.g. <svg><style><img src=x onerror=alert(1)></style></svg>
re-serialized into a live <img onerror>.

Vendor DOMPurify 3.4.8 (allowlist) following the existing marked.min.js
vendor pattern (same-origin, CSP script-src 'self'; not in package.json so
no lockfile drift). New sanitize-html.js wires a hardened allowlist config
(FORBID style/svg/math/script/iframe/object/embed/form; no data attrs);
app.js _sanitizeHtml delegates to it with a fail-closed escape-all fallback.
index.html loads dompurify -> sanitize-html -> app.js (defer); build.mjs
minifies + content-hashes sanitize-html.js.

Test: test/markdown-sanitizer.test.ts (jsdom, real shipping artifacts) —
mXSS payloads neutralized + legit markdown preserved.
2026-06-14 12:33:48 -04:00
Claude (Codeman maintainer) 1a363a3e62 fix(attachments): address review findings on attachment history drawer (#121)
Follow-up fixes applied during review of PR #121 (all confirmed minor/nit;
no blockers). Security posture verified sound (externalPath never leaves
toState()/the list route; re-registration runs the guard).

- fix(recovery): restoreAttachmentHistory now skips malformed/legacy saved
  items (null, non-object, missing source/fileName) instead of throwing inside
  the Session constructor — a corrupt __attachmentHistory entry could otherwise
  abort the entire mux-recovery loop. (P1)
- fix(routes): the attachment-list route degrades a single failing entry to
  {missing:true} instead of failing the whole drawer. (INT-4)
- fix(ui): give the attachments header button a positioning context so the
  unread badge anchors to the icon, not the header bar. (F1/CSS-1)
- fix(ui): cancel the debounced history refresh on drawer close and guard it
  against a stale session/closed drawer. (F3)
- fix(ui): re-show ("Card") of a detected item now uses the item's own
  timestamp so the cardId is stable — focuses the existing card instead of
  stacking duplicates. (F4)
- fix(ui): Escape now closes the drawer, matching every other panel. (UX-1)
- fix(ui): badge shows "99+" past 99 (was an inconsistent 100/99 cap). (BADGE-1)
- style: drop the duplicate @keyframes notif-badge-pulse (dead CSS). (INT-1/CSS-3)
- style: empty-state used three undefined CSS custom properties
  (--text-primary/--border-color/--bg-tertiary) → use the defined
  --text/--border-light/--bg-input tokens. (CSS-2)
- test: add constructor restore round-trip + malformed-item resilience tests.

Deferred (noted for author): broadcasting the full 100-item history in every
session-state SSE event (payload bloat), "unread" badge semantics, making the
header button opt-in, and app.inject route tests for the two new endpoints.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 09:30:01 +02:00
Aamer Akhter 577b6d7384 COD-39 attachment history drawer
Stacks on COD-38: accumulates a per-session attachment history and exposes it
through a slide-in drawer with an unread badge, so attachments stay reachable
after their cards are dismissed.

Backend:
- session-attachment-history: history state — dedupe by source path / relative
  path, newest-first, 100-item cap, and externalPath sanitization (the absolute
  host path is server-private and never leaves toState()).
- session.ts: _attachmentHistory + getter (sanitized) / upsert / restore /
  getAttachmentHistoryForPersist; restored from saved state in the constructor.
- file-routes: GET /attachments (list — resolves each entry to live metadata +
  routes; external entries are re-registered) and GET /attachments/:id
  (metadata poll). The by-id route guards via the registry's TOCTOU-safe
  resolveServableAttachmentPath.
- server.ts: detected/registered attachments upsert into history and persist;
  the private (externalPath-bearing) history rides on disk under
  __attachmentHistory, separate from the sanitized public copy, and is restored
  on mux-session recovery.
- types/session.ts: SessionAttachmentHistoryItem + SessionState.attachmentHistory.

Frontend:
- panels-ui: the drawer (lazy-built), unread badge, list render with per-item
  preview/download/open/"Card" (reshow) actions, and live refresh of the open
  drawer on new detections.
- app.js: history state + per-session badge/cleanup wiring.
- index.html / styles.css / mobile.css: header button + badge and the drawer.

Verified: tsc / eslint / prettier / frontend-syntax / public-assets clean; new
history-module unit tests pass; full test:ci green (2866 passed); badge, drawer
open/render/reshow/close verified in-browser.
2026-06-14 09:05:17 +02:00
Claude (Codeman maintainer) 5eacb1cf03 Merge PR #120: document attachment previews + thumbnails (COD-38)
Adds attachment cards with first-page thumbnails and inline document
previews (PDF/Office via pdftoppm + LibreOffice), plus review hardening
(converter concurrency limiter, bounded preview cache, fixed detected-doc
preview routing). See PR #120.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

# Conflicts:
#	src/web/public/styles.css
2026-06-14 08:49:36 +02:00
Claude (Codeman maintainer) 5fbe451c26 fix(attachments): harden document preview/thumbnail path (review of #120)
Follow-up hardening applied during review of PR #120, addressing the
adversarial multi-agent findings:

- fix(preview): render auto-detected (workspace, unregistered) DOCX/PPTX via
  the file-preview route and PDFs via file-raw in openFilePreview. Previously
  the Preview button fell through to file-content, dumping the binary Office/PDF
  bytes as mojibake, and the new file-preview route was unreachable dead code.
  (MAJOR: file-preview-route-unreachable-detected-office)

- perf(convert): add a global converter-concurrency limiter
  (document-conversion-limiter.ts) wrapping every pdftoppm / soffice /
  powershell spawn, so N simultaneous preview/thumbnail requests can no longer
  fork unbounded converter processes. Default cap 3, CODEMAN_MAX_DOCUMENT_CONVERSIONS.
  (MAJOR: no-converter-concurrency-limit)

- fix(cache): bound the converted-PDF disk cache with LRU-by-mtime eviction
  (pruneDocumentPreviewCache, default 100 files, CODEMAN_MAX_PREVIEW_CACHE_FILES),
  run after each successful conversion. Was unbounded.
  (MAJOR/MINOR: preview-cache-unbounded-disk-growth)

Tests: document-conversion-limiter.test.ts, document-preview-cache-eviction.test.ts,
and route coverage for the four new endpoints in
routes/file-routes-preview-thumbnail.test.ts (closes the missing-route-test gap).
Verified end-to-end against real pdftoppm (thumbnail render + concurrency cap).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 08:43:57 +02:00