Commit Graph

11 Commits

Author SHA1 Message Date
nntrivi2001 8ea40260ad feat(ui): terminal, charts and low-contrast text follow the selected theme
- The lane terminal (xterm) takes its palette from the selected DARK theme —
  it stays dark in light mode, as a terminal should — and repaints live when
  the theme changes (`client/src/lib/terminalTheme.ts`).
- Charts on the Dashboard and Workflows pages draw their grid, axes, labels
  and tooltips from theme tokens instead of hardcoded near-black/white, and
  the activity heatmap ramps from the page surface to the accent, so it reads
  on every theme instead of going near-black on light ones.
- Status/brand text that only worked on dark backgrounds
  (`text-*-300/400`) gets a darker light-mode shade (`text-*-700
  dark:text-*-300`) across sessions, conversations, workflows, config and
  settings. Categorical chart colours are kept.
2026-09-24 09:50:44 +07:00
nntrivi2001 f77049c238 feat(run,ui): make the lane console usable — terminal focus, live-exit recovery, auto permission mode, model aliases
Terminal (issue #33):
- `convertEol` is gone from xterm. It rewrote every bare LF as CR+LF, so a
  tmux pane that scrolled (tmux emits a bare LF and keeps drawing at the
  column it was on) had the rest of that draw land at column 0, stamping a
  line's first characters into the left margin of the rows below — the torn
  left edge that only showed up once content scrolled.
- The attach carries the client's real size (`?cols=&rows=`, `sizeFromQuery`
  in `server/websocket.js`, bounded because it reaches a TIOCSWINSZ ioctl).
  `attach()` spawned `tmux attach-session` at a hardcoded 80x24 on every
  attach, and tmux sizes the window to the newest client, so `claude` redrew
  its whole UI at 80 columns until the first `resize` message landed.
- The terminal takes the keyboard on mount, so typing reaches tmux without
  clicking the pane first.
- A pane that ends (double Ctrl+C, `exit`, a crash) reports itself through a
  new `onExit` prop and `LaneConsolePane` runs `newRun()`, so the console
  returns to the setup form instead of leaving a dead pane behind a
  "New run" button.

Pane environment:
- `server/lib/desktop-env.js` forwards the graphical session's DISPLAY /
  WAYLAND_DISPLAY / XAUTHORITY / XDG_RUNTIME_DIR (from
  `systemctl --user show-environment`, this process's own values winning)
  into the pane. The server normally runs as a systemd user service with none
  of them, so `claude` there could not read the clipboard and answered "no
  image found in clipboard" on every image paste.

Run setup:
- `auto` is a permission mode and is now the default everywhere — allowlists,
  the `--permission-mode` fallback, the select, and the pane's initial state.
- The model picker sends the CLI's aliases (`fable`/`opus`/`sonnet`/`haiku`)
  instead of pinned ids. `claude --model` resolves an alias to the latest
  model itself, so a release like Fable 5.1 needs no edit here; pricing is
  unaffected because it keys off the resolved id the transcript reports.
- **Create/update readme file** moved from the Add-lane modal into the lane's
  Tools menu: creating a lane and writing its docs are separate decisions,
  and the second is worth re-running long after the first.
- The Workspace's no-lane state is a real banner; the lane strip (add tile
  included) renders only once a lane exists.

Refs #32, #33.

Tests: npm run test:server (1828 pass), client vitest (558 pass).
2026-09-21 17:03:27 +07:00
nntrivi2001 5c297c6483 fix(run): pin the lane terminal font so Vietnamese glyphs never fall back
xterm left fontFamily unset, so it used the browser/OS default
(courier-new/monospace). That face is missing Vietnamese precomposed
diacritics on many systems, so the browser silently substitutes those
glyphs from another face with a different advance width — corrupting
the cell even under the canvas renderer, which only stops a glyph from
shoving its neighbours, not from overflowing its own cell. Pin the
terminal to the app's own bundled JetBrains Mono and import its
vietnamese subset (previously only latin was loaded).
2026-08-28 13:18:12 +07:00
nntrivi2001 cc4e71c127 fix(run): draw the lane console through the canvas renderer
Scrolling a console left stray characters glued to the left edge of blank
lines. Copying an affected line yields clean text, so the buffer was always
correct — only the painting drifted, which no CSS can correct.

xterm's default DOM renderer aligns each row with a single `letter-spacing`
value. That holds only while every glyph comes from one monospace font, and
TerminalView passed no fontFamily, so xterm used its default
`courier-new, courier, monospace`: Vietnamese diacritics and CJK fall
through to a fallback face whose advance width differs, glyphs shove their
neighbours along, and the row's tail spills onto the row below.

The canvas renderer draws every cell at a fixed position, so a wide glyph
can no longer displace its neighbours. Loaded best-effort — a browser that
cannot hand back a 2D context keeps the DOM renderer rather than losing the
terminal entirely.

Ruled out first, each by replaying the real byte stream into xterm.js and
diffing against `tmux capture-pane`: wrapped Vietnamese at the margin, mixed
CJK, wheel scrolling through copy-mode, and a live 131->95->131 resize all
render byte-identically at matched widths.

client 448/448, tsc clean.
2026-08-28 10:44:17 +07:00
nntrivi2001 ce5eadba89 fix(run): keep the tmux pane exactly as wide as xterm
Scrolling a lane console repainted history with stray characters glued to
the left edge of blank lines. Reproduced against a real tmux attach: a pane
WIDER than xterm makes every over-long row autowrap its tail onto the next
row's left margin, and tmux — believing that row unchanged — never repaints
over it. One column of difference leaves one stray character per wrapped
line, two columns leave two; matched widths render clean.

TerminalView only reported its size on WS open and on the container's
ResizeObserver, so nothing corrected:
- cols moving with no container resize (webfont load, page zoom, scrollbar)
- tmux's `window-size latest` letting a second attach client (another tab,
  `ccam lanes shell`) resize the window out from under this one

Size is now reported from xterm's own onResize, re-asserted when the
terminal regains focus, and never reported as zero — the console collapses
with CSS, and fitting a 0x0 container would resize the real pane to nothing.

client 447/447, tsc clean.
2026-08-28 10:07:12 +07:00
nntrivi2001 937c63361b feat(lanes): widen stage auto-detection, fix terminal UTF-8 decode
Add detect rules for plan/review/gate/tests/intake nodes (Agent subagent
matches, TaskCreate, broader test-runner regex) so more of the pipeline
lights up without an explicit ccam stage call, and fix a review/gate
detect overlap where senior-gate-reviewer also matched the generic
reviewer pattern. Also fix TerminalView's TextDecoder dropping the
`stream: true` flag, which corrupted multi-byte UTF-8 chars split across
WebSocket frames.
2026-08-21 16:20:34 +07:00
nntrivi2001 77ca6e0188 fix(run): restore wheel scrolling in the terminal, suppress only arrow emulation
The previous `attachCustomWheelEventHandler(() => false)` killed scrolling
outright. xterm's `bindMouse` consults that handler in BOTH wheel paths — the
one that emits a real SGR mouse report and the fallback that turns a notch on
an alt-screen buffer into ESC[A/ESC[B — so a blanket false also blocked tmux's
own wheel scrolling (tmux runs with `mouse on`, i.e. tracking is active).

Now the handler returns false only for the exact bad case: no wheel tracking
AND an alternate buffer. `x10` counts as untracked because that protocol
reports button presses only, never the wheel, so xterm takes the emulation
path for it too.
2026-08-20 08:30:43 +07:00
nntrivi2001 8f2dd5358e fix(run): keep Shift+Tab inside the terminal so claude's permission cycle works
xterm emits ESC[Z for Shift+Tab but, unlike plain Tab, never marks the
keyboard event cancelled, so the browser ran its default action and moved
focus out of the terminal. The pane saw only the first press, which made
`shift+tab to cycle` (claude's permission mode) look dead on /run while
working fine in a real terminal.

A custom key handler now calls preventDefault() on that one combo and still
returns true, so xterm's own handling — including the ESC[Z it sends — is
untouched; only the focus-moving default is suppressed. Verified against a
scratch tmux session: writing ESC[Z into the attach pty cycles claude's
status line from `bypass permissions on` to `auto mode on`.

Trade-off: Shift+Tab no longer tab-reverses out of the terminal; click
elsewhere to move focus.
2026-08-19 14:51:21 +07:00
nntrivi2001 c7e934b4b4 fix(run): stop the terminal wheel from typing arrow keys, re-attach dropped sockets
Three separate reasons the Workspace terminal looked unscrollable:

- xterm falls back, on an alt-screen buffer whose program has not enabled
  mouse tracking, to translating each wheel notch into a cursor-key press.
  Inside a `claude` pane that reads as arrow up/down, so scrolling walked the
  prompt history. A custom wheel handler returning false suppresses only that
  emulation branch; real mouse reports come from a separate listener xterm
  registers when the program does request wheel events.
- TerminalView never reconnected, so a server restart left the pane frozen on
  its last painted frame — still live-looking, but swallowing every keystroke
  and mouse report. It now re-attaches 1.5s after an unexpected close and
  stops only on the server's `exit` frame; tmux keeps the session, so the
  re-attach repaints in full.
- tmux repaints the whole pane, so xterm's own scrollback is always empty and
  its viewport scrollbar renders as a groove with no thumb that cannot move.
  Hidden, since scrolling there goes through mouse reports, not the DOM.
2026-08-19 12:02:01 +07:00
nntrivi2001 78fb82b257 fix(run): decode binary WS frames in TerminalView so pty output actually renders
Server streams PTY output as binary WS frames, but the browser's default
binaryType ("blob") handed onmessage a Blob that never matched the
`typeof === "string"` check — every keystroke response was silently
dropped and the terminal stayed blank despite the backend streaming
correctly (verified via a raw ws client against the live tmux session).
2026-08-13 13:54:19 +07:00
nntrivi2001 9b8d9bbe39 feat(run): add TerminalView xterm.js component for the PTY transport
- TerminalView.tsx: xterm.js component with WebSocket attachment to /ws-pty/:runId
- Test: validates WS connection URL, incoming terminal data, and outgoing keystrokes
- Added ResizeObserver stub to test-setup.ts for jsdom environment
2026-08-12 10:19:10 +07:00