mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-19 01:51:21 +08:00
feat(agent-manager): add session terminal with focus toggle keybindings (#491)
* feat(agent-manager): add session terminal with focus toggle keybindings * fix: dispose managed terminals on cleanup
This commit is contained in:
@@ -101,6 +101,18 @@
|
||||
"title": "Agent Manager: Next Tab",
|
||||
"category": "Kilo Code"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.agentManager.showTerminal",
|
||||
"title": "Agent Manager: Focus Terminal",
|
||||
"category": "Kilo Code",
|
||||
"icon": "$(terminal)"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.agentManager.focusPanel",
|
||||
"title": "Agent Manager: Focus Panel",
|
||||
"category": "Kilo Code",
|
||||
"icon": "$(circuit-board)"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.agentManager.newTab",
|
||||
"title": "Agent Manager: New Tab",
|
||||
@@ -309,6 +321,18 @@
|
||||
"mac": "cmd+right",
|
||||
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.agentManager.showTerminal",
|
||||
"key": "ctrl+/",
|
||||
"mac": "cmd+/",
|
||||
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.agentManager.focusPanel",
|
||||
"key": "ctrl+.",
|
||||
"mac": "cmd+.",
|
||||
"when": "terminalFocus && kilo-code.agentTerminalFocus"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.agentManager.newTab",
|
||||
"key": "ctrl+t",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { KiloProvider } from "../KiloProvider"
|
||||
import { buildWebviewHtml } from "../utils"
|
||||
import { WorktreeManager, type CreateWorktreeResult } from "./WorktreeManager"
|
||||
import { WorktreeStateManager } from "./WorktreeStateManager"
|
||||
import { SessionTerminalManager } from "./SessionTerminalManager"
|
||||
|
||||
/**
|
||||
* AgentManagerProvider opens the Agent Manager panel.
|
||||
@@ -21,12 +22,16 @@ export class AgentManagerProvider implements vscode.Disposable {
|
||||
private outputChannel: vscode.OutputChannel
|
||||
private worktrees: WorktreeManager | undefined
|
||||
private state: WorktreeStateManager | undefined
|
||||
private terminalManager: SessionTerminalManager
|
||||
|
||||
constructor(
|
||||
private readonly extensionUri: vscode.Uri,
|
||||
private readonly connectionService: KiloConnectionService,
|
||||
) {
|
||||
this.outputChannel = vscode.window.createOutputChannel("Kilo Agent Manager")
|
||||
this.terminalManager = new SessionTerminalManager((msg) =>
|
||||
this.outputChannel.appendLine(`[SessionTerminal] ${msg}`),
|
||||
)
|
||||
}
|
||||
|
||||
private log(...args: unknown[]) {
|
||||
@@ -124,11 +129,20 @@ export class AgentManagerProvider implements vscode.Disposable {
|
||||
return this.onAddSessionToWorktree(msg.worktreeId)
|
||||
if (type === "agentManager.closeSession" && typeof msg.sessionId === "string")
|
||||
return this.onCloseSession(msg.sessionId)
|
||||
if (type === "agentManager.showTerminal" && typeof msg.sessionId === "string") {
|
||||
this.terminalManager.showTerminal(msg.sessionId, this.state)
|
||||
return null
|
||||
}
|
||||
if (type === "agentManager.requestRepoInfo") {
|
||||
void this.sendRepoInfo()
|
||||
return null
|
||||
}
|
||||
|
||||
// When switching sessions, show existing terminal if one is open
|
||||
if (type === "loadMessages" && typeof msg.sessionID === "string") {
|
||||
this.terminalManager.showExisting(msg.sessionID)
|
||||
}
|
||||
|
||||
// After clearSession, re-register worktree sessions so SSE events keep flowing
|
||||
if (type === "clearSession") {
|
||||
void Promise.resolve().then(() => {
|
||||
@@ -443,11 +457,29 @@ export class AgentManagerProvider implements vscode.Disposable {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Show terminal for the currently active session (triggered by keyboard shortcut).
|
||||
* Posts an action to the webview which will respond with the session ID.
|
||||
*/
|
||||
public showTerminalForCurrentSession(): void {
|
||||
this.postToWebview({ type: "action", action: "showTerminal" })
|
||||
}
|
||||
|
||||
/**
|
||||
* Reveal the Agent Manager panel and focus the prompt input.
|
||||
* Used for the keyboard shortcut to switch back from terminal.
|
||||
*/
|
||||
public focusPanel(): void {
|
||||
if (!this.panel) return
|
||||
this.panel.reveal(vscode.ViewColumn.One, false)
|
||||
}
|
||||
|
||||
public postMessage(message: unknown): void {
|
||||
this.panel?.webview.postMessage(message)
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.terminalManager.dispose()
|
||||
this.provider?.dispose()
|
||||
this.panel?.dispose()
|
||||
this.outputChannel.dispose()
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import * as vscode from "vscode"
|
||||
import type { WorktreeStateManager } from "./WorktreeStateManager"
|
||||
|
||||
/**
|
||||
* Manages VS Code terminals for agent manager sessions.
|
||||
* Each session can have an associated terminal that opens in the session's worktree directory,
|
||||
* or the main workspace folder for local sessions.
|
||||
*/
|
||||
export class SessionTerminalManager {
|
||||
private terminals = new Map<string, { terminal: vscode.Terminal; cwd: string }>()
|
||||
private disposables: vscode.Disposable[] = []
|
||||
|
||||
constructor(private log: (msg: string) => void) {
|
||||
this.disposables.push(
|
||||
vscode.window.onDidCloseTerminal((terminal) => {
|
||||
for (const [sessionId, entry] of this.terminals) {
|
||||
if (entry.terminal !== terminal) continue
|
||||
this.terminals.delete(sessionId)
|
||||
this.log(`Removed terminal mapping for session ${sessionId} (terminal closed)`)
|
||||
break
|
||||
}
|
||||
this.updateContextKey()
|
||||
}),
|
||||
vscode.window.onDidChangeActiveTerminal((terminal) => {
|
||||
const managed = terminal ? this.isManaged(terminal) : false
|
||||
vscode.commands.executeCommand("setContext", "kilo-code.agentTerminalFocus", managed)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Show (or create) a terminal for the given session.
|
||||
* Resolves CWD from the worktree state, falling back to workspace root.
|
||||
*/
|
||||
showTerminal(sessionId: string, state: WorktreeStateManager | undefined): void {
|
||||
// If terminal already exists, just focus it
|
||||
if (this.showExisting(sessionId, false)) return
|
||||
|
||||
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
const worktreePath = state?.directoryFor(sessionId)
|
||||
const cwd = worktreePath ?? workspacePath
|
||||
|
||||
if (!cwd) {
|
||||
this.log(`showTerminal: no cwd resolved for session ${sessionId}`)
|
||||
vscode.window.showWarningMessage("No workspace folder open")
|
||||
return
|
||||
}
|
||||
|
||||
const session = state?.getSession(sessionId)
|
||||
const worktree = session?.worktreeId ? state?.getWorktree(session.worktreeId) : undefined
|
||||
const name = worktree ? `Agent: ${worktree.branch}` : "Agent: local"
|
||||
|
||||
this.showOrCreate(sessionId, cwd, name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the terminal for a session if it already exists (used when switching sessions).
|
||||
* Returns true if the terminal was shown, false if no terminal exists for the session.
|
||||
* Pass preserveFocus=true to keep focus on the current editor (default for session switching).
|
||||
*/
|
||||
showExisting(sessionId: string, preserveFocus = true): boolean {
|
||||
const entry = this.terminals.get(sessionId)
|
||||
if (!entry) return false
|
||||
|
||||
if (entry.terminal.exitStatus !== undefined) {
|
||||
this.terminals.delete(sessionId)
|
||||
this.log(`showExisting: terminal exited for session ${sessionId}, clearing`)
|
||||
return false
|
||||
}
|
||||
|
||||
entry.terminal.show(preserveFocus)
|
||||
this.log(`showExisting: revealed terminal for session ${sessionId}`)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a session has an active terminal.
|
||||
*/
|
||||
hasTerminal(sessionId: string): boolean {
|
||||
const entry = this.terminals.get(sessionId)
|
||||
return entry !== undefined && entry.terminal.exitStatus === undefined
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
vscode.commands.executeCommand("setContext", "kilo-code.agentTerminalFocus", false)
|
||||
for (const entry of this.terminals.values()) entry.terminal.dispose()
|
||||
this.terminals.clear()
|
||||
for (const d of this.disposables) d.dispose()
|
||||
}
|
||||
|
||||
private isManaged(terminal: vscode.Terminal): boolean {
|
||||
for (const entry of this.terminals.values()) {
|
||||
if (entry.terminal === terminal) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private updateContextKey(): void {
|
||||
const active = vscode.window.activeTerminal
|
||||
const managed = active ? this.isManaged(active) : false
|
||||
vscode.commands.executeCommand("setContext", "kilo-code.agentTerminalFocus", managed)
|
||||
}
|
||||
|
||||
private showOrCreate(sessionId: string, cwd: string, name: string): void {
|
||||
let entry = this.terminals.get(sessionId)
|
||||
|
||||
// Clean up exited terminals
|
||||
if (entry && entry.terminal.exitStatus !== undefined) {
|
||||
this.terminals.delete(sessionId)
|
||||
entry = undefined
|
||||
}
|
||||
|
||||
// Recreate if CWD changed
|
||||
if (entry && entry.cwd !== cwd) {
|
||||
entry.terminal.dispose()
|
||||
this.terminals.delete(sessionId)
|
||||
entry = undefined
|
||||
this.log(`showTerminal: cwd changed for session ${sessionId}, recreating`)
|
||||
}
|
||||
|
||||
if (!entry) {
|
||||
const terminal = vscode.window.createTerminal({
|
||||
cwd,
|
||||
name,
|
||||
iconPath: new vscode.ThemeIcon("terminal"),
|
||||
})
|
||||
entry = { terminal, cwd }
|
||||
this.terminals.set(sessionId, entry)
|
||||
this.log(`showTerminal: created terminal for session ${sessionId} (cwd=${cwd})`)
|
||||
}
|
||||
|
||||
entry.terminal.show(false)
|
||||
this.updateContextKey()
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,12 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
vscode.commands.registerCommand("kilo-code.new.agentManager.nextTab", () => {
|
||||
agentManagerProvider.postMessage({ type: "action", action: "tabNext" })
|
||||
}),
|
||||
vscode.commands.registerCommand("kilo-code.new.agentManager.showTerminal", () => {
|
||||
agentManagerProvider.showTerminalForCurrentSession()
|
||||
}),
|
||||
vscode.commands.registerCommand("kilo-code.new.agentManager.focusPanel", () => {
|
||||
agentManagerProvider.focusPanel()
|
||||
}),
|
||||
vscode.commands.registerCommand("kilo-code.new.agentManager.newTab", () => {
|
||||
agentManagerProvider.postMessage({ type: "action", action: "newTab" })
|
||||
}),
|
||||
|
||||
@@ -258,7 +258,10 @@ const AgentManagerContent: Component = () => {
|
||||
else if (msg.action === "sessionNext") navigate("down")
|
||||
else if (msg.action === "tabPrevious") navigateTab("left")
|
||||
else if (msg.action === "tabNext") navigateTab("right")
|
||||
else if (msg.action === "newTab") handleNewTabForCurrentSelection()
|
||||
else if (msg.action === "showTerminal") {
|
||||
const id = session.currentSessionID()
|
||||
if (id) vscode.postMessage({ type: "agentManager.showTerminal", sessionId: id })
|
||||
} else if (msg.action === "newTab") handleNewTabForCurrentSelection()
|
||||
else if (msg.action === "closeTab") closeActiveTab()
|
||||
else if (msg.action === "newWorktree") handleNewWorktreeOrPromote()
|
||||
else if (msg.action === "closeWorktree") closeSelectedWorktree()
|
||||
@@ -648,6 +651,20 @@ const AgentManagerContent: Component = () => {
|
||||
class="am-tab-add"
|
||||
onClick={handleAddSession}
|
||||
/>
|
||||
<div class="am-tab-terminal">
|
||||
<Tooltip value="Open Terminal" placement="bottom">
|
||||
<IconButton
|
||||
icon="console"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
label="Open Terminal"
|
||||
onClick={() => {
|
||||
const id = session.currentSessionID()
|
||||
if (id) vscode.postMessage({ type: "agentManager.showTerminal", sessionId: id })
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
|
||||
@@ -361,6 +361,13 @@
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.am-tab-terminal {
|
||||
flex-shrink: 0;
|
||||
align-self: center;
|
||||
margin-left: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
/* Empty worktree state */
|
||||
|
||||
.am-empty-state {
|
||||
|
||||
@@ -833,6 +833,12 @@ export interface RequestRepoInfoMessage {
|
||||
type: "agentManager.requestRepoInfo"
|
||||
}
|
||||
|
||||
// Show terminal for a session
|
||||
export interface ShowTerminalRequest {
|
||||
type: "agentManager.showTerminal"
|
||||
sessionId: string
|
||||
}
|
||||
|
||||
export type WebviewMessage =
|
||||
| SendMessageRequest
|
||||
| AbortRequest
|
||||
@@ -876,6 +882,7 @@ export type WebviewMessage =
|
||||
| CloseSessionRequest
|
||||
| TelemetryRequest
|
||||
| RequestRepoInfoMessage
|
||||
| ShowTerminalRequest
|
||||
|
||||
// ============================================
|
||||
// VS Code API type
|
||||
|
||||
Reference in New Issue
Block a user