feat(cli): interactive terminal tool - #11394
Conversation
The branch base referenced a removed WithInstance.provide helper; align with the renamed provideTestInstance fixture export (matching main) so the package typechecks and the pre-push hook passes.
| targetFps: 30, | ||
| maxFps: 60, | ||
| useMouse: false, | ||
| useMouse: true, // kilocode_change - interactive terminal close and scroll controls |
There was a problem hiding this comment.
[WARNING]: useMouse: true is now enabled for every kilo run interactive session, not only while the terminal dialog is open.
This flips mouse capture on globally for the run renderer (previously false). When the interactive terminal isn't active, mouse events will still be captured by OpenTUI, which can regress native terminal mouse behaviors in the rest of the run footer (text selection, scrollback wheel scrolling). Consider enabling mouse only while the terminal view is mounted, or scope the flag to terminal-active state, so non-terminal run sessions keep the previous no-mouse-capture behavior.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
| description?: string | ||
| } | ||
|
|
||
| export const ShellPermission = Effect.gen(function* () { |
There was a problem hiding this comment.
[SUGGESTION]: Fork-hygiene: ShellPermission adds ~110 lines to a shared upstream file.
This is the largest shared-file change in the PR and restructures upstream bash-permission logic into a new reusable service inside shell.ts. To minimize the upstream diff for future merges, consider whether the ShellPermission service itself could live under src/kilocode/tool/shell-permission.ts (reusing exported primitives from shell.ts), keeping only thin marked hooks here. The current placement is defensible since the helpers (cygpath, collect, argpath, etc.) are private, but it's worth a conscious call since it widens our diff against opencode in a hot shared file.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Files Reviewed (5 files)
Previous Review Summaries (3 snapshots, latest commit 9d9a5d0)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 9d9a5d0)Status: No Issues Found | Recommendation: Merge Files Reviewed (1 file)
Previous review (commit df70221)Status: 1 Issue Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
Files Reviewed (30 files)
Fix these issues in Kilo Cloud Previous review (commit 4e3a4da)Status: 2 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
SUGGESTION
Other Observations (not in diff)Issues found in unchanged code that cannot receive inline comments:
Notes on memory-leak reviewThe custom instruction to scrutinize memory leaks was applied to the new PTY engine (
Files Reviewed (15 of 49)
Reviewed by gpt-5.4-20260305 · Input: 88.4K · Output: 5.8K · Cached: 328.5K Review guidance: REVIEW.md from base branch |
# Conflicts: # bun.lock # packages/opencode/src/cli/cmd/run/footer.ts # packages/opencode/src/cli/cmd/run/footer.view.tsx # packages/opencode/src/cli/cmd/tui/context/sync.tsx # packages/opencode/src/kilocode/tool/registry.ts # packages/opencode/src/session/session.ts # packages/opencode/src/tool/shell.ts # packages/opencode/test/kilocode/task-nesting.test.ts # packages/opencode/test/kilocode/tool-registry-indexing.test.ts # packages/opencode/test/session/prompt.test.ts
| for (const [sessionID, list] of Object.entries(next)) { | ||
| const items = new Map((draft[sessionID] ?? []).map((item) => [item.info.id, item])) | ||
| for (const item of list) { | ||
| if (!items.has(item.info.id)) items.set(item.info.id, item) |
There was a problem hiding this comment.
[WARNING]: Reconnect bootstrap keeps stale terminal snapshots
interactiveTerminal.list() is the authoritative server snapshot after reconnect, but this merge only inserts missing IDs and never replaces or removes existing entries. If the client misses an interactive_terminal.deleted or interactive_terminal.data event while disconnected, the stale local snapshot survives every bootstrap: closed terminals still occupy sync.data.interactive_terminal, and existing terminals never refresh their output/cursor from the server. Because Session() hides the normal prompt whenever any terminal snapshot remains active, a missed delete can strand the user in a dead terminal overlay until a full reload.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
feat(cli): interactive terminal tool
Issue
No tracking issue. This is a self-contained feature addition to the CLI; opening it directly for review.
Fixes #
Context
The agent currently has no way to hand the user direct keyboard control of a running process. Commands that genuinely need a TTY or private human input —
gh auth login,vercel login, SSH/GPG passphrase and 2FA prompts, interactive scaffolders likenpm create, and REPLs likepython3/psql— either hang thebashtool waiting on a human who can't reach the process, or have to be awkwardly worked around with flags and piped input.This adds an
interactive_terminaltool that runs a single command in a real PTY, opens a terminal dialog over the session input area, streams live output, and hands keyboard control to the user. The tool blocks until the process exits or the user closes the terminal, then returns the captured output and how it ended (exit code, user close, or cancellation) back to the model.Implementation
The feature is isolated under
src/kilocode/as much as possible; shared upstream files carry small, markedkilocode_changehooks.Core engine —
src/kilocode/interactive-terminal/index.tsowns PTY lifecycle as an instance-scoped service:interactive_terminal.databus events; a rolling 2 MB UTF-8-safe cap (output.ts) bounds memory. The live stream keeps raw bytes for VT rendering, while the model-facing result is ANSI-stripped.stty -echo; readon POSIX,pause/ReadKeyon cmd/PowerShell). The server only writes the release byte after theonDatahandler is attached, so no early output is lost to a startup race.stopSessionand the tool'sabortsignal also terminate the PTY.Permissions — rather than duplicate bash's command scanning,
src/tool/shell.tsis refactored to extract a reusableShellPermissionEffect service (the largest shared-file change — worth a close look). The new tool reuses the exact same tree-sitter path scanning and permission prompt asbash, plus a directory permission whenworkdiris outside the project.Surfaces — live UI in two places:
routes/session/terminal.tsx) renders over the input area and takes input ownership while active;context/sync.tsxfolds the bus events into the store.kilo rundirect-interactive footer (src/kilocode/cli/cmd/run/interactive-terminal.tsx+ footer/runtime/session-data glue), with mouse enabled for close/scroll.src/kilocode/cli/cmd/tui/vt/vt-screen.ts) turns PTY escape sequences into a static text snapshot with scrollback/viewport offset for OpenTUI's<text>./interactive-terminallist/get/write/resize/close) plus regenerated SDK and OpenAPI for client control and SSE events.Availability gating — the tool is CLI-only (
Flag.KILO_CLIENT === "cli"), primary-agent only (hidden from subagents in the registry), and denied for subagents, system/utility agents, and non-interactivekilo run.Screenshots / Video
TUI
CLI
How to Test
Manual/local verification
bun test ./test/kilocode/interactive-terminal.test.ts ./test/kilocode/vt-screen.test.ts→ 27 pass (agent-executed).bun test ./test/kilocode/cli/cmd/run/interactive-terminal.test.ts→ 2 pass (agent-executed).tool-registry-indexing,task-nesting,cli/auto-mode,agent/agent,tool/taskpass (agent-executed).bun run typecheckinpackages/opencode/is clean; the cross-package pre-pushturbo typecheck(all 17 tasks) succeeded (agent-executed).Reviewer test steps
gh auth login) or "open a python REPL".Ctrl+Cto send an interrupt, and clickx(or pressEscinrunmode) to force-close; confirm the tool result reports exit code / user close accordingly.kilo run.Blocked checks and substitute verification
bun testrun surfaced one failure intest/session/prompt.test.ts("shell correlates the persisted tool part with its completed v2 record"). This test is not modified by this branch and fails only because the local developer shell'sdirenvhook injects adirenv: unloadingbanner into command stdout. Substitute verification: inspected the assertion diff and confirmed the only delta is the injected direnv banner (not a logic regression); the test passes in a clean environment / CI without direnv shell hooks.Checklist