import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent"; import { inspectGate } from "../src/harness/gates.ts"; import { readActiveRun } from "../src/harness/state.ts"; import type { ActiveRun, GateResult } from "../src/harness/types.ts"; import { phaseOrderForRun } from "../src/harness/types.ts"; import { formatProductProfile } from "../src/product/profile.ts"; /** ANSI escape sequence pattern: CSI, OSC, APC. */ const ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]|\x1b\][^\x07]*\x07|\x1b_[^\x1b]*\x1b\\/g; /** Visible width of a string (strips ANSI codes; CJK/wide chars = 2 columns). */ function visibleWidth(str: string): number { let width = 0; const clean = str.replace(ANSI_RE, ""); for (const ch of clean) { const code = ch.codePointAt(0)!; width += code >= 0x1100 && !(code >= 0x00a0 && code <= 0x00ff) && ((code >= 0x1100 && code <= 0x115f) || (code >= 0x2329 && code <= 0x232a) || (code >= 0x2e80 && code <= 0x303e) || (code >= 0x3040 && code <= 0x3247) || (code >= 0x3250 && code <= 0x4dbf) || (code >= 0x4e00 && code <= 0xa4c6) || (code >= 0xa960 && code <= 0xa97c) || (code >= 0xac00 && code <= 0xd7a3) || (code >= 0xf900 && code <= 0xfaff) || (code >= 0xfe10 && code <= 0xfe19) || (code >= 0xfe30 && code <= 0xfe6b) || (code >= 0xff01 && code <= 0xff60) || (code >= 0xffe0 && code <= 0xffe6) || (code >= 0x1f300 && code <= 0x1f9ff) || (code >= 0x20000 && code <= 0x2fffd)) ? 2 : 1; } return width; } /** * If the line's visible width exceeds `maxWidth`, truncate visible characters * and append `>` so the result fits. Preserves ANSI codes in the kept portion * and appends SGR reset before the `>`. No padding — pi-tui only requires * visibleWidth <= width. */ export function clipLine(text: string, maxWidth: number): string { if (maxWidth <= 0) return ""; const vw = visibleWidth(text); if (vw <= maxWidth) return text; const targetW = maxWidth - 1; // reserve 1 col for ">" let result = ""; let visibleSoFar = 0; let i = 0; while (i < text.length && visibleSoFar < targetW) { // Preserve ANSI escape sequences if (text[i] === "\x1b") { const m = text.slice(i).match(/^\x1b(?:\[[0-9;]*[A-Za-z]|\][^\x07]*\x07|_[^\x1b]*\x1b\\)/); if (m) { result += m[0]; i += m[0].length; continue; } } const ch = text[i]; const code = ch.codePointAt(0)!; const wide = (code >= 0x1100 && code <= 0x115f) || (code >= 0x2329 && code <= 0x232a) || (code >= 0x2e80 && code <= 0x303e) || (code >= 0x3040 && code <= 0x3247) || (code >= 0x3250 && code <= 0x4dbf) || (code >= 0x4e00 && code <= 0xa4c6) || (code >= 0xa960 && code <= 0xa97c) || (code >= 0xac00 && code <= 0xd7a3) || (code >= 0xf900 && code <= 0xfaff) || (code >= 0xfe10 && code <= 0xfe19) || (code >= 0xfe30 && code <= 0xfe6b) || (code >= 0xff01 && code <= 0xff60) || (code >= 0xffe0 && code <= 0xffe6) || (code >= 0x1f300 && code <= 0x1f9ff) || (code >= 0x20000 && code <= 0x2fffd); const charW = wide ? 2 : 1; if (visibleSoFar + charW > targetW) break; result += ch; visibleSoFar += charW; i++; } return result + "\x1b[0m>"; } function formatProduct(run: ActiveRun | undefined): string { if (!run) return "未选择"; if (run.profile?.product === "unknown") return "未确认"; return formatProductProfile(run.profile); } function formatPhase(phase: ActiveRun["phase"] | undefined): string { return phase ?? "空闲"; } function formatGate(gate: GateResult | undefined): string { if (!gate) return "门禁:待检查"; return gate.passed ? "门禁:通过" : "门禁:阻塞"; } function safeInspectGate(cwd: string, run: ActiveRun | undefined): GateResult | undefined { if (!run) return undefined; try { return inspectGate(cwd, run); } catch (error) { return { passed: false, reason: error instanceof Error ? error.message : String(error), checkedAt: new Date().toISOString(), }; } } function riskLevel(run: ActiveRun | undefined): string { return run?.riskAssessment?.level ?? "未知"; } function riskColor(risk: string): "error" | "warning" | "muted" | "success" { if (risk === "high") return "error"; if (risk === "medium") return "warning"; if (risk === "未知") return "muted"; return "success"; } function logoLines(theme: Theme): string[] { const accent = (text: string) => theme.fg("accent", text); const muted = (text: string) => theme.fg("muted", text); return [ `${accent("KCode")} ${muted("金蝶 Pi Harness")}`, `${accent("=====")} ${muted("lite | standard | strict")}`, ]; } export default function (pi: ExtensionAPI) { pi.on("session_start", async (_event, ctx) => { if (ctx.mode !== "tui") return; ctx.ui.setHeader((_tui, theme) => { return { render(width: number): string[] { const run = readActiveRun(ctx.cwd); const gateState = safeInspectGate(ctx.cwd, run); const phase = formatPhase(run?.phase); const product = formatProduct(run); const gate = formatGate(gateState); const risk = riskLevel(run); const runId = run?.id ?? "无"; const mode = run?.mode ?? "未启动"; const order = run ? phaseOrderForRun(run).join(" -> ") : "未启动"; const status = [ theme.fg("muted", "阶段:"), theme.fg("accent", phase), theme.fg("muted", " | 模式:"), theme.fg("text", mode), theme.fg("muted", " | 产品:"), theme.fg("text", product), theme.fg("muted", " | 风险:"), theme.fg(riskColor(risk), risk), theme.fg("muted", " | "), theme.fg(gateState?.passed === false ? "error" : "muted", gate), ].join(""); return [ "", ...logoLines(theme).map((line) => clipLine(line, width)), clipLine(status, width), clipLine(theme.fg("dim", `流程:${order}`), width), clipLine(theme.fg("dim", `run:${runId}`), width), "", ]; }, invalidate() {}, }; }); }); pi.registerCommand("kcode-header", { description: "恢复 KCode 金蝶标题栏", handler: async (_args, ctx) => { if (ctx.mode !== "tui") return; ctx.ui.notify("重启会话或重新加载扩展以恢复 KCode 标题栏。", "info"); }, }); pi.registerCommand("builtin-header", { description: "恢复 Pi 内置标题栏", handler: async (_args, ctx) => { if (ctx.mode !== "tui") return; ctx.ui.setHeader(undefined); ctx.ui.notify("已恢复 Pi 内置标题栏", "info"); }, }); }