Skip to content

feat(vscode): xterm.js terminal tabs in agent manager#9268

Merged
marius-kilocode merged 2 commits intomainfrom
feat/agent-manager-terminal-tabs
Apr 22, 2026
Merged

feat(vscode): xterm.js terminal tabs in agent manager#9268
marius-kilocode merged 2 commits intomainfrom
feat/agent-manager-terminal-tabs

Conversation

@marius-kilocode
Copy link
Copy Markdown
Collaborator

@marius-kilocode marius-kilocode commented Apr 20, 2026

Summary

  • Click the chevron next to the + tab button and pick "New Terminal" (or press Cmd+Shift+T / Ctrl+Shift+T) to spawn a real xterm.js terminal in the selected worktree or Local directory. Each tab runs its own shell via the Kilo CLI's existing PTY backend, streamed over a direct loopback WebSocket.
  • Coexists with the legacy VS Code integrated-terminal entry points — Cmd+/ and the >_ console button are unchanged. Users can pick whichever they prefer.
  • Terminals render as proper tabs alongside agent sessions, support mixed drag-reorder with session tabs, remember per-context focus, and survive webview reloads without blanking.

Notes for reviewers

  • Zero changes to shared opencode surfaces outside the existing PTY namespace. All new code lives in packages/kilo-vscode/ and the narrow security fix in packages/opencode/src/global/index.ts + packages/opencode/src/pty/index.ts (strip $KILO_SERVER_PASSWORD from spawned shell env; defensive .trim() on xdg paths).
  • Terminal-specific code is isolated under webview-ui/agent-manager/terminal/ so the feature can be retired cleanly if we ever want to.
  • eslint.config.mjs raises AgentManagerApp.tsx max-lines 3100 → 3175 (~40 lines of signal bindings and a stacking-container wrapper that must live alongside existing selection state).
  • The hydration invariant and why it matters (why display: none doesn't work for xterm subtrees) is documented in terminal/render.tsx's JSDoc.
image image image

Design docs and follow-up

  • #9338 — MVP design doc (implemented in this PR)
  • #9337 — Continuation v1: warm / cold reconnect + persistence across reloads

Testing

Walked through 22 edge cases locally. Key invariants verified:

  • Per-worktree cwd isolation (pwd returns the worktree path when opened in a worktree context)
  • Multiple independent shells per worktree; output keeps streaming while other tabs are focused
  • Extension reload cleanly kills all PTYs via TerminalRouter.dispose()
  • Drag reorder mixes sessions / terminals / review tabs cleanly
  • Terminal canvases repaint correctly after cross-worktree navigation, alt-tab, and window focus loss
  • New session created while a terminal is focused properly shifts focus to the new session instead of leaving the terminal on top

) {
return undefined
}
if (!this.isExperimentalTerminalsEnabled()) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Toggling the flag off can orphan already-open PTYs

This guard drops agentManager.terminal.close and agentManager.terminal.resize for terminals that were opened before the setting changed. The webview still keeps those tabs around, so closing one only removes the UI state; xtermManager.close() never runs and the backend shell keeps running until the whole panel is disposed. The flag check should only block new terminal creation, or explicitly tear down tracked PTYs when the feature is turned off.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer applicable — the experimentalTerminalTabs feature flag was removed in a later commit on this PR; the guard this comment was about no longer exists. Terminal tabs ship unconditionally alongside the legacy integrated-terminal shortcuts now, so there's no flag to toggle off.

this.entries.delete(terminalId)
try {
const client = this.deps.getClient()
await client.pty.remove({ directory: entry.cwd, ptyID: entry.ptyID })
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Failed PTY deletes are silently treated as success

client.pty.remove() only throws when throwOnError is enabled. Here the returned result is ignored, so a 4xx/5xx response leaves the backend PTY alive while the entry is dropped locally and Terminal closed is logged. That can leak orphan shells for the lifetime of the CLI server. Please check the returned error field or call this endpoint with { throwOnError: true }.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 4007c1f252. Both close() and dispose() now destructure { error } from the SDK response (same pattern used in create() and resize()) and log a clear "PTY may linger until kilo serve exits" message when the delete fails. Transport-level exceptions continue to be caught separately.

@kilo-code-bot
Copy link
Copy Markdown
Contributor

kilo-code-bot Bot commented Apr 20, 2026

Code Review Summary

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
packages/kilo-vscode/src/agent-manager/terminal-routing.ts 77 The router still emits agentManager.terminal.closed after close() resolves even when backend PTY deletion fails, so the UI can hide a terminal while the shell keeps running until kilo serve exits.
Other Observations (not in diff)

Issues found in unchanged code that cannot receive inline comments:

File Line Issue
None No additional out-of-diff issues found.

Fix these issues in Kilo Cloud

Files Reviewed (40 files)
  • .changeset/agent-manager-terminal-tabs.md
  • bun.lock
  • packages/kilo-vscode/eslint.config.mjs
  • packages/kilo-vscode/package.json
  • packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts
  • packages/kilo-vscode/src/agent-manager/terminal-manager.ts - 1 issue still active via router interaction
  • packages/kilo-vscode/src/agent-manager/terminal-routing.ts - 1 issue
  • packages/kilo-vscode/src/agent-manager/types.ts
  • packages/kilo-vscode/src/extension.ts
  • packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts
  • packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx
  • packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css
  • packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts
  • packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts
  • packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts
  • packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts
  • packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts
  • packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts
  • packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts
  • packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts
  • packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts
  • packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts
  • packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts
  • packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts
  • packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts
  • packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts
  • packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts
  • packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts
  • packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts
  • packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts
  • packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts
  • packages/kilo-vscode/webview-ui/agent-manager/tab-rendering.tsx
  • packages/kilo-vscode/webview-ui/agent-manager/terminal/SortableTerminalTab.tsx
  • packages/kilo-vscode/webview-ui/agent-manager/terminal/TerminalTab.tsx
  • packages/kilo-vscode/webview-ui/agent-manager/terminal/index.ts
  • packages/kilo-vscode/webview-ui/agent-manager/terminal/render.tsx
  • packages/kilo-vscode/webview-ui/agent-manager/terminal/state.ts
  • packages/kilo-vscode/webview-ui/src/types/messages.ts
  • packages/opencode/src/global/index.ts
  • packages/opencode/src/pty/index.ts

Reviewed by gpt-5.4-20260305 · 1,063,553 tokens

@marius-kilocode marius-kilocode marked this pull request as draft April 20, 2026 16:53
@marius-kilocode marius-kilocode force-pushed the feat/agent-manager-terminal-tabs branch 6 times, most recently from 0346e95 to c2137f7 Compare April 21, 2026 21:15
@marius-kilocode marius-kilocode marked this pull request as ready for review April 21, 2026 21:16
@marius-kilocode marius-kilocode changed the title feat(vscode): experimental xterm.js terminal tabs in agent manager feat(vscode): xterm.js terminal tabs in agent manager Apr 21, 2026
@marius-kilocode marius-kilocode force-pushed the feat/agent-manager-terminal-tabs branch from c2137f7 to 4007c1f Compare April 21, 2026 21:25
@marius-kilocode marius-kilocode force-pushed the feat/agent-manager-terminal-tabs branch 2 times, most recently from 2bdb62f to 7094a31 Compare April 21, 2026 21:44
@@ -0,0 +1,211 @@
/**
* Experimental terminal manager.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you consider it experimental? if so, should we hide it behind an experimental setting?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So there are probably some edge, cases but it also doesn't hurt to introduce it. We won't intefere with standard functionality for now.

const closeTerminal = (terminalId: string) => {
const ids = deps.tabIds()
const idx = ids.indexOf(terminalId)
const nextId = ids[idx + 1] ?? ids[idx - 1]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this a hacky way of checking idx == ids.length?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a hacky check — it's ids[idx+1] ?? ids[idx-1], which handles three cases in one line:

  • closed tab wasn't the last → focus the next tab
  • closed tab was the last → ids[idx+1] is undefined, fall back to the previous tab
  • closed tab was the only one → both sides are undefined, nextId is undefined and we intentionally focus nothing

Explicit length check would need the same two branches. Added a comment in 387ed0001b so the intent reads faster.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rewritten in f210beb732 — explicit IIFE with named hasNext / hasPrev locals and early returns for each case:

const nextId = ((): string | undefined => {
  if (idx < 0) return undefined
  const hasNext = idx + 1 < ids.length
  if (hasNext) return ids[idx + 1]
  const hasPrev = idx > 0
  if (hasPrev) return ids[idx - 1]
  return undefined
})()

No more ?? trick, reads top-to-bottom as 'next → previous → nothing'. Matches the style guide's 'use IIFE instead of let' preference.

Comment thread packages/kilo-vscode/package.json Outdated
"@opencode-ai/ui": "workspace:*",
"@thisbeyond/solid-dnd": "0.7.5",
"@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "5.5.0",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have an idea of the impact on the size of the extension this has?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Measured on disk before bundling (@xterm/xterm 5.5 ships pre-minified):

  • @xterm/xterm runtime: 283 KB (lib/xterm.js)
  • @xterm/addon-fit: 1.5 KB
  • xterm.css: 5.4 KB
  • Total: ~290 KB uncompressed, ~82 KB gzipped added to agent-manager.js / agent-manager.css.

The .js.map files (~1.1 MB) are not bundled into the webview output.

For context, the unminified dev agent-manager.js is ~15 MB and the production-minified bundle is a few MB, so ~290 KB is a small fraction.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update after bumping to @xterm/xterm@6.0.0 + @xterm/addon-fit@0.11.0 in e5a8232f26:

  • @xterm/xterm runtime: 477 KB (up from 283 KB on 5.5 — 6.0 integrates the VS Code base platform scrollbar and other features)
  • @xterm/addon-fit: 1.5 KB (unchanged)
  • xterm.css: 6.9 KB (up from 5.4 KB)
  • New total: ~485 KB uncompressed, ~135 KB gzipped added to agent-manager.js / agent-manager.css.

Dev bundle: 15 MB → 17 MB; production-minified delta is the ~135 KB gzip figure above.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added three more addons in 95dfb6cd6b — all compatible with 6.x:

  • @xterm/addon-web-links 0.12.0 → 3.0 KB (clickable URLs in output)
  • @xterm/addon-clipboard 0.2.0 → 6.2 KB (OSC 52 for tmux/nvim)
  • @xterm/addon-unicode11 0.9.0 → 51 KB (fixes emoji / CJK cell width for powerlevel10k / starship prompts; ~48 KB of that is the Unicode 11 width tables themselves)

New combined total: ~545 KB uncompressed, ~155 KB gzipped added to agent-manager.js.

@marius-kilocode marius-kilocode force-pushed the feat/agent-manager-terminal-tabs branch 4 times, most recently from f210beb to 95dfb6c Compare April 22, 2026 15:12
}
if (m.type === "agentManager.terminal.close") {
void this.manager.close(m.terminalId).then(() => {
this.deps.post({ type: "agentManager.terminal.closed", terminalId: m.terminalId })
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Close success is reported even when PTY teardown fails

TerminalManager.close() logs and returns when client.pty.remove() fails, so this callback still posts agentManager.terminal.closed and the webview removes the tab. That leaves the backend PTY alive until kilo serve exits, which is the resource-leak path this feature is trying to avoid. Only emit the closed message after a confirmed delete, or send an error message instead.

@marius-kilocode marius-kilocode force-pushed the feat/agent-manager-terminal-tabs branch from 95dfb6c to 2396737 Compare April 22, 2026 15:18
Click the chevron next to the + tab button and pick 'New Terminal' (or hit Cmd+Shift+T / Ctrl+Shift+T) to spawn a real shell inside the selected worktree or Local directory. Each terminal runs via the Kilo CLI's PTY backend, streamed over a direct loopback WebSocket so raw bytes bypass postMessage. Tabs mirror the worktree split-button pattern, support mixed drag-reorder with session tabs, and survive worktree-context switches without losing xterm state (slots are opacity-toggled in a persistent absolute-positioned layer, never unmounted). The legacy Cmd+/ integrated-terminal shortcut and console icon are preserved so existing muscle memory keeps working.
@marius-kilocode marius-kilocode force-pushed the feat/agent-manager-terminal-tabs branch from 2396737 to 561f862 Compare April 22, 2026 15:28
@marius-kilocode marius-kilocode enabled auto-merge (squash) April 22, 2026 15:58
…rminal-tabs

# Conflicts:
#	packages/opencode/src/global/index.ts
#	packages/opencode/src/pty/index.ts
@marius-kilocode marius-kilocode merged commit 48c0553 into main Apr 22, 2026
18 of 20 checks passed
@marius-kilocode marius-kilocode deleted the feat/agent-manager-terminal-tabs branch April 22, 2026 16:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants