fix(vscode): complete Agent Manager terminal font sync

This commit is contained in:
marius-kilocode
2026-06-12 08:55:45 +02:00
parent 287f75aa2b
commit 3cd4628a52
7 changed files with 135 additions and 64 deletions
@@ -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<string>("fontFamily")?.trim() || editor.get<string>("fontFamily")?.trim() || FALLBACK
const size = term.get<number>("fontSize") || editor.get<number>("fontSize") || 13
return {
fontFamily: family,
fontSize: size,
fontWeight: term.get<string>("fontWeight") || undefined,
fontWeightBold: term.get<string>("fontWeightBold") || undefined,
lineHeight: term.get<number>("lineHeight") || undefined,
letterSpacing: term.get<number>("letterSpacing") || undefined,
fontSize: term.get<number>("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")
)
}
@@ -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<typeof affectsTerminalFont>[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<AgentManagerOutMessage>((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()
})
})
})
@@ -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<string, unknown>)
terminalDispatch(msg)
})
const unsub = vscode.onMessage((msg) => {
@@ -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> = (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> = (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> = (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
}
@@ -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<string, unknown>): 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
@@ -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
@@ -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