Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/agent-manager-side-terminal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---

Let users open Agent Manager terminals in the VS Code terminal or an embedded side panel. The terminal button's dropdown picks the destination; the side panel shares the right-hand inspector with the diff view and keeps running in the background when hidden.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions packages/kilo-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1016,6 +1016,20 @@
"scope": "application",
"description": "Prefix for automatically named Agent Manager branches, for example 'marius/' or 'feature/'. Explicit branch names are unchanged."
},
"kilo-code.new.agentManager.terminalButtonDestination": {
"type": "string",
"scope": "application",
"default": "vscode",
"enum": [
"vscode",
"agentManager"
],
"enumDescriptions": [
"Open or focus the VS Code integrated terminal.",
"Open or focus an embedded terminal in the Agent Manager side panel."
],
"description": "Choose where the Agent Manager terminal button and Focus Terminal keyboard shortcut open a terminal."
},
"kilo-code.new.indexing.showButtonWhenDisabled": {
"type": "boolean",
"default": true,
Expand Down
10 changes: 10 additions & 0 deletions packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { pruneSubagents } from "./prune-subagents"

import { startSession } from "./mcp-warmup"
import { readTerminalFont, watchTerminalFont } from "./terminal-font"
import { readTerminalDestination, watchTerminalDestination } from "./terminal-destination"
import { buildKeybindingMap } from "./format-keybinding"
import { resolveVersionModels, buildInitialMessages, type CreatedVersion } from "./multi-version"
import { ensureSandbox } from "./sandbox-bootstrap"
Expand Down Expand Up @@ -78,6 +79,7 @@ export class AgentManagerProvider implements Disposable {
private unsubTool: (() => void) | undefined
private unsubStatus: (() => void) | undefined
private unsubFont: (() => void) | undefined
private unsubDestination: (() => void) | undefined
private closing: Promise<void> | undefined
private onVisibilityChange: ((visible: boolean) => void) | undefined
// Tracks sessions owned by this panel until they are explicitly closed.
Expand Down Expand Up @@ -111,6 +113,9 @@ export class AgentManagerProvider implements Disposable {
this.unsubFont = watchTerminalFont((font) => {
this.postToWebview({ type: "agentManager.terminal.fontChanged", font })
})
this.unsubDestination = watchTerminalDestination((destination) => {
this.postToWebview({ type: "agentManager.terminal.destinationChanged", destination })
})
this.run = new RunController({
root: () => this.getRoot(),
state: () => this.getStateManager(),
Expand Down Expand Up @@ -304,6 +309,7 @@ export class AgentManagerProvider implements Disposable {
this.activeSessionId = undefined
this.visiblePresence.clear()
this.panel = undefined
void this.terminalRouter.dispose()
this.onVisibilityChange?.(false)
}
ctx.sessions.dispose()
Expand Down Expand Up @@ -922,6 +928,7 @@ export class AgentManagerProvider implements Disposable {
case "agentManager.toggleSectionCollapsed":
case "agentManager.moveToSection":
case "agentManager.moveSection":
case "agentManager.terminal.create":
return true
default:
return false
Expand Down Expand Up @@ -1624,6 +1631,7 @@ export class AgentManagerProvider implements Disposable {
sidebarCollapsed: state.getSidebarCollapsed(),
reviewDiffStyle: state.getReviewDiffStyle(),
reviewMarkdownRender: getDiffMarkdownRender(),
terminalDestination: readTerminalDestination(),
isGitRepo: true,
defaultBaseBranch: state.getDefaultBaseBranch(),
...run,
Expand All @@ -1646,6 +1654,7 @@ export class AgentManagerProvider implements Disposable {
staleWorktreeIds: [],
reviewDiffStyle: "unified",
reviewMarkdownRender: getDiffMarkdownRender(),
terminalDestination: readTerminalDestination(),
isGitRepo: false,
runStatuses: [],
runScriptConfigured: false,
Expand Down Expand Up @@ -1923,6 +1932,7 @@ export class AgentManagerProvider implements Disposable {
this.unsubTool?.()
this.unsubStatus?.()
this.unsubFont?.()
this.unsubDestination?.()
this.orchestration.dispose()
this.visiblePresence.clear()
this.diffs.stop()
Expand Down
37 changes: 37 additions & 0 deletions packages/kilo-vscode/src/agent-manager/terminal-destination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Read and watch the user's Agent Manager terminal destination setting.
*
* The terminal button and `Cmd/Ctrl+/` either open the VS Code integrated
* terminal (default, backwards compatible) or an embedded xterm in the
* Agent Manager side panel. Kept next to `terminal-font.ts`; a separate
* module so the font helpers stay untouched.
*/

import * as vscode from "vscode"

export type TerminalDestination = "vscode" | "agentManager"

const KEY = "kilo-code.new.agentManager.terminalButtonDestination"

/** Unknown values fall back to the VS Code terminal so a stale or
* hand-edited setting never strands the user without a terminal. */
export function resolveTerminalDestination(value: unknown): TerminalDestination {
return value === "agentManager" ? value : "vscode"
}

export function readTerminalDestination(): TerminalDestination {
const config = vscode.workspace.getConfiguration("kilo-code.new.agentManager")
return resolveTerminalDestination(config.get("terminalButtonDestination"))
}

export function affectsTerminalDestination(e: vscode.ConfigurationChangeEvent): boolean {
return e.affectsConfiguration(KEY)
}

/** Subscribe to destination changes. Returns a cleanup function. */
export function watchTerminalDestination(callback: (destination: TerminalDestination) => void): () => void {
const sub = vscode.workspace.onDidChangeConfiguration((e) => {
if (affectsTerminalDestination(e)) callback(readTerminalDestination())
})
return () => sub.dispose()
}
61 changes: 44 additions & 17 deletions packages/kilo-vscode/src/agent-manager/terminal-routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@
* Owns:
* - the `TerminalManager` lifecycle (create / close / resize / dispose)
* - the per-context "Terminal N" ordinal counter
* - cwd resolution (worktree path → workspace root fallback)
* - cwd resolution (selected worktree or workspace root)
* - WebSocket URL construction with loopback `auth_token` auth
*
* Vscode-free: all VS Code access is funnelled through the `deps`
* callbacks so this module is trivially unit-testable with fakes.
*/

import type { KiloClient } from "@kilocode/sdk/v2/client"
import type { AgentManagerInMessage, AgentManagerOutMessage, TerminalFont } from "./types"
import type { AgentManagerInMessage, AgentManagerOutMessage, TerminalFont, TerminalPlacement } from "./types"
import { TerminalManager } from "./terminal-manager"

interface ServerConfig {
Expand Down Expand Up @@ -52,14 +52,19 @@ function isTerminalMessage(
}

export class TerminalRouter {
private readonly manager: TerminalManager
private manager: TerminalManager
private readonly ordinals = new Map<string, number>()
private generation = 0

constructor(private readonly deps: TerminalRoutingDeps) {
this.manager = new TerminalManager({
getClient: () => deps.getClient(),
this.manager = this.createManager()
}

private createManager(): TerminalManager {
return new TerminalManager({
getClient: () => this.deps.getClient(),
buildWsUrl: (ptyID, cwd) => this.buildWsUrl(ptyID, cwd),
log: deps.log,
log: this.deps.log,
})
}

Expand All @@ -71,7 +76,7 @@ export class TerminalRouter {
handle(m: AgentManagerInMessage): boolean {
if (!isTerminalMessage(m)) return false
if (m.type === "agentManager.terminal.create") {
void this.handleCreate(m.worktreeId)
void this.handleCreate(m.createId, m.placement, m.worktreeId)
return true
}
if (m.type === "agentManager.terminal.close") {
Expand All @@ -85,48 +90,70 @@ export class TerminalRouter {
return true
}

/** Tear down every live PTY. Forwards to `TerminalManager.dispose`. */
/**
* Tear down every live PTY and invalidate in-flight create requests.
* The router stays usable afterwards: a create landing from before the
* disposal is closed immediately instead of leaking a PTY the webview
* no longer tracks.
*/
dispose(): Promise<void> {
return this.manager.dispose()
this.generation++
const manager = this.manager
this.manager = this.createManager()
return manager.dispose()
}

private async handleCreate(worktreeId: string | null): Promise<void> {
private async handleCreate(createId: string, placement: TerminalPlacement, worktreeId: string | null): Promise<void> {
const generation = this.generation
const manager = this.manager
const cwd = this.resolveCwd(worktreeId)
if (!cwd) {
this.deps.post({
type: "agentManager.terminal.error",
message: "Open a folder before creating a terminal",
createId,
message: worktreeId
? "The selected worktree is no longer available"
: "Open a folder before creating a terminal",
})
return
}
const title = `Terminal ${this.nextOrdinal(worktreeId)}`
try {
const created = await this.manager.create({ worktreeId, cwd, title })
const created = await manager.create({ worktreeId, cwd, title })
if (generation !== this.generation) {
await manager.close(created.terminalId)
return
}
this.deps.post({
type: "agentManager.terminal.created",
createId,
placement,
worktreeId: created.worktreeId,
terminalId: created.terminalId,
title: created.title,
wsUrl: created.wsUrl,
font: this.deps.getTerminalFont(),
})
} catch (err) {
if (generation !== this.generation) return
const message = err instanceof Error ? err.message : String(err)
this.deps.log(`Terminal create failed: ${message}`)
this.deps.post({ type: "agentManager.terminal.error", message })
this.deps.post({ type: "agentManager.terminal.error", createId, message })
}
}

/**
* Resolve the cwd for a terminal in the given context.
*
* LOCAL (null) falls back to the workspace root; a worktree id
* resolves to its on-disk path. Returns undefined when no folder is
* open — the caller surfaces this as a user-facing error.
* LOCAL (null) uses the workspace root; a worktree id resolves strictly
* to its on-disk path — silently falling back to the workspace root
* would run the shell in the wrong directory. Returns undefined when
* no folder is open or the worktree is gone; the caller surfaces this
* as a user-facing error.
*/
private resolveCwd(worktreeId: string | null): string | undefined {
if (worktreeId === null) return this.deps.getRoot()
return this.deps.getWorktreePath(worktreeId) ?? this.deps.getRoot()
return this.deps.getWorktreePath(worktreeId)
}

/** Per-context counter so default titles are "Terminal 1", "Terminal 2"…
Expand Down
21 changes: 21 additions & 0 deletions packages/kilo-vscode/src/agent-manager/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ import type { BranchListItem, WorktreeSetupErrorCode } from "./git-import"
import type { ExternalWorktreeItem } from "./WorktreeManager"
import type { RunStatus } from "./run/manager"
import type { TerminalFont } from "./terminal-font"
import type { TerminalDestination } from "./terminal-destination"

export type { TerminalFont }

/** Where a terminal lives: main tab strip or right-side inspector panel. */
export type TerminalPlacement = "tab" | "side"

// ---------------------------------------------------------------------------
// Shared payload types
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -140,6 +144,7 @@ interface StateMessage {
runStatuses?: RunStatus[]
runScriptConfigured?: boolean
runScriptPath?: string
terminalDestination?: TerminalDestination
}

// ---------------------------------------------------------------------------
Expand All @@ -148,6 +153,11 @@ interface StateMessage {

interface TerminalCreatedMessage {
type: "agentManager.terminal.created"
/** Correlates with the create request; lets the webview spot stale
* creates. Deliberately not named `requestId`: that field name is the
* generic webview request/response correlation channel. */
createId: string
placement: TerminalPlacement
/** null for LOCAL, worktree id otherwise */
worktreeId: string | null
terminalId: string
Expand All @@ -164,9 +174,16 @@ interface TerminalClosedMessage {
interface TerminalErrorMessage {
type: "agentManager.terminal.error"
terminalId?: string
/** Set when the error answers a specific create request. */
createId?: string
message: string
}

interface TerminalDestinationChangedMessage {
type: "agentManager.terminal.destinationChanged"
destination: TerminalDestination
}

interface TerminalFontChangedMessage {
type: "agentManager.terminal.fontChanged"
font: TerminalFont
Expand Down Expand Up @@ -332,6 +349,7 @@ export type AgentManagerOutMessage =
| TerminalCreatedMessage
| TerminalClosedMessage
| TerminalErrorMessage
| TerminalDestinationChangedMessage
| TerminalFontChangedMessage

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -758,6 +776,9 @@ interface MoveSectionIn {

interface TerminalCreateIn {
type: "agentManager.terminal.create"
/** Webview-generated correlation id, echoed back in created/error. */
createId: string
placement: TerminalPlacement
/** null for LOCAL, worktree id otherwise */
worktreeId: string | null
}
Expand Down
1 change: 1 addition & 0 deletions packages/kilo-vscode/tests/accessibility.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const STORIES = [
{ id: "settings--providers-configure", name: "Settings / providers empty state" },
{ id: "marketplace--empty-list", name: "Marketplace / empty state" },
{ id: "agentmanager--sidebar-search-open", name: "Agent Manager / sidebar search" },
{ id: "agentmanager--side-terminal-panel-empty", name: "Agent Manager / side terminal" },
{ id: "session-tabs--switcher-open", name: "Session tabs / switcher" },
]

Expand Down
6 changes: 6 additions & 0 deletions packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ const TSX_FILES = [
path.join(ROOT, "webview-ui/agent-manager/WorktreeSectionActions.tsx"),
path.join(ROOT, "webview-ui/agent-manager/tab-rendering.tsx"),
path.join(ROOT, "webview-ui/agent-manager/terminal/TerminalTab.tsx"),
path.join(ROOT, "webview-ui/agent-manager/terminal/SideTerminalPanel.tsx"),
path.join(ROOT, "webview-ui/agent-manager/terminal/TerminalDestinationButton.tsx"),
path.join(ROOT, "webview-ui/agent-manager/terminal/SortableTerminalTab.tsx"),
path.join(ROOT, "webview-ui/agent-manager/terminal/render.tsx"),
path.join(ROOT, "webview-ui/diff-virtual/DiffVirtualApp.tsx"),
Expand Down Expand Up @@ -801,6 +803,10 @@ const VSCODE_ALLOWED: Record<string, { note: string }> = {
"terminal-font.ts": {
note: "vscode config reader for integrated terminal font settings",
},
// Reads + watches the terminal button destination setting
"terminal-destination.ts": {
note: "vscode config reader for the terminal destination setting",
},
}

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/kilo-vscode/tests/unit/agent-manager-i18n.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ const ROOT = path.resolve(import.meta.dir, "../..")
const TSX_FILES = [
path.join(ROOT, "webview-ui/agent-manager/AgentManagerApp.tsx"),
path.join(ROOT, "webview-ui/agent-manager/sortable-tab.tsx"),
path.join(ROOT, "webview-ui/agent-manager/terminal/SideTerminalPanel.tsx"),
path.join(ROOT, "webview-ui/agent-manager/terminal/TerminalDestinationButton.tsx"),
]

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, expect, it } from "bun:test"
import { affectsTerminalDestination, resolveTerminalDestination } from "../../src/agent-manager/terminal-destination"

function event(key: string) {
return {
affectsConfiguration: (target: string) => target === key,
} as Parameters<typeof affectsTerminalDestination>[0]
}

describe("Agent Manager terminal destination", () => {
it("defaults unknown settings to the VS Code terminal", () => {
expect(resolveTerminalDestination(undefined)).toBe("vscode")
expect(resolveTerminalDestination("invalid")).toBe("vscode")
expect(resolveTerminalDestination("vscode")).toBe("vscode")
expect(resolveTerminalDestination("agentManager")).toBe("agentManager")
})

it("watches only the terminal button destination setting", () => {
expect(affectsTerminalDestination(event("kilo-code.new.agentManager.terminalButtonDestination"))).toBe(true)
expect(affectsTerminalDestination(event("terminal.integrated.fontFamily"))).toBe(false)
})
})
Loading
Loading