diff --git a/packages/wow-statusline/src/index.ts b/packages/wow-statusline/src/index.ts index 73f297c..50e88d3 100644 --- a/packages/wow-statusline/src/index.ts +++ b/packages/wow-statusline/src/index.ts @@ -4,7 +4,7 @@ import type { } from "@earendil-works/pi-coding-agent"; import { registerModule, resetWowConfigCache } from "wow-core"; import { getStatuslineConfig } from "./config"; -import { TAG, log } from "./global"; +import { log, TAG } from "./global"; import { createState } from "./state"; import { applyStatuslineUI, @@ -12,6 +12,8 @@ import { updateStatuslineState, } from "./ui"; +const STREAMING_REFRESH_INTERVAL_MS = 80; + const setup = (pi: ExtensionAPI): void => { const state = createState(); let lastStreamingRefresh = 0; @@ -26,47 +28,36 @@ const setup = (pi: ExtensionAPI): void => { updateStatuslineState(state); }; + const refreshOnEvent = async (): Promise => { + refresh(); + }; + pi.on("session_start", async (_event, ctx) => { state.thinkingLevel = pi.getThinkingLevel(); apply(ctx); log.debug("statusline applied", { cwd: ctx.cwd }); }); - pi.on("agent_start", async () => { - refresh(); - }); + pi.on("agent_start", refreshOnEvent); pi.on("message_update", async () => { const now = Date.now(); - if (now - lastStreamingRefresh < 80) return; + if (now - lastStreamingRefresh < STREAMING_REFRESH_INTERVAL_MS) return; lastStreamingRefresh = now; refresh(); }); - pi.on("model_select", async () => { - refresh(); - }); + pi.on("model_select", refreshOnEvent); pi.on("thinking_level_select", async (event) => { state.thinkingLevel = event.level; refresh(); }); - pi.on("turn_end", async () => { - refresh(); - }); - - pi.on("agent_end", async () => { - refresh(); - }); - - pi.on("session_tree", async () => { - refresh(); - }); - - pi.on("session_compact", async () => { - refresh(); - }); + pi.on("turn_end", refreshOnEvent); + pi.on("agent_end", refreshOnEvent); + pi.on("session_tree", refreshOnEvent); + pi.on("session_compact", refreshOnEvent); pi.on("session_shutdown", async (_event, ctx) => { clearStatuslineUI(ctx, state); diff --git a/packages/wow-statusline/src/statusline-editor.ts b/packages/wow-statusline/src/statusline-editor.ts index 46fc998..479e618 100644 --- a/packages/wow-statusline/src/statusline-editor.ts +++ b/packages/wow-statusline/src/statusline-editor.ts @@ -7,7 +7,7 @@ import { import type { Component, EditorTheme, TUI } from "@earendil-works/pi-tui"; import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui"; import { buildEditorTopLine, buildStatusSummary } from "./format"; -import { snapshotFromContext, type StatuslineState } from "./state"; +import { type StatuslineState, snapshotFromContext } from "./state"; import type { ResolvedStatuslineConfig } from "./types"; const ANSI_SGR_RE = /\u001b\[[0-9;]*m/g; @@ -50,46 +50,62 @@ export class StatuslineEditor extends CustomEditor { this.statuslineState, this.extensionCtx, ); - let bottom = lines.length - 1; - for (let index = lines.length - 1; index >= 1; index--) { - const plain = lines[index]!.replace(ANSI_SGR_RE, ""); - if (plain.length > 0 && /^─{3,}/.test(plain)) { - bottom = index; - break; - } - } - - const topText = buildEditorTopLine(theme, snapshot, this.statuslineConfig); - const folder = this.statuslineConfig.editorStyle.folderIcon; - const content = `${folder} ${topText}`; const border = theme.fg("borderMuted", "─"); - const contentWidth = visibleWidth(content); - const remaining = width - contentWidth - 2; + const borderLine = border.repeat(Math.max(0, width)); + const topContent = `${this.statuslineConfig.editorStyle.folderIcon} ${buildEditorTopLine(theme, snapshot, this.statuslineConfig)}`; + const remainingBorderWidth = width - visibleWidth(topContent) - 2; - lines[0] = - remaining >= 1 - ? content + theme.fg("dim", " ") + border.repeat(remaining) + border - : border.repeat(Math.max(0, width)); + lines[0] = buildTopBorderLine( + topContent, + border, + borderLine, + theme, + remainingBorderWidth, + ); - if (lines.length > 1) { - lines[1] = - theme.fg("accent", this.statuslineConfig.editorStyle.prompt) + - theme.fg("dim", " ") + - (lines[1] ?? ""); - } + lines[1] = + theme.fg("accent", this.statuslineConfig.editorStyle.prompt) + + theme.fg("dim", " ") + + (lines[1] ?? ""); - lines[bottom] = border.repeat(Math.max(0, width)); + lines[findBottomBorderIndex(lines)] = borderLine; for (let index = 0; index < lines.length; index++) { - if (visibleWidth(lines[index]!) > width) { - lines[index] = truncateToWidth(lines[index]!, width); - } + if (visibleWidth(lines[index]!) <= width) continue; + lines[index] = truncateToWidth(lines[index]!, width); } return lines; } } +function buildTopBorderLine( + content: string, + border: string, + borderLine: string, + theme: Theme, + remainingBorderWidth: number, +): string { + if (remainingBorderWidth < 1) return borderLine; + return ( + content + + theme.fg("dim", " ") + + border.repeat(remainingBorderWidth) + + border + ); +} + +function findBottomBorderIndex(lines: string[]): number { + for (let index = lines.length - 1; index >= 1; index--) { + const plain = lines[index]!.replace(ANSI_SGR_RE, ""); + if (plain.length > 0 && /^─{3,}/.test(plain)) { + return index; + } + } + + return lines.length - 1; +} + export function buildStatuslineWidget( ctx: ExtensionContext, state: StatuslineState, diff --git a/packages/wow-statusline/src/ui.ts b/packages/wow-statusline/src/ui.ts index 5ede06c..5d994dc 100644 --- a/packages/wow-statusline/src/ui.ts +++ b/packages/wow-statusline/src/ui.ts @@ -1,11 +1,11 @@ import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent"; import { buildEditorTopLine, buildStatusSummary, EMPTY_USAGE } from "./format"; +import { type StatuslineState, syncFooterSnapshot } from "./state"; import { + buildStatuslineWidget, EmptyFooter, StatuslineEditor, - buildStatuslineWidget, } from "./statusline-editor"; -import { syncFooterSnapshot, type StatuslineState } from "./state"; import type { FooterDataSnapshot, ResolvedStatuslineConfig } from "./types"; const STATUS_WIDGET_ID = "wow-statusline:summary"; @@ -61,26 +61,10 @@ export function applyStatuslineUI( return; } - ctx.ui.setWorkingVisible(false); hijackFooter(ctx, state); installEmptyFooter(ctx, state); - - ctx.ui.setWidget( - STATUS_WIDGET_ID, - (_tui, theme) => ({ - render(width: number): string[] { - const render = buildStatuslineWidget(ctx, state, config); - return render(width, theme); - }, - invalidate() {}, - }), - { placement: "belowEditor" }, - ); - - ctx.ui.setEditorComponent( - (tui, theme, keybindings) => - new StatuslineEditor(tui, theme, keybindings, ctx, state, config), - ); + installStatusWidget(ctx, state, config); + installEditor(ctx, state, config); } export function updateStatuslineState(state: StatuslineState): void { @@ -102,8 +86,6 @@ export function clearStatuslineUI( ctx.ui.setWidget(STATUS_WIDGET_ID, undefined, { placement: "belowEditor" }); ctx.ui.setWidget(STATUS_WIDGET_ID, undefined, { placement: "aboveEditor" }); ctx.ui.setEditorComponent(undefined); - ctx.ui.setWorkingVisible(true); - ctx.ui.setWorkingIndicator(); } function hijackFooter(ctx: ExtensionContext, state: StatuslineState): void { @@ -133,6 +115,35 @@ function hijackFooter(ctx: ExtensionContext, state: StatuslineState): void { state.footerHijacked = true; } +function installStatusWidget( + ctx: ExtensionContext, + state: StatuslineState, + config: ResolvedStatuslineConfig, +): void { + ctx.ui.setWidget( + STATUS_WIDGET_ID, + (_tui, theme) => ({ + render(width: number): string[] { + const render = buildStatuslineWidget(ctx, state, config); + return render(width, theme); + }, + invalidate() {}, + }), + { placement: "belowEditor" }, + ); +} + +function installEditor( + ctx: ExtensionContext, + state: StatuslineState, + config: ResolvedStatuslineConfig, +): void { + ctx.ui.setEditorComponent( + (tui, theme, keybindings) => + new StatuslineEditor(tui, theme, keybindings, ctx, state, config), + ); +} + function installEmptyFooter( ctx: ExtensionContext, state: StatuslineState, @@ -151,7 +162,6 @@ function installEmptyFooter( ) => { captureFooterData(state, theme, footerData); const dispose = footerData.onBranchChange(() => { - state.branch = footerData.getGitBranch() || ""; syncFooterSnapshot(state); state.requestRender?.(); });