fix(wow-statusline): stopped hiding working indicator when applying statusline UI

- Removed setWorkingVisible(false) call in applyStatuslineUI that hid the working indicator.
- Removed setWorkingVisible(true) and setWorkingIndicator() calls in clearStatuslineUI on teardown.
- Extracted installStatusWidget and installEditor functions from applyStatuslineUI.
- Consolidated duplicate inline event handlers into shared refreshOnEvent function.
- Extracted buildTopBorderLine and findBottomBorderIndex helpers in StatuslineEditor.
This commit is contained in:
kmou424
2026-06-17 17:37:32 +08:00
parent 9f95a34c18
commit 206c0886a6
3 changed files with 92 additions and 75 deletions

View File

@@ -4,7 +4,7 @@ import type {
} from "@earendil-works/pi-coding-agent"; } from "@earendil-works/pi-coding-agent";
import { registerModule, resetWowConfigCache } from "wow-core"; import { registerModule, resetWowConfigCache } from "wow-core";
import { getStatuslineConfig } from "./config"; import { getStatuslineConfig } from "./config";
import { TAG, log } from "./global"; import { log, TAG } from "./global";
import { createState } from "./state"; import { createState } from "./state";
import { import {
applyStatuslineUI, applyStatuslineUI,
@@ -12,6 +12,8 @@ import {
updateStatuslineState, updateStatuslineState,
} from "./ui"; } from "./ui";
const STREAMING_REFRESH_INTERVAL_MS = 80;
const setup = (pi: ExtensionAPI): void => { const setup = (pi: ExtensionAPI): void => {
const state = createState(); const state = createState();
let lastStreamingRefresh = 0; let lastStreamingRefresh = 0;
@@ -26,47 +28,36 @@ const setup = (pi: ExtensionAPI): void => {
updateStatuslineState(state); updateStatuslineState(state);
}; };
const refreshOnEvent = async (): Promise<void> => {
refresh();
};
pi.on("session_start", async (_event, ctx) => { pi.on("session_start", async (_event, ctx) => {
state.thinkingLevel = pi.getThinkingLevel(); state.thinkingLevel = pi.getThinkingLevel();
apply(ctx); apply(ctx);
log.debug("statusline applied", { cwd: ctx.cwd }); log.debug("statusline applied", { cwd: ctx.cwd });
}); });
pi.on("agent_start", async () => { pi.on("agent_start", refreshOnEvent);
refresh();
});
pi.on("message_update", async () => { pi.on("message_update", async () => {
const now = Date.now(); const now = Date.now();
if (now - lastStreamingRefresh < 80) return; if (now - lastStreamingRefresh < STREAMING_REFRESH_INTERVAL_MS) return;
lastStreamingRefresh = now; lastStreamingRefresh = now;
refresh(); refresh();
}); });
pi.on("model_select", async () => { pi.on("model_select", refreshOnEvent);
refresh();
});
pi.on("thinking_level_select", async (event) => { pi.on("thinking_level_select", async (event) => {
state.thinkingLevel = event.level; state.thinkingLevel = event.level;
refresh(); refresh();
}); });
pi.on("turn_end", async () => { pi.on("turn_end", refreshOnEvent);
refresh(); pi.on("agent_end", refreshOnEvent);
}); pi.on("session_tree", refreshOnEvent);
pi.on("session_compact", refreshOnEvent);
pi.on("agent_end", async () => {
refresh();
});
pi.on("session_tree", async () => {
refresh();
});
pi.on("session_compact", async () => {
refresh();
});
pi.on("session_shutdown", async (_event, ctx) => { pi.on("session_shutdown", async (_event, ctx) => {
clearStatuslineUI(ctx, state); clearStatuslineUI(ctx, state);

View File

@@ -7,7 +7,7 @@ import {
import type { Component, EditorTheme, TUI } from "@earendil-works/pi-tui"; import type { Component, EditorTheme, TUI } from "@earendil-works/pi-tui";
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui"; import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
import { buildEditorTopLine, buildStatusSummary } from "./format"; import { buildEditorTopLine, buildStatusSummary } from "./format";
import { snapshotFromContext, type StatuslineState } from "./state"; import { type StatuslineState, snapshotFromContext } from "./state";
import type { ResolvedStatuslineConfig } from "./types"; import type { ResolvedStatuslineConfig } from "./types";
const ANSI_SGR_RE = /\u001b\[[0-9;]*m/g; const ANSI_SGR_RE = /\u001b\[[0-9;]*m/g;
@@ -50,46 +50,62 @@ export class StatuslineEditor extends CustomEditor {
this.statuslineState, this.statuslineState,
this.extensionCtx, 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 border = theme.fg("borderMuted", "─");
const contentWidth = visibleWidth(content); const borderLine = border.repeat(Math.max(0, width));
const remaining = width - contentWidth - 2; const topContent = `${this.statuslineConfig.editorStyle.folderIcon} ${buildEditorTopLine(theme, snapshot, this.statuslineConfig)}`;
const remainingBorderWidth = width - visibleWidth(topContent) - 2;
lines[0] = lines[0] = buildTopBorderLine(
remaining >= 1 topContent,
? content + theme.fg("dim", " ") + border.repeat(remaining) + border border,
: border.repeat(Math.max(0, width)); borderLine,
theme,
remainingBorderWidth,
);
if (lines.length > 1) { lines[1] =
lines[1] = theme.fg("accent", this.statuslineConfig.editorStyle.prompt) +
theme.fg("accent", this.statuslineConfig.editorStyle.prompt) + theme.fg("dim", " ") +
theme.fg("dim", " ") + (lines[1] ?? "");
(lines[1] ?? "");
}
lines[bottom] = border.repeat(Math.max(0, width)); lines[findBottomBorderIndex(lines)] = borderLine;
for (let index = 0; index < lines.length; index++) { for (let index = 0; index < lines.length; index++) {
if (visibleWidth(lines[index]!) > width) { if (visibleWidth(lines[index]!) <= width) continue;
lines[index] = truncateToWidth(lines[index]!, width); lines[index] = truncateToWidth(lines[index]!, width);
}
} }
return lines; 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( export function buildStatuslineWidget(
ctx: ExtensionContext, ctx: ExtensionContext,
state: StatuslineState, state: StatuslineState,

View File

@@ -1,11 +1,11 @@
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent"; import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
import { buildEditorTopLine, buildStatusSummary, EMPTY_USAGE } from "./format"; import { buildEditorTopLine, buildStatusSummary, EMPTY_USAGE } from "./format";
import { type StatuslineState, syncFooterSnapshot } from "./state";
import { import {
buildStatuslineWidget,
EmptyFooter, EmptyFooter,
StatuslineEditor, StatuslineEditor,
buildStatuslineWidget,
} from "./statusline-editor"; } from "./statusline-editor";
import { syncFooterSnapshot, type StatuslineState } from "./state";
import type { FooterDataSnapshot, ResolvedStatuslineConfig } from "./types"; import type { FooterDataSnapshot, ResolvedStatuslineConfig } from "./types";
const STATUS_WIDGET_ID = "wow-statusline:summary"; const STATUS_WIDGET_ID = "wow-statusline:summary";
@@ -61,26 +61,10 @@ export function applyStatuslineUI(
return; return;
} }
ctx.ui.setWorkingVisible(false);
hijackFooter(ctx, state); hijackFooter(ctx, state);
installEmptyFooter(ctx, state); installEmptyFooter(ctx, state);
installStatusWidget(ctx, state, config);
ctx.ui.setWidget( installEditor(ctx, state, config);
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),
);
} }
export function updateStatuslineState(state: StatuslineState): void { 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: "belowEditor" });
ctx.ui.setWidget(STATUS_WIDGET_ID, undefined, { placement: "aboveEditor" }); ctx.ui.setWidget(STATUS_WIDGET_ID, undefined, { placement: "aboveEditor" });
ctx.ui.setEditorComponent(undefined); ctx.ui.setEditorComponent(undefined);
ctx.ui.setWorkingVisible(true);
ctx.ui.setWorkingIndicator();
} }
function hijackFooter(ctx: ExtensionContext, state: StatuslineState): void { function hijackFooter(ctx: ExtensionContext, state: StatuslineState): void {
@@ -133,6 +115,35 @@ function hijackFooter(ctx: ExtensionContext, state: StatuslineState): void {
state.footerHijacked = true; 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( function installEmptyFooter(
ctx: ExtensionContext, ctx: ExtensionContext,
state: StatuslineState, state: StatuslineState,
@@ -151,7 +162,6 @@ function installEmptyFooter(
) => { ) => {
captureFooterData(state, theme, footerData); captureFooterData(state, theme, footerData);
const dispose = footerData.onBranchChange(() => { const dispose = footerData.onBranchChange(() => {
state.branch = footerData.getGitBranch() || "";
syncFooterSnapshot(state); syncFooterSnapshot(state);
state.requestRender?.(); state.requestRender?.();
}); });