pi-zentui 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,6 +28,7 @@ Zentui brings two popular aesthetics to Pi:
28
28
  - Bordered input box with accent-colored left rail
29
29
  - Model name and provider displayed inside the editor frame
30
30
  - Thinking level indicator when enabled
31
+ - Prompt-box-style user messages matching the ZentUI input chrome
31
32
 
32
33
  ### Git Status Icons
33
34
 
@@ -39,59 +39,11 @@ export type PolishedTuiConfig = {
39
39
 
40
40
  export const configPath = join(getAgentDir(), "zentui.json");
41
41
 
42
- const themeColorTokens = new Set([
43
- "accent",
44
- "border",
45
- "borderAccent",
46
- "borderMuted",
47
- "success",
48
- "error",
49
- "warning",
50
- "muted",
51
- "dim",
52
- "text",
53
- "thinkingText",
54
- "userMessageText",
55
- "customMessageText",
56
- "customMessageLabel",
57
- "toolTitle",
58
- "toolOutput",
59
- "mdHeading",
60
- "mdLink",
61
- "mdLinkUrl",
62
- "mdCode",
63
- "mdCodeBlock",
64
- "mdCodeBlockBorder",
65
- "mdQuote",
66
- "mdQuoteBorder",
67
- "mdHr",
68
- "mdListBullet",
69
- "toolDiffAdded",
70
- "toolDiffRemoved",
71
- "toolDiffContext",
72
- "syntaxComment",
73
- "syntaxKeyword",
74
- "syntaxFunction",
75
- "syntaxVariable",
76
- "syntaxString",
77
- "syntaxNumber",
78
- "syntaxType",
79
- "syntaxOperator",
80
- "syntaxPunctuation",
81
- "thinkingOff",
82
- "thinkingMinimal",
83
- "thinkingLow",
84
- "thinkingMedium",
85
- "thinkingHigh",
86
- "thinkingXhigh",
87
- "bashMode",
88
- ]);
89
-
90
42
  export const defaultConfig: PolishedTuiConfig = {
91
43
  projectRefreshIntervalMs: DEFAULT_PROJECT_REFRESH_INTERVAL_MS,
92
44
  icons: {
93
45
  cwd: "󰝰",
94
- git: "",
46
+ git: "",
95
47
  ahead: "↑",
96
48
  behind: "↓",
97
49
  diverged: "⇕",
@@ -117,62 +69,6 @@ export const defaultConfig: PolishedTuiConfig = {
117
69
  },
118
70
  };
119
71
 
120
- function isHexColor(value: string): boolean {
121
- return /^#(?:[0-9a-fA-F]{6})$/.test(value);
122
- }
123
-
124
- function hexToAnsi(hex: string, isBackground = false): string {
125
- const normalized = hex.slice(1);
126
- const r = Number.parseInt(normalized.slice(0, 2), 16);
127
- const g = Number.parseInt(normalized.slice(2, 4), 16);
128
- const b = Number.parseInt(normalized.slice(4, 6), 16);
129
- return `\x1b[${isBackground ? 48 : 38};2;${r};${g};${b}m`;
130
- }
131
-
132
- const terminalColorCodes = new Map([
133
- ["black", 30],
134
- ["red", 31],
135
- ["green", 32],
136
- ["yellow", 33],
137
- ["blue", 34],
138
- ["purple", 35],
139
- ["cyan", 36],
140
- ["white", 37],
141
- ["bright-black", 90],
142
- ["bright-red", 91],
143
- ["bright-green", 92],
144
- ["bright-yellow", 93],
145
- ["bright-blue", 94],
146
- ["bright-purple", 95],
147
- ["bright-cyan", 96],
148
- ["bright-white", 97],
149
- ]);
150
-
151
- const terminalStyleModifiers = new Map([
152
- ["bold", 1],
153
- ["dim", 2],
154
- ["dimmed", 2],
155
- ["italic", 3],
156
- ["underline", 4],
157
- ]);
158
-
159
- function terminalColorToAnsi(color: string): string | undefined {
160
- const normalized = color.toLowerCase();
161
- const colorCode = terminalColorCodes.get(normalized);
162
- if (colorCode !== undefined) return `${colorCode}`;
163
-
164
- if (/^(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/.test(normalized)) {
165
- return `38;5;${normalized}`;
166
- }
167
-
168
- if (isHexColor(normalized)) return hexToAnsi(normalized).slice(2, -1);
169
- return undefined;
170
- }
171
-
172
- type ThemeLike = {
173
- fg(color: string, text: string): string;
174
- };
175
-
176
72
  type ConfigRecord = Record<string, unknown>;
177
73
 
178
74
  function isRecord(value: unknown): value is ConfigRecord {
@@ -191,36 +87,6 @@ function parseProjectRefreshIntervalMs(value: unknown): number {
191
87
  : defaultConfig.projectRefreshIntervalMs;
192
88
  }
193
89
 
194
- export function colorize(theme: ThemeLike, color: ColorSpec, text: string): string {
195
- if (themeColorTokens.has(color)) {
196
- return theme.fg(color, text);
197
- }
198
- if (isHexColor(color)) {
199
- return `${hexToAnsi(color)}${text}\x1b[39m`;
200
- }
201
- return theme.fg("text", text);
202
- }
203
-
204
- export function renderTerminalStyle(style: string, text: string): string {
205
- const codes: string[] = [];
206
- for (const token of style.trim().split(/\s+/)) {
207
- if (!token) continue;
208
-
209
- const normalized = token.toLowerCase();
210
- const modifier = terminalStyleModifiers.get(normalized);
211
- if (modifier !== undefined) {
212
- codes.push(`${modifier}`);
213
- continue;
214
- }
215
-
216
- const foreground = normalized.startsWith("fg:") ? normalized.slice(3) : normalized;
217
- const color = terminalColorToAnsi(foreground);
218
- if (color) codes.push(color);
219
- }
220
-
221
- return codes.length ? `\x1b[${codes.join(";")}m${text}\x1b[0m` : text;
222
- }
223
-
224
90
  export function ensureConfigExists(): void {
225
91
  try {
226
92
  if (!existsSync(configPath)) {
@@ -0,0 +1,97 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
+ import type { PolishedTuiConfig } from "./config";
4
+ import { formatCwdLabel, formatRuntimeSegment } from "./format";
5
+ import type { FooterState } from "./state";
6
+ import { colorize } from "./style";
7
+
8
+ export function installFooter(
9
+ ctx: ExtensionContext,
10
+ state: FooterState,
11
+ config: PolishedTuiConfig,
12
+ hooks: {
13
+ setRequestRender: (fn: (() => void) | undefined) => void;
14
+ scheduleProjectRefresh: (ctx: ExtensionContext) => void;
15
+ },
16
+ ): void {
17
+ ctx.ui.setFooter((tui, theme, footerData) => {
18
+ hooks.setRequestRender(() => tui.requestRender());
19
+ const unsubscribeBranch = footerData.onBranchChange(() => {
20
+ hooks.scheduleProjectRefresh(ctx);
21
+ tui.requestRender();
22
+ });
23
+ const separator = colorize(theme, config.colors.separator, " | ");
24
+
25
+ return {
26
+ dispose: () => {
27
+ unsubscribeBranch();
28
+ hooks.setRequestRender(undefined);
29
+ },
30
+ invalidate() {},
31
+ render(width: number): string[] {
32
+ if (width <= 0) return [""];
33
+ const innerWidth = Math.max(1, width - 2);
34
+ const cwdLabel = colorize(
35
+ theme,
36
+ config.colors.cwdText,
37
+ formatCwdLabel(ctx.cwd, config.icons.cwd),
38
+ );
39
+ const branch = state.branch;
40
+ const contextUsage = ctx.getContextUsage();
41
+ const contextColor =
42
+ contextUsage?.percent !== null && contextUsage?.percent !== undefined
43
+ ? contextUsage.percent >= 90
44
+ ? config.colors.contextError
45
+ : contextUsage.percent >= 70
46
+ ? config.colors.contextWarning
47
+ : config.colors.contextNormal
48
+ : config.colors.contextNormal;
49
+ const gitColor = (text: string) => colorize(theme, config.colors.git, text);
50
+ const gitStatusColor = (text: string) => colorize(theme, config.colors.gitStatus, text);
51
+ const gitIcon = gitColor(config.icons.git);
52
+ const allStatus = [
53
+ state.conflicted > 0 ? config.icons.conflicted : "",
54
+ state.stashed ? config.icons.stashed : "",
55
+ state.deleted > 0 ? config.icons.deleted : "",
56
+ state.renamed > 0 ? config.icons.renamed : "",
57
+ state.modified > 0 ? config.icons.modified : "",
58
+ state.typechanged > 0 ? config.icons.typechanged : "",
59
+ state.staged > 0 ? config.icons.staged : "",
60
+ state.untracked > 0 ? config.icons.untracked : "",
61
+ ].join("");
62
+ const aheadBehind =
63
+ state.ahead > 0 && state.behind > 0
64
+ ? config.icons.diverged
65
+ : state.ahead > 0
66
+ ? config.icons.ahead
67
+ : state.behind > 0
68
+ ? config.icons.behind
69
+ : "";
70
+ const statusBlock =
71
+ allStatus || aheadBehind ? gitStatusColor(`[${allStatus}${aheadBehind}]`) : "";
72
+ const branchLabel = branch
73
+ ? `${colorize(theme, "text", "on")} ${gitIcon} ${gitColor(branch)}${statusBlock ? ` ${statusBlock}` : ""}`
74
+ : "";
75
+ const runtimeLabel = formatRuntimeSegment(theme, state.runtime, "text");
76
+
77
+ const left = [cwdLabel, branchLabel, runtimeLabel].filter(Boolean).join(" ");
78
+ const right = [
79
+ colorize(theme, contextColor, state.contextLabel),
80
+ colorize(theme, config.colors.tokens, state.tokenLabel),
81
+ colorize(theme, config.colors.cost, state.costLabel),
82
+ ].join(separator);
83
+
84
+ const leftWidth = visibleWidth(left);
85
+ const rightWidth = visibleWidth(right);
86
+ const content =
87
+ leftWidth >= innerWidth
88
+ ? truncateToWidth(left, innerWidth, "")
89
+ : leftWidth + 1 + rightWidth <= innerWidth
90
+ ? `${left}${" ".repeat(innerWidth - leftWidth - rightWidth)}${right}`
91
+ : truncateToWidth(left, innerWidth, "");
92
+ const framed = width > 2 ? ` ${truncateToWidth(content, width - 2, "")} ` : content;
93
+ return [truncateToWidth(framed, width, "")];
94
+ },
95
+ };
96
+ });
97
+ }
@@ -0,0 +1,85 @@
1
+ import type { AssistantMessage } from "@earendil-works/pi-ai";
2
+ import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
3
+ import type { RuntimeInfo } from "./runtime";
4
+ import { type ThemeLike, colorize, renderTerminalStyle } from "./style";
5
+
6
+ export type UsageTotals = {
7
+ input: number;
8
+ output: number;
9
+ cost: number;
10
+ };
11
+
12
+ export function formatCount(value: number): string {
13
+ if (value < 1000) return `${value}`;
14
+ if (value < 10_000) return `${(value / 1000).toFixed(1)}k`;
15
+ return `${Math.round(value / 1000)}k`;
16
+ }
17
+
18
+ export function formatProviderLabel(provider: string | undefined): string {
19
+ if (!provider) return "Unknown";
20
+
21
+ const known: Record<string, string> = {
22
+ anthropic: "Anthropic",
23
+ gemini: "Google",
24
+ google: "Google",
25
+ ollama: "Ollama",
26
+ openai: "OpenAI",
27
+ "openai-codex": "OpenAI",
28
+ };
29
+
30
+ return (
31
+ known[provider] ?? provider.replace(/[-_]/g, " ").replace(/\b\w/g, (char) => char.toUpperCase())
32
+ );
33
+ }
34
+
35
+ export function getUsageTotals(ctx: ExtensionContext): UsageTotals {
36
+ let input = 0;
37
+ let output = 0;
38
+ let cost = 0;
39
+
40
+ for (const entry of ctx.sessionManager.getBranch()) {
41
+ if (entry.type !== "message" || entry.message.role !== "assistant") continue;
42
+ const message = entry.message as AssistantMessage;
43
+ input += message.usage?.input ?? 0;
44
+ output += message.usage?.output ?? 0;
45
+ cost += message.usage?.cost?.total ?? 0;
46
+ }
47
+
48
+ return { input, output, cost };
49
+ }
50
+
51
+ export function buildTokenLabel(totals: UsageTotals): string {
52
+ return `↑${formatCount(totals.input)} ↓${formatCount(totals.output)}`;
53
+ }
54
+
55
+ export function buildCostLabel(totals: UsageTotals): string {
56
+ return `$${totals.cost.toFixed(3)}`;
57
+ }
58
+
59
+ export function buildContextLabel(ctx: ExtensionContext): string {
60
+ const usage = ctx.getContextUsage();
61
+ const contextWindow = ctx.model?.contextWindow ?? usage?.contextWindow;
62
+
63
+ if (!usage || !contextWindow || contextWindow <= 0) return "--";
64
+
65
+ const percent =
66
+ usage.percent === null ? "?" : `${Math.max(0, Math.min(999, Math.round(usage.percent)))}%`;
67
+ return `${percent}/${formatCount(contextWindow)}`;
68
+ }
69
+
70
+ export function formatRuntimeSegment(
71
+ theme: Pick<Theme, "fg">,
72
+ runtime: RuntimeInfo | undefined,
73
+ mutedColor: string,
74
+ ): string {
75
+ if (!runtime) return "";
76
+ const label = runtime.version ? `${runtime.symbol} ${runtime.version}` : runtime.symbol;
77
+ return `${colorize(theme, mutedColor, "via")} ${renderTerminalStyle(runtime.style, label)}`;
78
+ }
79
+
80
+ export function formatCwdLabel(cwd: string, cwdIcon: string): string {
81
+ const normalized = cwd.replace(/\\/g, "/").replace(/\/+$/, "");
82
+ const parts = normalized.split("/").filter(Boolean);
83
+ const last = parts[parts.length - 1] ?? cwd;
84
+ return cwdIcon ? `${cwdIcon} ${last}` : last;
85
+ }
@@ -1,159 +1,31 @@
1
- import type { AssistantMessage } from "@earendil-works/pi-ai";
2
1
  import type {
3
2
  ExtensionAPI,
4
3
  ExtensionContext,
5
4
  KeybindingsManager,
6
5
  Theme,
7
6
  } from "@earendil-works/pi-coding-agent";
8
- import { type EditorTheme, type TUI, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
9
- import {
10
- type PolishedTuiConfig,
11
- colorize,
12
- ensureConfigExists,
13
- loadConfig,
14
- renderTerminalStyle,
15
- } from "./config";
16
- import { type GitStatusSummary, emptyGitStatus, readGitStatus } from "./git";
7
+ import type { EditorTheme, TUI } from "@earendil-works/pi-tui";
8
+ import { type PolishedTuiConfig, ensureConfigExists, loadConfig } from "./config";
9
+ import { installFooter } from "./footer";
10
+ import { emptyGitStatus, readGitStatus } from "./git";
17
11
  import { type StopProjectRefreshInterval, startProjectRefreshInterval } from "./project-refresh";
18
- import { type RuntimeInfo, readRuntimeInfo } from "./runtime";
12
+ import { readRuntimeInfo } from "./runtime";
13
+ import { type FooterState, createInitialState, syncState } from "./state";
19
14
  import { PolishedEditor } from "./ui";
20
-
21
- type FooterState = GitStatusSummary & {
22
- modelLabel: string;
23
- providerLabel: string;
24
- contextLabel: string;
25
- tokenLabel: string;
26
- costLabel: string;
27
- runtime?: RuntimeInfo;
28
- };
29
-
30
- type UsageTotals = {
31
- input: number;
32
- output: number;
33
- cost: number;
34
- };
35
-
36
- function formatCount(value: number): string {
37
- if (value < 1000) return `${value}`;
38
- if (value < 10_000) return `${(value / 1000).toFixed(1)}k`;
39
- return `${Math.round(value / 1000)}k`;
40
- }
41
-
42
- function formatProviderLabel(provider: string | undefined): string {
43
- if (!provider) return "Unknown";
44
-
45
- const known: Record<string, string> = {
46
- anthropic: "Anthropic",
47
- gemini: "Google",
48
- google: "Google",
49
- ollama: "Ollama",
50
- openai: "OpenAI",
51
- "openai-codex": "OpenAI",
52
- };
53
-
54
- return (
55
- known[provider] ?? provider.replace(/[-_]/g, " ").replace(/\b\w/g, (char) => char.toUpperCase())
56
- );
57
- }
58
-
59
- function getUsageTotals(ctx: ExtensionContext): UsageTotals {
60
- let input = 0;
61
- let output = 0;
62
- let cost = 0;
63
-
64
- for (const entry of ctx.sessionManager.getBranch()) {
65
- if (entry.type !== "message" || entry.message.role !== "assistant") continue;
66
- const message = entry.message as AssistantMessage;
67
- input += message.usage?.input ?? 0;
68
- output += message.usage?.output ?? 0;
69
- cost += message.usage?.cost?.total ?? 0;
70
- }
71
-
72
- return { input, output, cost };
73
- }
74
-
75
- function buildTokenLabel(totals: UsageTotals): string {
76
- return `↑${formatCount(totals.input)} ↓${formatCount(totals.output)}`;
77
- }
78
-
79
- function buildCostLabel(totals: UsageTotals): string {
80
- return `$${totals.cost.toFixed(3)}`;
81
- }
82
-
83
- function buildContextLabel(ctx: ExtensionContext): string {
84
- const usage = ctx.getContextUsage();
85
- const contextWindow = ctx.model?.contextWindow ?? usage?.contextWindow;
86
-
87
- if (!usage || !contextWindow || contextWindow <= 0) return "--";
88
-
89
- const percent =
90
- usage.percent === null ? "?" : `${Math.max(0, Math.min(999, Math.round(usage.percent)))}%`;
91
- return `${percent}/${formatCount(contextWindow)}`;
92
- }
93
-
94
- function formatRuntimeSegment(
95
- theme: Pick<Theme, "fg">,
96
- runtime: RuntimeInfo | undefined,
97
- mutedColor: string,
98
- ): string {
99
- if (!runtime) return "";
100
- const label = runtime.version ? `${runtime.symbol} ${runtime.version}` : runtime.symbol;
101
- return `${colorize(theme, mutedColor, "via")} ${renderTerminalStyle(runtime.style, label)}`;
102
- }
103
-
104
- function formatCwdLabel(cwd: string, cwdIcon: string): string {
105
- const normalized = cwd.replace(/\\/g, "/").replace(/\/+$/, "");
106
- const parts = normalized.split("/").filter(Boolean);
107
- const last = parts[parts.length - 1] ?? cwd;
108
- return cwdIcon ? `${cwdIcon} ${last}` : last;
109
- }
15
+ import { installUserMessageStyle } from "./user-message";
110
16
 
111
17
  export default function (pi: ExtensionAPI) {
112
- const state: FooterState = {
113
- modelLabel: "no-model",
114
- providerLabel: "Unknown",
115
- contextLabel: "--",
116
- tokenLabel: "↑0 ↓0",
117
- costLabel: "$0.000",
118
- runtime: undefined,
119
- ...emptyGitStatus(),
120
- };
18
+ const state: FooterState = createInitialState(emptyGitStatus());
121
19
 
122
20
  let currentConfig: PolishedTuiConfig = loadConfig();
21
+ let activeTheme: Theme | undefined;
123
22
  let requestFooterRender: (() => void) | undefined;
124
- let stopProjectRefreshInterval: StopProjectRefreshInterval = () => {};
23
+ let stopRefreshInterval: StopProjectRefreshInterval = () => {};
125
24
  let projectRefreshInFlight = false;
126
25
  let projectRefreshPending = false;
127
26
 
128
27
  const refresh = () => requestFooterRender?.();
129
-
130
- const cleanupUi = (ctx?: ExtensionContext) => {
131
- stopProjectRefreshInterval();
132
- stopProjectRefreshInterval = () => {};
133
- projectRefreshInFlight = false;
134
- projectRefreshPending = false;
135
- requestFooterRender = undefined;
136
- if (ctx?.hasUI) {
137
- ctx.ui.setFooter(undefined);
138
- ctx.ui.setEditorComponent(undefined);
139
- }
140
- };
141
-
142
- const refreshInteractiveState = (ctx: ExtensionContext, project = false) => {
143
- if (!ctx.hasUI) return;
144
- syncState(ctx);
145
- if (project) scheduleProjectRefresh(ctx);
146
- refresh();
147
- };
148
-
149
- const syncState = (ctx: ExtensionContext) => {
150
- const totals = getUsageTotals(ctx);
151
- state.modelLabel = ctx.model?.id ?? "no-model";
152
- state.providerLabel = formatProviderLabel(ctx.model?.provider);
153
- state.contextLabel = buildContextLabel(ctx);
154
- state.tokenLabel = buildTokenLabel(totals);
155
- state.costLabel = buildCostLabel(totals);
156
- };
28
+ const getActiveTheme = () => activeTheme;
157
29
 
158
30
  const refreshProjectState = async (ctx: ExtensionContext) => {
159
31
  const [gitStatus, runtime] = await Promise.all([
@@ -181,127 +53,66 @@ export default function (pi: ExtensionAPI) {
181
53
  });
182
54
  };
183
55
 
184
- const installFooter = (ctx: ExtensionContext) => {
185
- syncState(ctx);
186
-
187
- ctx.ui.setFooter((tui, theme, footerData) => {
188
- requestFooterRender = () => tui.requestRender();
189
- const unsubscribeBranch = footerData.onBranchChange(() => {
190
- scheduleProjectRefresh(ctx);
191
- tui.requestRender();
192
- });
193
- const separator = colorize(theme, currentConfig.colors.separator, " | ");
194
-
195
- return {
196
- dispose: () => {
197
- unsubscribeBranch();
198
- requestFooterRender = undefined;
199
- },
200
- invalidate() {},
201
- render(width: number): string[] {
202
- if (width <= 0) return [""];
203
- const innerWidth = Math.max(1, width - 2);
204
- const cwdLabel = colorize(
205
- theme,
206
- currentConfig.colors.cwdText,
207
- formatCwdLabel(ctx.cwd, currentConfig.icons.cwd),
208
- );
209
- const branch = state.branch;
210
- const contextUsage = ctx.getContextUsage();
211
- const contextColor =
212
- contextUsage?.percent !== null && contextUsage?.percent !== undefined
213
- ? contextUsage.percent >= 90
214
- ? currentConfig.colors.contextError
215
- : contextUsage.percent >= 70
216
- ? currentConfig.colors.contextWarning
217
- : currentConfig.colors.contextNormal
218
- : currentConfig.colors.contextNormal;
219
- const gitColor = (text: string) => colorize(theme, currentConfig.colors.git, text);
220
- const gitStatusColor = (text: string) =>
221
- colorize(theme, currentConfig.colors.gitStatus, text);
222
- const gitIcon = gitColor(currentConfig.icons.git);
223
- const allStatus = [
224
- state.conflicted > 0 ? currentConfig.icons.conflicted : "",
225
- state.stashed ? currentConfig.icons.stashed : "",
226
- state.deleted > 0 ? currentConfig.icons.deleted : "",
227
- state.renamed > 0 ? currentConfig.icons.renamed : "",
228
- state.modified > 0 ? currentConfig.icons.modified : "",
229
- state.typechanged > 0 ? currentConfig.icons.typechanged : "",
230
- state.staged > 0 ? currentConfig.icons.staged : "",
231
- state.untracked > 0 ? currentConfig.icons.untracked : "",
232
- ].join("");
233
- const aheadBehind =
234
- state.ahead > 0 && state.behind > 0
235
- ? currentConfig.icons.diverged
236
- : state.ahead > 0
237
- ? currentConfig.icons.ahead
238
- : state.behind > 0
239
- ? currentConfig.icons.behind
240
- : "";
241
- const statusBlock =
242
- allStatus || aheadBehind ? gitStatusColor(`[${allStatus}${aheadBehind}]`) : "";
243
- const branchLabel = branch
244
- ? `${colorize(theme, "text", "on")} ${gitIcon} ${gitColor(branch)}${statusBlock ? ` ${statusBlock}` : ""}`
245
- : "";
246
- const runtimeLabel = formatRuntimeSegment(theme, state.runtime, "text");
247
-
248
- const left = [cwdLabel, branchLabel, runtimeLabel].filter(Boolean).join(" ");
249
- const right = [
250
- colorize(theme, contextColor, state.contextLabel),
251
- colorize(theme, currentConfig.colors.tokens, state.tokenLabel),
252
- colorize(theme, currentConfig.colors.cost, state.costLabel),
253
- ].join(separator);
254
-
255
- const leftWidth = visibleWidth(left);
256
- const rightWidth = visibleWidth(right);
257
- const content =
258
- leftWidth >= innerWidth
259
- ? truncateToWidth(left, innerWidth, "")
260
- : leftWidth + 1 + rightWidth <= innerWidth
261
- ? `${left}${" ".repeat(innerWidth - leftWidth - rightWidth)}${right}`
262
- : truncateToWidth(left, innerWidth, "");
263
- const framed = width > 2 ? ` ${truncateToWidth(content, width - 2, "")} ` : content;
264
- return [truncateToWidth(framed, width, "")];
265
- },
266
- };
267
- });
56
+ const refreshInteractiveState = (ctx: ExtensionContext, project = false) => {
57
+ if (!ctx.hasUI) return;
58
+ syncState(state, ctx);
59
+ if (project) scheduleProjectRefresh(ctx);
60
+ refresh();
268
61
  };
269
62
 
270
63
  const installEditor = (ctx: ExtensionContext) => {
271
- syncState(ctx);
272
-
273
- const editorFactory = (tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) =>
274
- new PolishedEditor(
275
- tui,
276
- theme,
277
- keybindings,
278
- ctx.ui.theme,
279
- () =>
280
- [
281
- ctx.ui.theme.fg("accent", state.modelLabel),
282
- ctx.ui.theme.fg("text", state.providerLabel),
283
- ].join(ctx.ui.theme.fg("borderMuted", " ")),
284
- () => pi.getThinkingLevel(),
285
- );
286
-
287
- ctx.ui.setEditorComponent(editorFactory);
64
+ ctx.ui.setEditorComponent(
65
+ (tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) =>
66
+ new PolishedEditor(
67
+ tui,
68
+ theme,
69
+ keybindings,
70
+ ctx.ui.theme,
71
+ () =>
72
+ [
73
+ ctx.ui.theme.fg("accent", state.modelLabel),
74
+ ctx.ui.theme.fg("text", state.providerLabel),
75
+ ].join(ctx.ui.theme.fg("borderMuted", " ")),
76
+ () => pi.getThinkingLevel(),
77
+ ),
78
+ );
288
79
  };
289
80
 
290
81
  const installUi = (ctx: ExtensionContext) => {
291
82
  if (!ctx.hasUI) return;
83
+ activeTheme = ctx.ui.theme;
84
+ installUserMessageStyle(getActiveTheme);
292
85
  ensureConfigExists();
293
86
  currentConfig = loadConfig();
294
- installFooter(ctx);
87
+ syncState(state, ctx);
88
+ installFooter(ctx, state, currentConfig, {
89
+ setRequestRender: (fn) => {
90
+ requestFooterRender = fn;
91
+ },
92
+ scheduleProjectRefresh,
93
+ });
295
94
  installEditor(ctx);
296
- stopProjectRefreshInterval();
297
- stopProjectRefreshInterval = startProjectRefreshInterval(
298
- currentConfig.projectRefreshIntervalMs,
299
- () => scheduleProjectRefresh(ctx),
95
+ stopRefreshInterval();
96
+ stopRefreshInterval = startProjectRefreshInterval(currentConfig.projectRefreshIntervalMs, () =>
97
+ scheduleProjectRefresh(ctx),
300
98
  );
301
99
  scheduleProjectRefresh(ctx);
302
100
  refresh();
303
101
  };
304
102
 
103
+ const cleanupUi = (ctx?: ExtensionContext) => {
104
+ stopRefreshInterval();
105
+ stopRefreshInterval = () => {};
106
+ projectRefreshInFlight = false;
107
+ projectRefreshPending = false;
108
+ requestFooterRender = undefined;
109
+ if (ctx?.hasUI) {
110
+ ctx.ui.setFooter(undefined);
111
+ ctx.ui.setEditorComponent(undefined);
112
+ }
113
+ activeTheme = undefined;
114
+ };
115
+
305
116
  pi.on("session_start", async (_event, ctx) => {
306
117
  installUi(ctx);
307
118
  });