diff --git a/.changeset/fast-agent-manager-terminals.md b/.changeset/fast-agent-manager-terminals.md new file mode 100644 index 00000000000..6beace30f40 --- /dev/null +++ b/.changeset/fast-agent-manager-terminals.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Open Agent Manager terminals faster and avoid delayed shell prompts. diff --git a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts index bf72af74ab8..7670f9549ff 100644 --- a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts +++ b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts @@ -103,6 +103,7 @@ export class AgentManagerProvider implements Disposable { ) this.terminalRouter = new TerminalRouter({ getClient: () => this.connectionService.getClient(), + getClientAsync: () => this.connectionService.getClientAsync(this.getRoot()), getServerConfig: () => this.connectionService.getServerConfig() ?? undefined, getRoot: () => this.getRoot(), getWorktreePath: (id) => this.getStateManager()?.getWorktree(id)?.path, @@ -906,8 +907,9 @@ export class AgentManagerProvider implements Disposable { case "agentManager.toggleSectionCollapsed": case "agentManager.moveToSection": case "agentManager.moveSection": - case "agentManager.terminal.create": return true + case "agentManager.terminal.create": + return m.worktreeId !== null default: return false } diff --git a/packages/kilo-vscode/src/agent-manager/terminal-routing.ts b/packages/kilo-vscode/src/agent-manager/terminal-routing.ts index 566952f9b21..3fef7352ed0 100644 --- a/packages/kilo-vscode/src/agent-manager/terminal-routing.ts +++ b/packages/kilo-vscode/src/agent-manager/terminal-routing.ts @@ -26,6 +26,8 @@ interface ServerConfig { export interface TerminalRoutingDeps { /** Shared SDK client. Throws when the CLI backend is not connected. */ getClient(): KiloClient + /** Shared SDK client, connecting the CLI backend when needed. */ + getClientAsync(): Promise /** Loopback URL + basic-auth password for the running `kilo serve`. */ getServerConfig(): ServerConfig | undefined /** Workspace root — used as cwd fallback when no worktree is selected (LOCAL). */ @@ -119,6 +121,9 @@ export class TerminalRouter { } const title = `Terminal ${this.nextOrdinal(worktreeId)}` try { + // Join the shared backend connection instead of racing its synchronous + // client accessor when this is the first Kilo action in the window. + await this.deps.getClientAsync() const created = await manager.create({ worktreeId, cwd, title }) if (generation !== this.generation) { await manager.close(created.terminalId) @@ -182,6 +187,9 @@ export class TerminalRouter { const token = Buffer.from(`kilo:${config.password}`).toString("base64") const dir = encodeURIComponent(cwd) const auth = encodeURIComponent(token) - return `${base}/pty/${encodeURIComponent(ptyID)}/connect?directory=${dir}&cursor=-1&auth_token=${auth}` + // A new terminal has one initial attachment. Replay its retained startup + // bytes so xterm can answer shell capability queries emitted before the + // WebSocket connected; tailing from -1 can make shells wait for a timeout. + return `${base}/pty/${encodeURIComponent(ptyID)}/connect?directory=${dir}&cursor=0&auth_token=${auth}` } } diff --git a/packages/kilo-vscode/tests/unit/agent-manager-terminal-font.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-terminal-font.test.ts index 64504d2600b..02b7fffe27b 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-terminal-font.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-terminal-font.test.ts @@ -53,6 +53,7 @@ describe("Agent Manager terminal font", () => { const message = new Promise((resolve) => { const router = new TerminalRouter({ getClient: () => client, + getClientAsync: async () => client, getServerConfig: () => ({ baseUrl: "http://127.0.0.1:4096", password: "secret" }), getRoot: () => "/workspace", getWorktreePath: () => undefined, @@ -77,6 +78,7 @@ describe("Agent Manager terminal font", () => { expect(created.font).toEqual(font) expect(created.worktreeId).toBeNull() expect(created.wsUrl).toContain("/pty/pty-1/connect") + expect(created.wsUrl).toContain("cursor=0") }) it("keeps the created font in terminal state", () => { diff --git a/packages/kilo-vscode/tests/unit/agent-manager-terminal-routing.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-terminal-routing.test.ts index bfa243d1fc8..ffa58bd6db6 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-terminal-routing.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-terminal-routing.test.ts @@ -21,6 +21,7 @@ describe("Agent Manager terminal routing", () => { } as unknown as KiloClient const router = new TerminalRouter({ getClient: () => client, + getClientAsync: async () => client, getServerConfig: () => ({ baseUrl: "http://127.0.0.1:4096", password: "secret" }), getRoot: () => "/workspace", getWorktreePath: (id) => (id === "wt-1" ? "/workspace/wt-1" : undefined), @@ -78,6 +79,7 @@ describe("Agent Manager terminal routing", () => { } as unknown as KiloClient const router = new TerminalRouter({ getClient: () => client, + getClientAsync: async () => client, getServerConfig: () => ({ baseUrl: "http://127.0.0.1:4096", password: "secret" }), getRoot: () => "/workspace", getWorktreePath: () => undefined, @@ -108,4 +110,46 @@ describe("Agent Manager terminal routing", () => { await router.dispose() expect(removed).toContain("pty-new") }) + + it("awaits the shared backend connection before creating a terminal", async () => { + let connected = false + const client = { + pty: { + create: async () => ({ data: { id: "pty-1", title: "Terminal 1" } }), + remove: async () => ({ data: true }), + update: async () => ({ data: true }), + }, + } as unknown as KiloClient + const router = new TerminalRouter({ + getClient: () => { + if (!connected) throw new Error("Not connected") + return client + }, + getClientAsync: async () => { + await wait() + connected = true + return client + }, + getServerConfig: () => ({ baseUrl: "http://127.0.0.1:4096", password: "secret" }), + getRoot: () => "/workspace", + getWorktreePath: () => undefined, + log: () => undefined, + post: (message) => messages.push(message), + getTerminalFont: () => font, + }) + + const messages: AgentManagerOutMessage[] = [] + router.handle({ + type: "agentManager.terminal.create", + createId: "real", + placement: "tab", + worktreeId: null, + }) + expect(messages).toHaveLength(0) + await wait() + await wait() + + expect(messages[0]).toMatchObject({ type: "agentManager.terminal.created", createId: "real" }) + await router.dispose() + }) })