* fix: wire MCP config into CLI passthrough * fix: address passthrough MCP review feedback * fix: fail fast when passthrough MCP port is missing * fix: track passthrough MCP config with nil metadata
13 KiB
status, created, owner, issue, needs-upgrade
| status | created | owner | issue | needs-upgrade |
|---|---|---|---|---|
| draft | 2026-05-16 | cfl | https://github.com/kdlbs/kandev/issues/906 | true |
CLI-Mode Task Parity (Kanban)
Why
Anthropic has announced that agent-SDK / claude -p usage will draw from a paid API budget, while the interactive claude CLI continues to draw from the user's Pro/Max subscription quota. Today kandev drives Claude almost exclusively through the ACP bridge (@agentclientprotocol/claude-agent-acp), which is an SDK-mode integration. After the change, every kandev task run by a subscription user would burn API dollars they do not have.
Kandev already supports a per-profile CLI Passthrough mode that launches the agent CLI under a PTY (apps/backend/internal/agentctl/server/process/interactive_runner.go). Users who enable it keep their subscription billing — but the experience is bare:
- The task-create dialog disables the prompt textarea when a CLI agent is selected (legacy assumption: "you'll type your prompt directly into the terminal"). So the user can't even attach a description to a CLI-mode task.
- The task description that kandev would have sent through ACP is not delivered to the CLI. The user has to retype it.
- The chat compose box on a CLI-mode session does nothing — there is no path from kandev's UI into the agent's stdin for follow-up messages.
This spec brings CLI-passthrough mode up to feature parity with ACP for the kanban task-execution surface. Office (autonomous) mode is explicitly deferred.
What
Prompt allowed at task creation in CLI mode
The task-create dialog's prompt textarea is enabled when a CLI/passthrough-capable agent is selected. Users can write the prompt the same way they would for ACP. The control flow that today gates this off when cli_passthrough is true is removed.
Task-prompt injection (idle-based, agent-agnostic)
When a passthrough session starts for a task that has a description:
- Kandev launches the PTY as today.
- After the existing idle detector in
InteractiveRunnerfires for the first time (i.e. the CLI has stopped emitting output and is presumed to be at its input prompt), kandev writes the task description plus a configurable submit sequence to the PTY's stdin. - Auto-injection is opt-in per agent via a new
PassthroughConfig.AutoInjectPromptflag (default false). Agents that already usePromptFlag(headless one-shot mode) are unaffected — their prompt is already on the CLI before launch.
No per-agent pattern matchers. The existing idle window is the only readiness signal. If an agent's CLI is unusual enough that an idle window misfires (writes a banner, then waits 5 seconds, then prompts), we make the idle window per-agent-configurable in PassthroughConfig (already exists as IdleTimeout). No new detection machinery.
For the Claude case: claude_acp.go sets AutoInjectPrompt: true, DisableBracketedPaste: true (Claude Code already enables bracketed-paste mode in its Ink TUI — injecting ESC[200~…ESC[201~ delimiters breaks the prompt), and SubmitViaBackslashEnter: true (PTY writes: prompt, then \, then \r per Claude terminal docs). Ink may still treat programmatic Enter as newline only (anthropics/claude-code#15553) — if auto-submit fails, the user confirms with Enter. Other passthrough-capable agents use bracketed-paste delimiters for multi-line prompts and stay default-off for auto-inject unless configured.
Follow-up prompts via PTY stdin (backend route landed; UI surface deferred)
The orchestrator's PromptTask handler branches on IsPassthroughSession(sessionID) and writes the prompt text + SubmitSequence to the agent's PTY stdin instead of sending over ACP. This is the same path used by auto-injection and is reachable today from any caller that hits agent.prompt for a passthrough session.
UI gap (deferred): kandev's chat compose box is rendered by ChatInputArea, which is replaced entirely by PassthroughTerminal when session.is_passthrough is true. There is no compose surface in passthrough mode today. Users send follow-ups by typing directly into the terminal (xterm forwards each keystroke to the PTY) — which works perfectly for the Claude CLI use case. A dedicated kandev compose box that drives the PTY (without replacing the terminal) is a follow-up.
Kandev MCP tools are wired for CLI mode
When a passthrough agent supports an MCP config flag, kandev generates a per-session MCP config file that points at the task's local agentctl MCP endpoint and launches the CLI with that config. The generated config exposes the same kandev tool server used by ACP sessions, scoped by the already-known KANDEV_TASK_ID / KANDEV_SESSION_ID context.
For the Claude case, the passthrough command includes --mcp-config <generated-file> with an mcpServers.kandev HTTP entry for the local agentctl /mcp endpoint.
Stop sends Ctrl-C (backend route landed; UI surface deferred)
The orchestrator's CancelAgent handler branches on IsPassthroughSession(sessionID) and writes \x03 to the PTY's stdin instead of sending an ACP cancel. DB reconciliation still runs so the UI unsticks regardless of the write outcome.
UI gap (deferred): Same root cause as the compose-box gap — ChatInputArea (which hosts the Stop button) is not rendered in passthrough mode. Users today press Ctrl-C directly in the xterm terminal, which travels through the same PTY stdin path. A dedicated Stop button overlay in PassthroughTerminal is a follow-up.
Scenarios
CLI-mode task can have a prompt
- GIVEN a Claude profile with
cli_passthrough: true - WHEN the user opens the task-create dialog and types in the prompt field
- THEN the prompt textarea accepts input (today it is disabled / hidden)
Prompt injection on fresh start
- GIVEN a CLI-mode task whose agent has
AutoInjectPrompt: trueand a non-empty description - WHEN the task starts
- THEN the PTY launches, the idle detector fires once after the CLI settles, and the task description is written to stdin followed by
SubmitSequence - AND the description appears in the terminal output
Resume does NOT re-inject
- GIVEN a CLI-mode task whose PTY exited (backend restart, crash) and is resumed via
--resume - WHEN the user reopens the terminal and the session resumes
- THEN no auto-injection occurs (the conversation is already in the agent's history)
Fresh-start fallback DOES inject
- GIVEN a CLI-mode task whose resume launch fast-failed and the fresh-start fallback ran (existing
attemptResumeFallbackpath) - THEN auto-injection runs against the new fresh session — because that fallback is functionally a new conversation
Follow-up prompt route is in place (UI surface follow-up)
- GIVEN a CLI-mode task running
- WHEN any caller invokes
PromptTaskfor the session (today: future kandev compose surface, integration tests) - THEN the text +
SubmitSequenceis written to the PTY - AND no ACP prompt is sent
Kandev MCP config is present for Claude passthrough
- GIVEN a Claude profile with
cli_passthrough: true - WHEN kandev starts or fresh-starts its passthrough command
- THEN the argv includes
--mcp-config <generated-file> - AND the generated file contains
mcpServers.kandevpointing at the local agentctl/mcpendpoint
Stop / cancel route is in place (UI surface follow-up)
- GIVEN a CLI-mode task running
- WHEN any caller invokes
CancelAgentfor the session - THEN
\x03is written to the PTY - AND DB reconciliation still completes so the session unsticks
Agent without AutoInjectPrompt
- GIVEN a passthrough-capable agent with
AutoInjectPrompt: false - WHEN a task starts
- THEN no stdin write happens (today's behavior preserved); the user pastes their prompt manually
Kandev toolbar above the passthrough terminal
Where it appears
PassthroughToolbar (apps/web/components/task/passthrough-toolbar.tsx) wraps PassthroughTerminal with the same kandev controls that ACP sessions get from ChatStatusBar + ChatInputArea. It is mounted:
- Full-page task view:
dockview-shared.tsx(DockviewSharedContent) renders it for passthrough sessions in place of the chat input area; the dockview desktop layout intask-center-panel.tsxuses the same shared content. - Kanban Preview tab:
preview-session-tabs.tsxrendersPassthroughComposerdirectly (compose-only, no terminal wrap) for passthrough sessions in the side-peek surface.
What it surfaces
The status row at the bottom of PassthroughToolbar contains, left to right:
PRStatusChip— PR open/merged/draft indicator for the task.PRMergedBanner— banner shown when the PR is merged.passthrough-proceed-next-stepbutton — moves the task to the next workflow step. Only shown whennextStepNameis non-null and the agent is not busy (sessionStateis notRUNNINGorSTARTING).ChatToggleButton— toggles thePassthroughComposeropen and closed. Showsvariant="default"when the composer is open or when there are pending review comments; shows a numeric chip (data-testid="passthrough-pending-count") when collapsed with pending comments.- Stop button (
data-testid="passthrough-stop") — disabled when the session is notRUNNINGorSTARTING.
Default focus contract
The terminal (PassthroughTerminal) renders above the status row and fills the remaining height. The compose box is collapsed by default so xterm retains keyboard focus for raw PTY interaction. The user opts in to the kandev composer explicitly via the Chat button.
How the composer formats and sends pending review comments
When PassthroughComposer submits:
usePendingDiffCommentsByFile(sessionId)provides comments grouped by file path.- Non-empty
pendingCommentsare formatted viaformatReviewCommentsAsMarkdown(produces### Review Comments\n…) and prepended to the typed text. - The combined string is sent via
client.request("message.add", { task_id, session_id, content }, 10_000). markCommentsSent(ids)is called only on WS success — a failed send leaves both the composer open (typed text preserved for retry) and the comments pending.- A
PendingCommentsBanner(data-testid="passthrough-pending-comments-banner") renders inside the open composer whenpendingCount > 0.
How Stop maps to Ctrl-C
Clicking the Stop button calls client.request("agent.cancel", { session_id }, 10_000) over WS. The WS handler calls Service.CancelAgent → agentManager.CancelAgentBySessionID in apps/backend/internal/agent/runtime/lifecycle/manager_interaction.go, which branches on execution.PassthroughProcessID != "" and writes \x03 to PTY stdin. DB reconciliation completes regardless of the write outcome so the UI unsticks.
Not included
TodoIndicator— ACP-only (requires message stream); not rendered inPassthroughToolbar.- Plan-comments and PR-feedback attachment — same pattern as diff comments if needed in future; deferred.
- Per-message model/mode picker — ACP-only feature; not applicable to PTY sessions.
Out of scope
- Office / autonomous-agent CLI mode — explicitly deferred. Office launches stay on ACP for now.
- Parsing PTY output into
task_messages(transcriber). The terminal panel shows live output; the chat transcript only shows user-sent text in CLI mode. - Billing-type / subscription-quota badge or DTO surface. That belongs to
subscription-usage.md. - Adding passthrough support to agents that don't have it (
Supported: false). - Migrating away from the current ACP bridge.
- Headless
-pmode for Claude (drains API credit; we deliberately do not use it).
Follow-ups
- Compose box in passthrough mode. Surface a dedicated kandev compose input (alongside the PTY terminal) that writes to PTY stdin via the now-wired backend route. Today users type into xterm directly, which works but is inconsistent with kandev's ACP UX.
- Stop button overlay in
PassthroughTerminal. Small affordance that calls the existing cancel route. Today users press Ctrl-C inside xterm directly. - AutoInjectPrompt on other passthrough agents (Codex CLI, OpenCode TUI, etc.) — only Claude is opted in for v1. Adding others requires verifying their submit sequence and idle behavior.
Open questions
- Submit sequence for Claude TUI:
"\r"is the expected default. If the TUI swallows the first\ras a focus-acquisition event we may need a tiny pre-write delay or"\r\n". Resolve empirically during dogfooding; the field is configurable per agent. - Idle window for Claude TUI: today's default (3s) should be enough — the TUI prints its banner and then sits at the prompt. Tune the per-agent
IdleTimeoutonly if real sessions misfire.