pi-zentui 0.2.1 → 0.2.5

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.
@@ -2,6 +2,7 @@ import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
 
4
4
  const execFileAsync = promisify(execFile);
5
+ const GIT_COMMAND_TIMEOUT_MS = 2_000;
5
6
 
6
7
  export type GitStatusSummary = {
7
8
  branch?: string;
@@ -88,10 +89,14 @@ export function parseGitStatusPorcelain(stdoutText: string, hasStash: boolean):
88
89
  export async function readGitStatus(cwd: string): Promise<GitStatusSummary> {
89
90
  try {
90
91
  const [{ stdout: statusStdout }, stashResult] = await Promise.all([
91
- execFileAsync("git", ["status", "--porcelain=2", "--branch"], { cwd }),
92
- execFileAsync("git", ["rev-parse", "--verify", "--quiet", "refs/stash"], { cwd }).catch(
93
- () => ({ stdout: "" }),
94
- ),
92
+ execFileAsync("git", ["status", "--porcelain=2", "--branch"], {
93
+ cwd,
94
+ timeout: GIT_COMMAND_TIMEOUT_MS,
95
+ }),
96
+ execFileAsync("git", ["rev-parse", "--verify", "--quiet", "refs/stash"], {
97
+ cwd,
98
+ timeout: GIT_COMMAND_TIMEOUT_MS,
99
+ }).catch(() => ({ stdout: "" })),
95
100
  ]);
96
101
  const stdoutText = typeof statusStdout === "string" ? statusStdout : String(statusStdout);
97
102
  const stashStdout =
@@ -18,7 +18,12 @@ import {
18
18
  } from "./config";
19
19
  import { installFooter } from "./footer";
20
20
  import { emptyGitStatus, readGitStatus } from "./git";
21
- import { type StopProjectRefreshInterval, startProjectRefreshInterval } from "./project-refresh";
21
+ import {
22
+ type ScheduleProjectRefreshOptions,
23
+ type StopProjectRefreshInterval,
24
+ createProjectRefreshScheduler,
25
+ startProjectRefreshInterval,
26
+ } from "./project-refresh";
22
27
  import { readRuntimeInfo } from "./runtime";
23
28
  import { installSelectorBorderStyle } from "./selector-border";
24
29
  import { registerZentuiSettingsCommand } from "./settings-command";
@@ -27,11 +32,13 @@ import { PolishedEditor, WrappedPolishedEditor } from "./ui";
27
32
  import { installUserMessageStyle } from "./user-message";
28
33
 
29
34
  const ZENTUI_EDITOR_FACTORY = Symbol.for("pi-zentui.editor-factory");
35
+ const ZENTUI_EDITOR_BASE_FACTORY = Symbol.for("pi-zentui.editor-base-factory");
30
36
 
31
37
  type EditorFactory = NonNullable<Parameters<ExtensionContext["ui"]["setEditorComponent"]>[0]>;
32
38
 
33
39
  type ZentuiEditorFactory = EditorFactory & {
34
40
  [ZENTUI_EDITOR_FACTORY]?: true;
41
+ [ZENTUI_EDITOR_BASE_FACTORY]?: EditorFactory;
35
42
  };
36
43
 
37
44
  type ApplyUiResult = {
@@ -44,6 +51,15 @@ function isZentuiEditorFactory(factory: EditorFactory | undefined): boolean {
44
51
  return Boolean((factory as ZentuiEditorFactory | undefined)?.[ZENTUI_EDITOR_FACTORY]);
45
52
  }
46
53
 
54
+ function getZentuiEditorBaseFactory(factory: EditorFactory | undefined): EditorFactory | undefined {
55
+ return (factory as ZentuiEditorFactory | undefined)?.[ZENTUI_EDITOR_BASE_FACTORY];
56
+ }
57
+
58
+ function isTuiContext(ctx: ExtensionContext): boolean {
59
+ const mode = (ctx as ExtensionContext & { mode?: string }).mode;
60
+ return ctx.hasUI && (mode === undefined || mode === "tui");
61
+ }
62
+
47
63
  export default function (pi: ExtensionAPI) {
48
64
  const state: FooterState = createInitialState(emptyGitStatus());
49
65
 
@@ -56,10 +72,9 @@ export default function (pi: ExtensionAPI) {
56
72
  let footerInstalled = false;
57
73
  let editorInstalled = false;
58
74
  let editorInstallMode: EditorInstallMode = "none";
75
+ let installedEditorFactory: EditorFactory | undefined;
59
76
  let wrappedEditorFactory: EditorFactory | undefined;
60
77
  let prototypePatchesInstalled = false;
61
- let projectRefreshInFlight = false;
62
- let projectRefreshPending = false;
63
78
 
64
79
  const refresh = () => requestFooterRender?.();
65
80
  const getActiveTheme = () => activeTheme;
@@ -77,22 +92,9 @@ export default function (pi: ExtensionAPI) {
77
92
  state.runtime = runtime;
78
93
  };
79
94
 
80
- const scheduleProjectRefresh = (ctx: ExtensionContext) => {
81
- if (projectRefreshInFlight) {
82
- projectRefreshPending = true;
83
- return;
84
- }
85
-
86
- projectRefreshInFlight = true;
87
- void refreshProjectState(ctx).finally(() => {
88
- projectRefreshInFlight = false;
89
- refresh();
90
- if (projectRefreshPending) {
91
- projectRefreshPending = false;
92
- scheduleProjectRefresh(ctx);
93
- }
94
- });
95
- };
95
+ const projectRefreshScheduler = createProjectRefreshScheduler(refreshProjectState, refresh);
96
+ const scheduleProjectRefresh = (ctx: ExtensionContext, options?: ScheduleProjectRefreshOptions) =>
97
+ projectRefreshScheduler.schedule(ctx, options);
96
98
 
97
99
  const refreshInteractiveState = (ctx: ExtensionContext, project = false) => {
98
100
  if (!ctx.hasUI) return;
@@ -104,8 +106,7 @@ export default function (pi: ExtensionAPI) {
104
106
  const stopProjectRefresh = () => {
105
107
  stopRefreshInterval();
106
108
  stopRefreshInterval = () => {};
107
- projectRefreshInFlight = false;
108
- projectRefreshPending = false;
109
+ projectRefreshScheduler.stop();
109
110
  };
110
111
 
111
112
  const installPrototypePatches = () => {
@@ -159,24 +160,38 @@ export default function (pi: ExtensionAPI) {
159
160
  getThinkingLevel,
160
161
  )) as ZentuiEditorFactory;
161
162
  factory[ZENTUI_EDITOR_FACTORY] = true;
163
+ factory[ZENTUI_EDITOR_BASE_FACTORY] = baseFactory;
162
164
  return factory;
163
165
  };
164
166
 
165
167
  const installEditor = (ctx: ExtensionContext): boolean => {
166
168
  const currentFactory = ctx.ui.getEditorComponent();
167
- if (currentFactory && isZentuiEditorFactory(currentFactory)) {
169
+ if (currentFactory && currentFactory === installedEditorFactory) {
168
170
  editorInstalled = true;
169
171
  return true;
170
172
  }
171
173
 
172
174
  installPrototypePatches();
173
- if (currentFactory) {
175
+ const currentZentuiBaseFactory = getZentuiEditorBaseFactory(currentFactory);
176
+ if (currentFactory && isZentuiEditorFactory(currentFactory)) {
177
+ wrappedEditorFactory = currentZentuiBaseFactory;
178
+ const nextFactory = currentZentuiBaseFactory
179
+ ? makeWrappedEditorFactory(ctx, currentZentuiBaseFactory)
180
+ : makeEditorFactory(ctx);
181
+ ctx.ui.setEditorComponent(nextFactory);
182
+ installedEditorFactory = nextFactory;
183
+ editorInstallMode = currentZentuiBaseFactory ? "wrapper" : "standalone";
184
+ } else if (currentFactory) {
174
185
  wrappedEditorFactory = currentFactory;
175
- ctx.ui.setEditorComponent(makeWrappedEditorFactory(ctx, currentFactory));
186
+ const nextFactory = makeWrappedEditorFactory(ctx, currentFactory);
187
+ ctx.ui.setEditorComponent(nextFactory);
188
+ installedEditorFactory = nextFactory;
176
189
  editorInstallMode = "wrapper";
177
190
  } else {
178
191
  wrappedEditorFactory = undefined;
179
- ctx.ui.setEditorComponent(makeEditorFactory(ctx));
192
+ const nextFactory = makeEditorFactory(ctx);
193
+ ctx.ui.setEditorComponent(nextFactory);
194
+ installedEditorFactory = nextFactory;
180
195
  editorInstallMode = "standalone";
181
196
  }
182
197
  editorInstalled = true;
@@ -192,6 +207,7 @@ export default function (pi: ExtensionAPI) {
192
207
  editorInstallMode === "wrapper" && wrappedEditorFactory ? wrappedEditorFactory : undefined,
193
208
  );
194
209
  wrappedEditorFactory = undefined;
210
+ installedEditorFactory = undefined;
195
211
  editorInstallMode = "none";
196
212
  editorInstalled = false;
197
213
  return true;
@@ -213,7 +229,7 @@ export default function (pi: ExtensionAPI) {
213
229
  stopRefreshInterval = startProjectRefreshInterval(currentConfig.projectRefreshIntervalMs, () =>
214
230
  scheduleProjectRefresh(ctx),
215
231
  );
216
- scheduleProjectRefresh(ctx);
232
+ scheduleProjectRefresh(ctx, { force: true });
217
233
  refresh();
218
234
  };
219
235
 
@@ -227,7 +243,7 @@ export default function (pi: ExtensionAPI) {
227
243
 
228
244
  const applyConfiguredUi = (ctx: ExtensionContext): ApplyUiResult => {
229
245
  const result: ApplyUiResult = { editorBlocked: false };
230
- if (!ctx.hasUI) return result;
246
+ if (!isTuiContext(ctx)) return result;
231
247
  activeTheme = ctx.ui.theme;
232
248
  if (currentConfig.features.editor) {
233
249
  const currentFactory = ctx.ui.getEditorComponent();
@@ -246,11 +262,12 @@ export default function (pi: ExtensionAPI) {
246
262
  };
247
263
 
248
264
  const installUi = (ctx: ExtensionContext) => {
249
- if (!ctx.hasUI) return;
265
+ if (!isTuiContext(ctx)) return;
250
266
  activeTheme = ctx.ui.theme;
251
267
  uninstallPrototypePatches();
252
268
  footerInstalled = false;
253
269
  editorInstalled = false;
270
+ installedEditorFactory = undefined;
254
271
  ensureConfigExists();
255
272
  currentConfig = loadConfig();
256
273
  syncFooterState(ctx);
@@ -261,9 +278,9 @@ export default function (pi: ExtensionAPI) {
261
278
 
262
279
  const scheduleEditorReconciliation = (ctx: ExtensionContext) => {
263
280
  setTimeout(() => {
264
- if (!ctx.hasUI || !currentConfig.features.editor) return;
281
+ if (!isTuiContext(ctx) || !currentConfig.features.editor) return;
265
282
  const currentFactory = ctx.ui.getEditorComponent();
266
- if (currentFactory && !isZentuiEditorFactory(currentFactory)) {
283
+ if (currentFactory && currentFactory !== installedEditorFactory) {
267
284
  applyConfiguredUi(ctx);
268
285
  refresh();
269
286
  }
@@ -275,18 +292,20 @@ export default function (pi: ExtensionAPI) {
275
292
  stopProjectRefresh();
276
293
  requestFooterRender = undefined;
277
294
  getActiveExtensionStatuses = () => new Map();
278
- if (ctx?.hasUI) {
295
+ if (ctx && isTuiContext(ctx)) {
279
296
  ctx.ui.setFooter(undefined);
280
297
  const currentFactory = ctx.ui.getEditorComponent();
281
298
  if (!currentFactory || isZentuiEditorFactory(currentFactory)) {
282
299
  ctx.ui.setEditorComponent(
283
- editorInstallMode === "wrapper" && wrappedEditorFactory
284
- ? wrappedEditorFactory
285
- : undefined,
300
+ getZentuiEditorBaseFactory(currentFactory) ??
301
+ (editorInstallMode === "wrapper" && wrappedEditorFactory
302
+ ? wrappedEditorFactory
303
+ : undefined),
286
304
  );
287
305
  }
288
306
  }
289
307
  wrappedEditorFactory = undefined;
308
+ installedEditorFactory = undefined;
290
309
  editorInstallMode = "none";
291
310
  footerInstalled = false;
292
311
  editorInstalled = false;
@@ -1,5 +1,16 @@
1
1
  export type StopProjectRefreshInterval = () => void;
2
2
 
3
+ export type ScheduleProjectRefreshOptions = {
4
+ force?: boolean;
5
+ };
6
+
7
+ export type ProjectRefreshScheduler<T> = {
8
+ schedule: (target: T, options?: ScheduleProjectRefreshOptions) => void;
9
+ stop: () => void;
10
+ };
11
+
12
+ export const PROJECT_REFRESH_THROTTLE_MS = 5_000;
13
+
3
14
  export function startProjectRefreshInterval(
4
15
  intervalMs: number,
5
16
  refresh: () => void,
@@ -11,3 +22,83 @@ export function startProjectRefreshInterval(
11
22
 
12
23
  return () => clearInterval(timer);
13
24
  }
25
+
26
+ export function createProjectRefreshScheduler<T>(
27
+ refresh: (target: T) => Promise<void>,
28
+ afterRefresh: () => void,
29
+ throttleMs = PROJECT_REFRESH_THROTTLE_MS,
30
+ ): ProjectRefreshScheduler<T> {
31
+ let refreshInFlight = false;
32
+ let refreshPending = false;
33
+ let pendingTarget: T | undefined;
34
+ let delayedRefresh: ReturnType<typeof setTimeout> | undefined;
35
+ let lastRefreshStartedAt: number | undefined;
36
+ let generation = 0;
37
+
38
+ const clearDelayedRefresh = () => {
39
+ if (!delayedRefresh) return;
40
+ clearTimeout(delayedRefresh);
41
+ delayedRefresh = undefined;
42
+ };
43
+
44
+ const runRefresh = (target: T) => {
45
+ clearDelayedRefresh();
46
+ if (refreshInFlight) {
47
+ refreshPending = true;
48
+ pendingTarget = target;
49
+ return;
50
+ }
51
+
52
+ const currentGeneration = generation;
53
+ refreshInFlight = true;
54
+ lastRefreshStartedAt = Date.now();
55
+ void refresh(target)
56
+ .catch(() => undefined)
57
+ .finally(() => {
58
+ if (currentGeneration !== generation) return;
59
+ refreshInFlight = false;
60
+ afterRefresh();
61
+ if (refreshPending) {
62
+ refreshPending = false;
63
+ const nextTarget = pendingTarget ?? target;
64
+ pendingTarget = undefined;
65
+ schedule(nextTarget);
66
+ }
67
+ });
68
+ };
69
+
70
+ const schedule = (target: T, options: ScheduleProjectRefreshOptions = {}) => {
71
+ if (options.force || throttleMs <= 0 || lastRefreshStartedAt === undefined) {
72
+ runRefresh(target);
73
+ return;
74
+ }
75
+
76
+ const delayMs = Math.max(0, throttleMs - (Date.now() - lastRefreshStartedAt));
77
+ if (delayMs === 0) {
78
+ runRefresh(target);
79
+ return;
80
+ }
81
+
82
+ pendingTarget = target;
83
+ if (delayedRefresh) return;
84
+ delayedRefresh = setTimeout(() => {
85
+ delayedRefresh = undefined;
86
+ const nextTarget = pendingTarget ?? target;
87
+ pendingTarget = undefined;
88
+ runRefresh(nextTarget);
89
+ }, delayMs);
90
+ delayedRefresh.unref?.();
91
+ };
92
+
93
+ return {
94
+ schedule,
95
+ stop() {
96
+ generation += 1;
97
+ clearDelayedRefresh();
98
+ refreshInFlight = false;
99
+ refreshPending = false;
100
+ pendingTarget = undefined;
101
+ lastRefreshStartedAt = undefined;
102
+ },
103
+ };
104
+ }
@@ -305,7 +305,8 @@ export function registerZentuiSettingsCommand(pi: ExtensionAPI, deps: SettingsCo
305
305
  return;
306
306
  }
307
307
 
308
- if (!ctx.hasUI) return;
308
+ const mode = (ctx as typeof ctx & { mode?: string }).mode;
309
+ if (!ctx.hasUI || (mode !== undefined && mode !== "tui")) return;
309
310
 
310
311
  await ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
311
312
  const settingsListTheme = deps.settingsListTheme ?? getSettingsListTheme();
@@ -132,6 +132,40 @@ function composeMetadataLine(left: string, right: string | undefined, width: num
132
132
  return `${leftText}${gap}${right}`;
133
133
  }
134
134
 
135
+ function plainRenderedText(line: string): string {
136
+ return line
137
+ .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
138
+ .replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
139
+ .replace(/\[[/?][^\]]+\]/g, "");
140
+ }
141
+
142
+ function isHorizontalBorder(line: string): boolean {
143
+ const plain = plainRenderedText(line).trim();
144
+ return plain.length > 0 && /^─+$/.test(plain);
145
+ }
146
+
147
+ function isRenderedModelMetaLine(line: string, modelMeta: EditorMeta): boolean {
148
+ const plain = plainRenderedText(line);
149
+ return plain.includes(modelMeta.modelLabel) && plain.includes(modelMeta.providerLabel);
150
+ }
151
+
152
+ function removeRenderedModelMetaLines(lines: string[], modelMeta: EditorMeta): string[] {
153
+ const result: string[] = [];
154
+ for (let index = 0; index < lines.length; index++) {
155
+ const line = lines[index] ?? "";
156
+ if (isRenderedModelMetaLine(line, modelMeta)) continue;
157
+
158
+ const plain = plainRenderedText(line).trim();
159
+ const previousWasMeta = index > 0 && isRenderedModelMetaLine(lines[index - 1] ?? "", modelMeta);
160
+ const nextIsMeta =
161
+ index < lines.length - 1 && isRenderedModelMetaLine(lines[index + 1] ?? "", modelMeta);
162
+ if (!plain && (previousWasMeta || nextIsMeta)) continue;
163
+
164
+ result.push(line);
165
+ }
166
+ return result;
167
+ }
168
+
135
169
  function vimModeColor(mode: string): string {
136
170
  switch (mode.toLowerCase()) {
137
171
  case "insert":
@@ -195,10 +229,9 @@ function renderPolishedFrame({
195
229
  autocompleteCount > 0 && autocompleteCount < baseRendered.length
196
230
  ? baseRendered.slice(-autocompleteCount)
197
231
  : [];
198
-
199
232
  if (editorFrame.length < 2) return clampRenderedLines(baseRendered, width);
200
233
 
201
- const editorLines = editorFrame.slice(1, -1);
234
+ const editorLines = removeRenderedModelMetaLines(editorFrame.slice(1, -1), modelMeta);
202
235
  const model = renderStyleForSourceOrFallback(
203
236
  uiTheme,
204
237
  colorSource,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-zentui",
3
- "version": "0.2.1",
3
+ "version": "0.2.5",
4
4
  "description": "A Starship-inspired statusline and Opencode-style TUI for Pi.",
5
5
  "type": "module",
6
6
  "license": "MIT",