From 3cd4628a525dbc9f84b5947b75ae61519c659f11 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 12 Jun 2026 08:55:45 +0200 Subject: [PATCH] fix(vscode): complete Agent Manager terminal font sync --- .../src/agent-manager/terminal-font.ts | 29 ++---- .../unit/agent-manager-terminal-font.test.ts | 93 +++++++++++++++++++ .../agent-manager/AgentManagerApp.tsx | 2 +- .../agent-manager/terminal/TerminalTab.tsx | 18 +--- .../agent-manager/terminal/state.ts | 44 ++++----- .../src/types/messages/agent-manager.ts | 5 + .../src/types/messages/extension-messages.ts | 8 ++ 7 files changed, 135 insertions(+), 64 deletions(-) create mode 100644 packages/kilo-vscode/tests/unit/agent-manager-terminal-font.test.ts diff --git a/packages/kilo-vscode/src/agent-manager/terminal-font.ts b/packages/kilo-vscode/src/agent-manager/terminal-font.ts index d49f27ddba3..69cfc06d73a 100644 --- a/packages/kilo-vscode/src/agent-manager/terminal-font.ts +++ b/packages/kilo-vscode/src/agent-manager/terminal-font.ts @@ -1,9 +1,9 @@ /** * Helpers to read and watch the user's integrated-terminal font settings. * - * VS Code's integrated terminal mirrors editor font settings when terminal - * font settings are unset. We replicate that same logic here so the Agent - * Manager xterm instances look identical to VS Code's own terminal. + * VS Code's integrated terminal falls back to the editor font family when + * its own family is unset. Font size has a separate platform default. We + * replicate those settings for Agent Manager xterm instances. */ import * as vscode from "vscode" @@ -11,42 +11,29 @@ import * as vscode from "vscode" export interface TerminalFont { fontFamily: string fontSize: number - fontWeight?: string - fontWeightBold?: string - lineHeight?: number - letterSpacing?: number } const FALLBACK = "Menlo, Monaco, 'Courier New', monospace" +const SIZE = process.platform === "darwin" ? 12 : 14 /** Resolve the user's integrated-terminal font, mirroring VS Code's own - * fallback to the editor font when the terminal font is unset. */ + * family fallback while preserving the terminal's independent size. */ export function readTerminalFont(): TerminalFont { const term = vscode.workspace.getConfiguration("terminal.integrated") const editor = vscode.workspace.getConfiguration("editor") const family = term.get("fontFamily")?.trim() || editor.get("fontFamily")?.trim() || FALLBACK - const size = term.get("fontSize") || editor.get("fontSize") || 13 return { fontFamily: family, - fontSize: size, - fontWeight: term.get("fontWeight") || undefined, - fontWeightBold: term.get("fontWeightBold") || undefined, - lineHeight: term.get("lineHeight") || undefined, - letterSpacing: term.get("letterSpacing") || undefined, + fontSize: term.get("fontSize") ?? SIZE, } } -/** True when a config change touches any terminal/editor font setting. */ +/** True when a config change affects the effective terminal family or size. */ export function affectsTerminalFont(e: vscode.ConfigurationChangeEvent): boolean { return ( e.affectsConfiguration("terminal.integrated.fontFamily") || e.affectsConfiguration("terminal.integrated.fontSize") || - e.affectsConfiguration("terminal.integrated.fontWeight") || - e.affectsConfiguration("terminal.integrated.fontWeightBold") || - e.affectsConfiguration("terminal.integrated.lineHeight") || - e.affectsConfiguration("terminal.integrated.letterSpacing") || - e.affectsConfiguration("editor.fontFamily") || - e.affectsConfiguration("editor.fontSize") + e.affectsConfiguration("editor.fontFamily") ) } 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 new file mode 100644 index 00000000000..14bee64105d --- /dev/null +++ b/packages/kilo-vscode/tests/unit/agent-manager-terminal-font.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it } from "bun:test" +import type { KiloClient } from "@kilocode/sdk/v2/client" +import { createRoot } from "solid-js" +import { affectsTerminalFont, readTerminalFont } from "../../src/agent-manager/terminal-font" +import { TerminalRouter } from "../../src/agent-manager/terminal-routing" +import type { AgentManagerOutMessage, TerminalFont } from "../../src/agent-manager/types" +import { createTerminalMessageHandler, createTerminalState } from "../../webview-ui/agent-manager/terminal/state" +import { LOCAL } from "../../webview-ui/agent-manager/navigate" +import type { ExtensionMessage } from "../../webview-ui/src/types/messages/extension-messages" + +const font: TerminalFont = { + fontFamily: "MesloLGS NF", + fontSize: 18, +} + +describe("Agent Manager terminal font", () => { + it("uses the platform terminal size when configuration is unavailable", () => { + expect(readTerminalFont()).toEqual({ + fontFamily: "Menlo, Monaco, 'Courier New', monospace", + fontSize: process.platform === "darwin" ? 12 : 14, + }) + }) + + it("watches only settings that affect the terminal family or size", () => { + const event = (key: string) => + ({ + affectsConfiguration: (target: string) => target === key, + }) as Parameters[0] + + expect(affectsTerminalFont(event("terminal.integrated.fontFamily"))).toBe(true) + expect(affectsTerminalFont(event("terminal.integrated.fontSize"))).toBe(true) + expect(affectsTerminalFont(event("editor.fontFamily"))).toBe(true) + expect(affectsTerminalFont(event("editor.fontSize"))).toBe(false) + expect(affectsTerminalFont(event("terminal.integrated.letterSpacing"))).toBe(false) + }) + + it("includes the current font when creating a terminal", async () => { + 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 message = new Promise((resolve) => { + const router = new TerminalRouter({ + getClient: () => client, + getServerConfig: () => ({ baseUrl: "http://127.0.0.1:4096", password: "secret" }), + getRoot: () => "/workspace", + getWorktreePath: () => undefined, + log: () => undefined, + post: resolve, + getTerminalFont: () => font, + }) + + expect(router.handle({ type: "agentManager.terminal.create", worktreeId: null })).toBe(true) + }) + + const created = await message + expect(created.type).toBe("agentManager.terminal.created") + if (created.type !== "agentManager.terminal.created") return + expect(created.font).toEqual(font) + expect(created.worktreeId).toBeNull() + expect(created.wsUrl).toContain("/pty/pty-1/connect") + }) + + it("keeps the created font in terminal state", () => { + createRoot((dispose) => { + const state = createTerminalState(() => LOCAL) + const activated: string[] = [] + const handler = createTerminalMessageHandler({ + state, + activate: (id) => activated.push(id), + saveTabMemory: () => undefined, + setSelection: () => undefined, + showError: () => undefined, + }) + const message = { + type: "agentManager.terminal.created", + worktreeId: null, + terminalId: "terminal-1", + title: "Terminal 1", + wsUrl: "ws://127.0.0.1/pty/pty-1/connect", + font, + } satisfies ExtensionMessage + + expect(handler(message)).toBe(true) + expect(state.forSelection(LOCAL)[0]?.font).toEqual(font) + expect(activated).toEqual(["terminal-1"]) + dispose() + }) + }) +}) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 3b178ef35a4..f7026acfb25 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -1129,7 +1129,7 @@ const AgentManagerContent: Component = () => { onCreated: (contextKey, terminalId) => appendToTabOrder(contextKey, terminalId), }) const unsubTerminals = vscode.onMessage((msg) => { - terminalDispatch(msg as unknown as { type: string } & Record) + terminalDispatch(msg) }) const unsub = vscode.onMessage((msg) => { diff --git a/packages/kilo-vscode/webview-ui/agent-manager/terminal/TerminalTab.tsx b/packages/kilo-vscode/webview-ui/agent-manager/terminal/TerminalTab.tsx index 8ad304f46ce..622fa520705 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/terminal/TerminalTab.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/terminal/TerminalTab.tsx @@ -11,7 +11,7 @@ */ import { Component, createEffect, onCleanup, onMount } from "solid-js" -import { Terminal, type FontWeight } from "@xterm/xterm" +import { Terminal } from "@xterm/xterm" import { FitAddon } from "@xterm/addon-fit" import { WebLinksAddon } from "@xterm/addon-web-links" import { ClipboardAddon } from "@xterm/addon-clipboard" @@ -136,10 +136,6 @@ export const TerminalTab: Component = (props) => { theme: readTheme(), allowProposedApi: true, }) - if (props.font.fontWeight) term.options.fontWeight = props.font.fontWeight as FontWeight - if (props.font.fontWeightBold) term.options.fontWeightBold = props.font.fontWeightBold as FontWeight - if (props.font.lineHeight) term.options.lineHeight = props.font.lineHeight - if (props.font.letterSpacing) term.options.letterSpacing = props.font.letterSpacing const fit = new FitAddon() term.loadAddon(fit) // Clickable URLs in terminal output (Cmd/Ctrl+click to open). @@ -276,6 +272,7 @@ export const TerminalTab: Component = (props) => { if (!isRenderable()) return try { fit.fit() + syncSize() } catch (err) { // Layout not settled yet; ResizeObserver retries on next change. log("repaint fit() threw", err) @@ -297,15 +294,8 @@ export const TerminalTab: Component = (props) => { } if (message.type === "agentManager.terminal.fontChanged") { - const font = message.font as TerminalFont - term.options.fontFamily = font.fontFamily - term.options.fontSize = font.fontSize - if (font.fontWeight) term.options.fontWeight = font.fontWeight as FontWeight - if (font.fontWeightBold) term.options.fontWeightBold = font.fontWeightBold as FontWeight - if (font.lineHeight) term.options.lineHeight = font.lineHeight - if (font.letterSpacing) term.options.letterSpacing = font.letterSpacing - fit.fit() - syncSize() + term.options.fontFamily = message.font.fontFamily + term.options.fontSize = message.font.fontSize scheduleRepaint() return } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/terminal/state.ts b/packages/kilo-vscode/webview-ui/agent-manager/terminal/state.ts index 64604167ada..f472623b287 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/terminal/state.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/terminal/state.ts @@ -10,16 +10,10 @@ import { createMemo, createSignal } from "solid-js" import type { Accessor } from "solid-js" import { LOCAL } from "../navigate" +import type { ExtensionMessage } from "../../src/types/messages/extension-messages" +import type { TerminalFont } from "../../src/types/messages/agent-manager" -/** Font settings forwarded from the extension host (mirrors terminal-font.ts). */ -export interface TerminalFont { - fontFamily: string - fontSize: number - fontWeight?: string - fontWeightBold?: string - lineHeight?: number - letterSpacing?: number -} +export type { TerminalFont } /** Prefix used for terminal tab IDs in the webview (mirrors terminal-manager.ts). */ export const TERMINAL_PREFIX = "terminal:" @@ -41,14 +35,6 @@ export interface TerminalTabStateWithContext extends TerminalTabState { contextKey: string } -export interface TerminalCreatedEvent { - worktreeId: string | null - terminalId: string - title: string - wsUrl: string - font: TerminalFont -} - export interface TerminalStateControls { /** Record received from `terminal.created`. */ add(worktreeId: string | null, term: TerminalTabState): void @@ -343,26 +329,28 @@ export interface TerminalMessageHandlerDeps { * out of the main webview component. */ export function createTerminalMessageHandler(deps: TerminalMessageHandlerDeps) { - return (msg: { type: string } & Record): boolean => { + return (msg: ExtensionMessage): boolean => { if (msg.type === "agentManager.terminal.created") { - const ev = msg as unknown as TerminalCreatedEvent - const contextKey = ev.worktreeId === null ? LOCAL : ev.worktreeId - deps.state.add(ev.worktreeId, { id: ev.terminalId, title: ev.title, wsUrl: ev.wsUrl, font: ev.font }) - deps.onCreated?.(contextKey, ev.terminalId) + const contextKey = msg.worktreeId === null ? LOCAL : msg.worktreeId + deps.state.add(msg.worktreeId, { + id: msg.terminalId, + title: msg.title, + wsUrl: msg.wsUrl, + font: msg.font, + }) + deps.onCreated?.(contextKey, msg.terminalId) deps.saveTabMemory() deps.setSelection(contextKey) - deps.activate(ev.terminalId) + deps.activate(msg.terminalId) return true } if (msg.type === "agentManager.terminal.closed") { - const ev = msg as unknown as { terminalId: string } - deps.state.remove(ev.terminalId) - if (deps.state.activeId() === ev.terminalId) deps.state.setActiveId(undefined) + deps.state.remove(msg.terminalId) + if (deps.state.activeId() === msg.terminalId) deps.state.setActiveId(undefined) return true } if (msg.type === "agentManager.terminal.error") { - const ev = msg as unknown as { message: string } - deps.showError(ev.message) + deps.showError(msg.message) return true } return false diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts b/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts index c19d22c9dd5..d97e1fc955b 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts @@ -1,5 +1,10 @@ export type WorktreeErrorCode = "git_not_found" | "not_git_repo" | "lfs_missing" +export interface TerminalFont { + fontFamily: string + fontSize: number +} + // Agent Manager worktree state types (mirrored from WorktreeStateManager) export interface WorktreeState { id: string diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts index 161cc14ead0..d0d74bd4d92 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts @@ -31,6 +31,7 @@ import type { ReviewComment, RunStatus, SectionState, + TerminalFont, WorktreeErrorCode, WorktreeFileDiff, WorktreeGitStats, @@ -582,6 +583,12 @@ export interface AgentManagerTerminalCreatedMessage { terminalId: string title: string wsUrl: string + font: TerminalFont +} + +export interface AgentManagerTerminalFontChangedMessage { + type: "agentManager.terminal.fontChanged" + font: TerminalFont } export interface AgentManagerTerminalClosedMessage { @@ -1033,6 +1040,7 @@ export type ExtensionMessage = | AgentManagerLocalStatsMessage | AgentManagerPRStatusMessage | AgentManagerTerminalCreatedMessage + | AgentManagerTerminalFontChangedMessage | AgentManagerTerminalClosedMessage | AgentManagerTerminalErrorMessage // legacy-migration start