Three things the terminal was getting wrong. **A program that grabs the mouse and dies used to freeze the tab.** A TUI sets DECSET ?1000/?1002/?1003; if it exits without resetting them, xterm keeps routing clicks, drags and — under ?1003 — every pointer *move* to the PTY. Text selection dies and escape bytes flood the prompt. The only exit was closing the tab. `TerminalView` now reconciles a flag against `term.modes.mouseTrackingMode` in the `term.write()` callback — the mode only changes because the container printed a sequence, so one check per write catches every transition with no polling — and `Ctrl+Shift+X` or a status-bar button writes the resets back through `term.write`, never `sendInput`: the reset belongs to xterm's parser, and a still-live TUI told about it would just re-grab on its next repaint. The control is in the status bar deliberately. Mouse tracking is the *normal* state of htop, vim, lazygit and Claude Code, so a badge over the terminal would be on screen for the whole life of those programs and would swallow clicks aimed at their own top-right corner. `macOptionClickForcesSelection` is also on now: xterm's force-select is Shift everywhere except macOS, where it is Option and is gated behind that option, which defaults to false — so until now Mac users had no way to select text while a program held the mouse. **"Following" and "Jump to Current" are gone.** Claude Code draws on the alternate screen, which has no scrollback, so `viewportY` always equalled `baseY` and neither control could do anything. They did still work in bash tabs; xterm's native follow covers that, and the per-write `scrollToBottom()` went with them because it fought exactly that. What remains, on activate and after a refit, now samples `viewportY >= baseY` *before* the fit, so opening the Notes dock no longer yanks a reader to the tail. **`claude update` runs before every Claude session, not just at container start.** Containers here stop/start and often just keep running, so a long-lived one never re-checked. Both copies take the same flock: the entrypoint prints "container ready" only after its own update finishes, so opening a tab immediately would otherwise run two updaters against the same ~/.claude/bin, with `|| echo` hiding a half-written install one line before `exec claude` ran it. This turns the non-Bedrock path from a bare argv into a `bash -c` wrapper, so flags and session names are shell-interpolated now and must go through `shell_quote_arg`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0145mQi9NZiCDrznBUEEDE4n
15 KiB
Triple-C Roadmap — Claude Code Feature Parity
Date: 2026-08-09 · Baseline: v0.3.0 · Claude Code reference: 2.1.226
Companion to DESIGN-REVIEW.md, which covers visual design and information architecture. This document covers which Claude Code capabilities Triple-C should surface, and why.
Guiding principle
Triple-C shows state and launches things. Claude Code edits its own config.
Triple-C's built-in MCP server management was removed in this cycle because Claude Code
absorbed the capability natively (claude mcp add/list/remove, .mcp.json, /mcp).
Hooks, skills, agents, plugins, output styles, and statusline are the same species: files
under .claude/ with first-class Claude Code TUIs. Building GUI form editors for them
means losing the same race again.
What Claude Code cannot do is what Triple-C uniquely owns: the container boundary and what persists behind it — the config volume, workspace mounts, lifecycle, the bundled scheduler, and the fleet view across many projects.
Current coverage (v0.3.0)
Triple-C sets exactly six settings.json keys, plus a sandbox block:
| Key | Surfaced as |
|---|---|
tui |
TUI mode select — unset (Claude Code chooses), default (classic renderer), fullscreen (flicker-free alt-screen). Three distinct states, not two. |
effortLevel |
Effort level select (low/medium/high/xhigh) |
viewMode |
Focus mode toggle, written as "focus". Unset means the user's own verbose setting and sticky /focus choice still apply. |
autoScrollEnabled |
Auto-scroll toggle. Claude Code's default is true, so it is the off state that writes false. |
showThinkingSummaries |
Thinking summaries toggle (Claude Code default false) |
awaySummaryEnabled |
Session recap toggle. Claude Code's recap is on by default, so again it is the off state that writes false. |
sandbox.* |
Sandbox toggle (enabled, enableWeakerNestedSandbox, allowUnsandboxedCommands) |
Every one of those keys is emitted on every start, with a JSON null standing for
"delete this key". ~/.claude/settings.json sits on the config volume and the entrypoint
merges into it, so a key merely omitted when its control goes off left the previous
on-value in place forever.
Plus four env feature flags — CLAUDE_CODE_NO_FLICKER, CLAUDE_CODE_ENABLE_AWAY_SUMMARY,
CLAUDE_CODE_SUBPROCESS_ENV_SCRUB, ENABLE_PROMPT_CACHING_1H — and arbitrary user-set
CLAUDE_CODE_* vars via the Env Vars modal. The four are written on every container
create including their off value, because docker commit bakes a container's env into
the snapshot image: a value written once would otherwise ride that snapshot into every
future container. That also makes them Triple-C's to own, so all four are reserved names
— hand-setting one in the Env Vars modal is skipped with a warning, the same as any other
triple-c.*-managed variable. CLAUDE_CODE_ENABLE_AWAY_SUMMARY is what actually enforces
the recap choice — it takes precedence over awaySummaryEnabled and over the
in-container /config toggle, so turning the control off sends 0 while leaving it on
sends an empty value rather than 1: Triple-C's default must not overrule a /config
choice it never asked about.
Also covered: per-project auth backends (Anthropic OAuth, Bedrock incl. SSO refresh,
Ollama, OpenAI-compatible), user-level CLAUDE.md composition, claude update on every
container start and before every Claude session launches, terminal ergonomics (OAuth URL detection, OSC 52 clipboard, image paste,
file drag-drop, STT), the web terminal, and workspace backup.
Gap analysis
Committed for this cycle
| # | Gap | Today | Plan |
|---|---|---|---|
| 1 | Permission modes | one boolean → --dangerously-skip-permissions |
Four-state control (Plan / Default / Accept Edits / Bypass) → --permission-mode. Verified choices on 2.1.226: acceptEdits, auto, bypassPermissions, manual, dontAsk, plan. |
| 2 | Session resume | none | List sessions from the config volume; [Resume] opens a terminal on claude --resume <id>. |
| 3 | Capability inventory | none | Read-only counts + names for skills / agents / hooks / plugins / commands / native MCP servers. Deep-link to the terminal to manage. |
| 4 | Automation | triple-c-scheduler ships in every container with zero UI |
Task list, cron editor, run-now, logs, notification badges. |
| 5 | Container auth handoff | manual code paste | See "Authentication handoff" below — design decision pending. |
Deliberately skipped
Status line builder · output-styles editor · hook editors · checkpoint/rewind browser · plugin marketplace browser. Each is niche, natively handled by Claude Code's own TUI, or a settings-editor trap. Surface counts and deep-link instead.
Not yet scheduled
- Granular
permissions.allow/ask/denyrules andadditionalDirectories - Sandbox detail settings (
filesystem.allowRead/allowWrite,allowedDomains,excludedCommands) — currently documented for hand-editing viaSANDBOX_INSTRUCTIONS - Project-level
.claude/settings.jsonvs user-level settings hierarchy - A model picker. Note: the only model strings in the app today are stale placeholders
(
anthropic.claude-sonnet-4-20250514-v1:0inAwsSettings.tsxandProjectCard.tsx,qwen3.5:27b,gpt-4o / gemini-pro / etc.). These are free-text placeholders, not dropdowns, but they should be refreshed to current model identifiers regardless. - The container's settings.json merge is shallow (
jq -s '.[0] * .[1]'), so a user-authored nested block such assandbox.filesystem.allowWriteis replaced wholesale on every container start. Worth deepening to*recursive merge.
Authentication handoff
Goal: stop making users hand-copy an auth code into every container.
Constraint discovered during research: claude login's callback server uses an
ephemeral port and its redirect URI is not configurable for the main login flow
(--callback-port and oauth.callbackPort apply to MCP server OAuth only). So a design
that pre-assigns each container a fixed callback port and routes to it cannot work as
stated — there is no fixed port to route.
There is also a known container gotcha: on Linux, Node resolves localhost to IPv6 first,
so the callback server may bind [::1]:PORT only and be unreachable over IPv4
(anthropics/claude-code#44844).
Two viable options:
Option A — long-lived token injection (simple)
claude setup-token (verified present on 2.1.226: "Set up a long-lived authentication
token (requires Claude subscription)") returns a ~1-year OAuth token. Triple-C runs it in
a running container, stores the token in the OS keychain via the existing secure.rs, and
injects CLAUDE_CODE_OAUTH_TOKEN into every container on the Anthropic backend.
Correction to an earlier assumption in this document. setup-token does not start a
loopback callback listener, so it does not need the Auth Bridge. Verified by running it
under a pty: its redirect_uri is Anthropic-hosted
(https://platform.claude.com/oauth/code/callback), the user copies a code off that page,
and the CLI blocks at a Paste code here if prompted > prompt on stdin. A stdin path
is therefore mandatory — the flow cannot complete without one.
- No routing, no ports, no proxy.
- One auth event covers every project.
- Cost: small. Reuses existing keychain and env-injection plumbing.
- Limits: token is subscription-scoped and expires annually; per the docs a
setup-tokentoken cannot drive Remote Control sessions or claude.ai connector fetches.
Change detection uses a random rotation id in the triple-c.claude-token-version
label, not a hash of the token. Labels are readable by anything that can run
docker inspect, so a hash would be an offline verification oracle — given a candidate
token you could confirm it. A presence boolean would instead miss rotations and silently
leave containers on a stale token.
Option B — the Auth Bridge (general loopback-callback bridge)
Option A only solves Claude Code. The same problem affects every CLI that authenticates by
starting a temporary loopback listener and opening a browser at a URL that redirects back
to it — Concourse fly login (random loopback port serving /auth/callback),
aws sso login, and many others. Inside a container the host browser cannot reach that
listener, so login stalls.
Because the ports are ephemeral and unconfigurable, nothing can be pre-assigned. The bridge discovers listeners instead:
- While enabled for a running project, poll the container for loopback TCP listeners by
reading
/proc/net/tcpand/proc/net/tcp6overdocker exec— no dependency onss/netstat/lsof, which aren't guaranteed in the image. - For each newly-appeared loopback listener, bind the same port on the host's
127.0.0.1(never0.0.0.0— that would expose container internals to the LAN). - Proxy each accepted connection into the container over the Docker API via
socat - TCP:127.0.0.1:<port>(socat already ships in the image), reusing the existing attached-exec streaming indocker/exec.rs. Going through the Docker API rather than a container IP keeps this working on Docker Desktop, where container IPs are not routable from the host. - Fall back to
TCP6:[::1]:<port>when the listener appeared only on IPv6 — on Linux, Node resolveslocalhostto IPv6 first, soclaude loginfrequently binds::1only (anthropics/claude-code#44844). - Tear down when the listener vanishes, the container stops, the bridge is disabled, or the app exits. Ports already covered by the project's explicit port mappings are skipped; host-side conflicts are reported rather than silently swallowed.
Opt-in per project (auth_bridge_enabled, default off), since it makes container-internal
loopback services reachable from the host.
Plan: ship A for Claude Code specifically — it removes the pain for the common case at a fraction of the cost — and B as the general mechanism covering every other CLI. They compose: A means most users never trigger a browser login at all; B catches AWS SSO, Concourse, and anything else that needs a real callback.
Sequencing
Phase 0 — done. Remove MCP (frontend, backend, entrypoint, docs) with a self-healing migration for containers created against the old per-project Docker network.
Phase 1 — foundations. Permission modes end-to-end (including the scheduler bug fix below). Read-only introspection backend: sessions, capabilities, scheduler.
Phase 2 — Tier-1 polish. Focus rings, contrast fixes, real buttons, inline start/stop
progress, status labels, onboarding welcome screen, shared accessible <Modal>.
Phase 3 — Project Home. Move project config out of the sidebar card into a tabbed
main-area view (Overview / Sessions / Automation / Config), dissolving the modal pile and
splitting the 1,257-line ProjectCard.
Phase 4 — authentication handoff. Option A, then evaluate B.
Phase 5 — Library. Global skills/agents/commands with per-project enable, synced into the config volume by the entrypoint. Generalizes the pattern the MCP tab was reaching for.
Bugs found during this review
-
Scheduled tasks ignore the project's permission setting.
container/triple-c-task-runner:69runsclaude -p "$PROMPT" --dangerously-skip-permissionsunconditionally, regardless of the project's Full Permissions toggle. Being fixed as part of Phase 1. -
Docs claim Reset preserves credentials; it does not.
rebuild_project_containercallsremove_project_volumes, which deletes bothtriple-c-home-{id}(holding~/.claude.json) andtriple-c-claude-config-{id}(holding~/.claude). README.md, HOW-TO-USE.md, and CLAUDE.md all still state that OAuth tokens survive a Reset. Pre-existing; not yet corrected. -
An invalid cron expression silently unscheduled every task. Found while adding task creation to the Automation tab, and the most serious bug in this review.
triple-c-schedulernever validated--schedule, andrebuild_crontabregenerates the entire crontab and pipes it tocrontab, which rejects the whole file if any single line is malformed — with the error discarded by2>/dev/null || true. So one bad schedule silently unscheduled every other task in the container, reporting success. Reproduced directly. This mattered because the global CLAUDE.md instructs Claude to use this CLI, so Claude itself could trigger it. Fixed at the root:addnow validates the expression and exits non-zero, andrebuild_crontabreports a rejected crontab instead of swallowing it. The Rustadd_scheduled_taskcommand validates independently. -
Reset was destructive with no confirmation. It deletes both volumes — the login, installed skills, all session transcripts — from a single unconfirmed click, while the comparably destructive Remove already confirmed. Now gated by a dialog that names each loss. Fixed.
-
Cancelling authentication did not cancel. Fixed — see the handoff section above.
-
Stale model placeholders — see "Not yet scheduled" above.
-
Silent save failures. Project config saves on blur; failures went only to
console.error. Fixed in Phase 3 —useProjectSavenow renders a Saved / Saving / Save failed indicator and raises a toast.
Known gaps left by Phase 2–3
- Editing a scheduled task changes its id.
triple-c-schedulerhas noeditsubcommand, and hand-editing its JSON behind its back would desync the crontab, so edit is implemented as add-then-remove. The add runs first, so a rejected edit leaves the original intact. The task gets a new id and its older logs stay under the old one; the editor says so before saving. open_terminal_sessiontakes no command argument. "Resume session" and "Manage in terminal" therefore open a bash tab and type the command after a fixed prompt delay. It works, but it is timing-dependent and will misfire on a slow container start. The fix is acommand: Option<String>parameter on the Tauri command so the exec launches the process directly.- Uptime is observed, not reported.
get_container_inforeturns a status enum with no start time, so Project Home records "running since" when the app sees the transition. A container already running when the app launches shows● Runningwith no elapsed time. Surfacing Docker'sState.StartedAtwould fix it. lucide-reactwas not adopted (DESIGN-REVIEW Tier-1 #9) — no package-registry access in the build environment used for this cycle. The existing inline SVGs and text glyphs remain.- The tab strip stayed in the TopBar rather than moving onto the terminal panel's top edge. DESIGN-REVIEW §A6 asks for the move but its own §B2 layout diagram puts the tabs in the TopBar; the diagram won. Worth revisiting.
Ctrl+Shift+W, notCtrl+W, closes a tab. PlainCtrl+Wis readline'skill-word, used constantly inside the terminal this app is built around; intercepting it globally would break word-erase in every shell.