pi-zentui 0.1.3 → 0.1.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Zentui
2
2
 
3
- A Starship-inspired statusline and Opencode-style TUI for [Pi](https://github.com/badlogic/pi-mono).
3
+ A Starship-inspired statusline and Opencode-style TUI for [Pi](https://pi.dev).
4
4
 
5
5
  ## Screenshots
6
6
 
@@ -11,7 +11,7 @@ A Starship-inspired statusline and Opencode-style TUI for [Pi](https://github.co
11
11
  Zentui brings two popular aesthetics to Pi:
12
12
 
13
13
  - **[Starship](https://starship.rs/) footer** — shows your current directory, git branch, git status indicators, and runtime/version detection in a compact, icon-rich format
14
- - **[Opencode](https://github.com/opencode-ai/opencode) editor and messages** — clean bordered input box with accent rail, transparent previous messages, and model/provider display inside the editor frame
14
+ - **[Opencode](https://github.com/opencode-ai/opencode) editor** — clean bordered input box with accent rail and model/provider display inside the editor frame
15
15
 
16
16
  ## Features
17
17
 
@@ -20,7 +20,7 @@ Zentui brings two popular aesthetics to Pi:
20
20
  - `󰝰 dirname` — current directory with icon
21
21
  - `on branch` — git branch with icon
22
22
  - `[!?↑]` — git status indicators (modified, untracked, ahead/behind, stashed, etc.)
23
- - `via v5.5.0` — runtime detection with version (Bun, Deno, Node, Python, Go, Rust, Lua, Java, Ruby, PHP)
23
+ - `via v5.5.0` — runtime detection with version and logo color for Starship Nerd Font runtime/language modules
24
24
  - Right side shows context usage, token counts, and cost
25
25
 
26
26
  ### Editor (Opencode-inspired)
@@ -28,7 +28,6 @@ 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
- - Matching style for previous user messages
32
31
 
33
32
  ### Git Status Icons
34
33
 
@@ -47,20 +46,67 @@ Zentui brings two popular aesthetics to Pi:
47
46
 
48
47
  ### Runtime Detection
49
48
 
50
- Detects project type and shows runtime version:
51
-
52
- | Runtime | Detection |
53
- | ------- | ----------------------------------------------------------- |
54
- | Bun | `bun.lock`, `bun.lockb` |
55
- | Deno | `deno.json`, `deno.jsonc`, `deno.lock` |
56
- | Node.js | `package.json`, `.nvmrc`, `.node-version` |
57
- | Python | `pyproject.toml`, `requirements.txt`, `setup.py`, `Pipfile` |
58
- | Go | `go.mod` |
59
- | Rust | `Cargo.toml` |
60
- | Lua | `stylua.toml`, `.luarc.json`, `init.lua`, `lua/` dir |
61
- | Java | `pom.xml`, `build.gradle` |
62
- | Ruby | `Gemfile`, `.ruby-version` |
63
- | PHP | `composer.json` |
49
+ Detects Starship Nerd Font runtime/language modules, uses the Starship Nerd Font symbols, and colors each runtime with its official brand color where available:
50
+
51
+ | Runtime/language | Detection examples |
52
+ | ---------------- | ------------------------------------------------------------ |
53
+ | Buf | `buf.yaml`, `buf.gen.yaml`, `buf.work.yaml` |
54
+ | Bun | `bun.lock`, `bun.lockb` |
55
+ | C | `.c`, `.h` files |
56
+ | C++ | `.cpp`, `.cc`, `.cxx`, `.hpp` files |
57
+ | CMake | `CMakeLists.txt`, `CMakeCache.txt` |
58
+ | COBOL | `.cbl`, `.cob` files |
59
+ | Conda | `CONDA_DEFAULT_ENV` environment |
60
+ | Crystal | `.cr` files, `shard.yml` |
61
+ | Dart | `.dart` files, `pubspec.yaml`, `.dart_tool/` |
62
+ | Deno | `deno.json`, `deno.jsonc`, `deno.lock` |
63
+ | .NET | `.csproj`, `.fsproj`, `global.json`, `Directory.Build.*` |
64
+ | Elixir | `mix.exs` |
65
+ | Elm | `.elm` files, `elm.json`, `elm-stuff/` |
66
+ | Erlang | `rebar.config`, `erlang.mk` |
67
+ | Fennel | `.fnl` files |
68
+ | Fortran | `.f`, `.f90`, `.f95`, `.f03`, `.f08`, `.f18`, `fpm.toml` |
69
+ | Gleam | `.gleam` files, `gleam.toml` |
70
+ | Go | `go.mod` |
71
+ | Gradle | `build.gradle`, `build.gradle.kts`, `gradle/` |
72
+ | Guix shell | `GUIX_ENVIRONMENT` environment |
73
+ | Haskell | `.hs`, `.cabal`, `stack.yaml`, `cabal.project` |
74
+ | Haxe | `.hx`, `.hxml`, `haxelib.json`, `.haxerc` |
75
+ | Helm | `helmfile.yaml`, `Chart.yaml` |
76
+ | Java | `.java-version` |
77
+ | Julia | `.jl` files, `Project.toml`, `Manifest.toml` |
78
+ | Kotlin | `.kt`, `.kts` files |
79
+ | Lua | `.lua` files, `stylua.toml`, `.luarc.json`, `lua/` dir |
80
+ | Maven | `pom.xml` |
81
+ | Meson | `MESON_DEVENV=1` and `MESON_PROJECT_NAME` environment |
82
+ | Mojo | `.mojo` files |
83
+ | Nim | `.nim`, `.nims`, `.nimble`, `nim.cfg` |
84
+ | Nix shell | `IN_NIX_SHELL=pure` or `IN_NIX_SHELL=impure` environment |
85
+ | Node.js | `package.json`, `.nvmrc`, `.node-version` |
86
+ | OCaml | `.opam`, `.ml`, `.mli`, `dune`, `_opam/`, `esy.lock/` |
87
+ | Odin | `.odin` files |
88
+ | OPA/Rego | `.rego` files |
89
+ | Perl | `.pl`, `.pm`, `Makefile.PL`, `cpanfile`, `META.*` |
90
+ | PHP | `composer.json` |
91
+ | Pixi | `pixi.toml`, `pixi.lock`, `PIXI_ENVIRONMENT_NAME` environment |
92
+ | Pulumi | `Pulumi.yaml`, `Pulumi.yml` |
93
+ | PureScript | `.purs` files, `spago.dhall`, `spago.yaml`, `spago.lock` |
94
+ | Python | `pyproject.toml`, `requirements.txt`, `setup.py`, `Pipfile` |
95
+ | R | `.R`, `.Rmd`, `.Rproj`, `DESCRIPTION`, `.Rproj.user/` |
96
+ | Raku | `.raku`, `.rakumod`, `.p6`, `.pm6`, `META6.json` |
97
+ | Red | `.red`, `.reds` files |
98
+ | Ruby | `Gemfile`, `.ruby-version` |
99
+ | Rust | `Cargo.toml` |
100
+ | Scala | `.scala`, `.sbt`, `build.sbt`, `.metals/` |
101
+ | Solidity | `.sol` files |
102
+ | Spack | `SPACK_ENV` environment |
103
+ | Swift | `.swift` files, `Package.swift` |
104
+ | Terraform | `.tf`, `.tfplan`, `.tfstate`, `.terraform/` |
105
+ | Typst | `.typ` files, `template.typ` |
106
+ | Vagrant | `Vagrantfile` |
107
+ | V | `.v` files, `v.mod`, `vpkg.json` |
108
+ | Xmake | `xmake.lua` |
109
+ | Zig | `.zig` files, `build.zig` |
64
110
 
65
111
  ## Install
66
112
 
@@ -84,6 +130,7 @@ On first run, Zentui creates a config file at:
84
130
 
85
131
  ```json
86
132
  {
133
+ "projectRefreshIntervalMs": 30000,
87
134
  "icons": {
88
135
  "cwd": "󰝰",
89
136
  "git": "",
@@ -109,18 +156,11 @@ On first run, Zentui creates a config file at:
109
156
  "tokens": "muted",
110
157
  "cost": "success",
111
158
  "separator": "borderMuted"
112
- },
113
- "tools": {
114
- "style": "compact"
115
159
  }
116
160
  }
117
161
  ```
118
162
 
119
- `tools.style` controls the initial built-in tool output style:
120
-
121
- - `compact` — one-line tool calls by default
122
- - `truncated` — expanded preview with long output truncated
123
- - `full` — expanded full output
163
+ `projectRefreshIntervalMs` controls how often Zentui refreshes project status (git/runtime) while Pi is idle. Set it to `0` to disable polling; invalid values or values below 5000 ms fall back to `30000`.
124
164
 
125
165
  ### Color values
126
166
 
@@ -133,7 +173,7 @@ This means Zentui works with any Pi theme — it uses your theme's colors by def
133
173
 
134
174
  ## Requirements
135
175
 
136
- - [Pi](https://github.com/badlogic/pi-mono) coding agent
176
+ - [Pi](https://pi.dev) coding agent 0.74 or newer
137
177
  - A [Nerd Font](https://www.nerdfonts.com/) for icons
138
178
 
139
179
  ## Development
@@ -1,11 +1,14 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { getAgentDir } from "@mariozechner/pi-coding-agent";
3
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
4
4
 
5
5
  export type ColorSpec = string;
6
- export type ToolOutputStyle = "compact" | "truncated" | "full";
6
+
7
+ const DEFAULT_PROJECT_REFRESH_INTERVAL_MS = 30_000;
8
+ const MIN_PROJECT_REFRESH_INTERVAL_MS = 5_000;
7
9
 
8
10
  export type PolishedTuiConfig = {
11
+ projectRefreshIntervalMs: number;
9
12
  icons: {
10
13
  cwd: string;
11
14
  git: string;
@@ -32,9 +35,6 @@ export type PolishedTuiConfig = {
32
35
  cost: ColorSpec;
33
36
  separator: ColorSpec;
34
37
  };
35
- tools: {
36
- style: ToolOutputStyle;
37
- };
38
38
  };
39
39
 
40
40
  export const configPath = join(getAgentDir(), "zentui.json");
@@ -88,6 +88,7 @@ const themeColorTokens = new Set([
88
88
  ]);
89
89
 
90
90
  export const defaultConfig: PolishedTuiConfig = {
91
+ projectRefreshIntervalMs: DEFAULT_PROJECT_REFRESH_INTERVAL_MS,
91
92
  icons: {
92
93
  cwd: "󰝰",
93
94
  git: "",
@@ -114,9 +115,6 @@ export const defaultConfig: PolishedTuiConfig = {
114
115
  cost: "success",
115
116
  separator: "borderMuted",
116
117
  },
117
- tools: {
118
- style: "compact",
119
- },
120
118
  };
121
119
 
122
120
  function isHexColor(value: string): boolean {
@@ -141,10 +139,16 @@ function isRecord(value: unknown): value is ConfigRecord {
141
139
  return value !== null && typeof value === "object" && !Array.isArray(value);
142
140
  }
143
141
 
144
- function parseToolOutputStyle(value: unknown): ToolOutputStyle {
145
- return value === "truncated" || value === "full" || value === "compact"
146
- ? value
147
- : defaultConfig.tools.style;
142
+ function parseProjectRefreshIntervalMs(value: unknown): number {
143
+ if (value === 0) return 0;
144
+ if (typeof value !== "number" || !Number.isFinite(value)) {
145
+ return defaultConfig.projectRefreshIntervalMs;
146
+ }
147
+
148
+ const interval = Math.round(value);
149
+ return interval >= MIN_PROJECT_REFRESH_INTERVAL_MS
150
+ ? interval
151
+ : defaultConfig.projectRefreshIntervalMs;
148
152
  }
149
153
 
150
154
  export function colorize(theme: ThemeLike, color: ColorSpec, text: string): string {
@@ -173,9 +177,8 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
173
177
  const colors = isRecord(config.colors)
174
178
  ? (config.colors as Partial<PolishedTuiConfig["colors"]>)
175
179
  : {};
176
- const tools = isRecord(config.tools) ? (config.tools as Partial<PolishedTuiConfig["tools"]>) : {};
177
-
178
180
  return {
181
+ projectRefreshIntervalMs: parseProjectRefreshIntervalMs(config.projectRefreshIntervalMs),
179
182
  icons: {
180
183
  ...defaultConfig.icons,
181
184
  ...icons,
@@ -184,10 +187,6 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
184
187
  ...defaultConfig.colors,
185
188
  ...colors,
186
189
  },
187
- tools: {
188
- ...defaultConfig.tools,
189
- style: parseToolOutputStyle(tools.style),
190
- },
191
190
  };
192
191
  }
193
192
 
@@ -1,16 +1,16 @@
1
- import type { AssistantMessage } from "@mariozechner/pi-ai";
1
+ import type { AssistantMessage } from "@earendil-works/pi-ai";
2
2
  import type {
3
3
  ExtensionAPI,
4
4
  ExtensionContext,
5
5
  KeybindingsManager,
6
6
  Theme,
7
- } from "@mariozechner/pi-coding-agent";
8
- import { type EditorTheme, type TUI, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
9
- import { registerCompactTools } from "./compact-tools";
7
+ } from "@earendil-works/pi-coding-agent";
8
+ import { type EditorTheme, type TUI, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
10
9
  import { type PolishedTuiConfig, colorize, ensureConfigExists, loadConfig } from "./config";
11
10
  import { type GitStatusSummary, emptyGitStatus, readGitStatus } from "./git";
11
+ import { type StopProjectRefreshInterval, startProjectRefreshInterval } from "./project-refresh";
12
12
  import { type RuntimeInfo, readRuntimeInfo } from "./runtime";
13
- import { PolishedEditor, patchUserMessageComponent, restoreUserMessageComponent } from "./ui";
13
+ import { PolishedEditor } from "./ui";
14
14
 
15
15
  type FooterState = GitStatusSummary & {
16
16
  modelLabel: string;
@@ -85,30 +85,6 @@ function buildContextLabel(ctx: ExtensionContext): string {
85
85
  return `${percent}/${formatCount(contextWindow)}`;
86
86
  }
87
87
 
88
- function getRuntimeColorToken(runtime: RuntimeInfo | undefined): string {
89
- switch (runtime?.name) {
90
- case "nodejs":
91
- return "success";
92
- case "deno":
93
- return "syntaxType";
94
- case "bun":
95
- return "warning";
96
- case "python":
97
- case "java":
98
- return "warning";
99
- case "rust":
100
- case "ruby":
101
- return "error";
102
- case "golang":
103
- return "syntaxType";
104
- case "lua":
105
- case "php":
106
- return "accent";
107
- default:
108
- return "text";
109
- }
110
- }
111
-
112
88
  function formatRuntimeSegment(
113
89
  theme: Pick<Theme, "fg">,
114
90
  runtime: RuntimeInfo | undefined,
@@ -116,7 +92,7 @@ function formatRuntimeSegment(
116
92
  ): string {
117
93
  if (!runtime) return "";
118
94
  const label = runtime.version ? `${runtime.symbol} ${runtime.version}` : runtime.symbol;
119
- return `${colorize(theme, mutedColor, "via")} ${colorize(theme, getRuntimeColorToken(runtime), label)}`;
95
+ return `${colorize(theme, mutedColor, "via")} ${colorize(theme, runtime.color, label)}`;
120
96
  }
121
97
 
122
98
  function formatCwdLabel(cwd: string, cwdIcon: string): string {
@@ -127,8 +103,6 @@ function formatCwdLabel(cwd: string, cwdIcon: string): string {
127
103
  }
128
104
 
129
105
  export default function (pi: ExtensionAPI) {
130
- registerCompactTools(pi);
131
-
132
106
  const state: FooterState = {
133
107
  modelLabel: "no-model",
134
108
  providerLabel: "Unknown",
@@ -141,11 +115,31 @@ export default function (pi: ExtensionAPI) {
141
115
 
142
116
  let currentConfig: PolishedTuiConfig = loadConfig();
143
117
  let requestFooterRender: (() => void) | undefined;
118
+ let stopProjectRefreshInterval: StopProjectRefreshInterval = () => {};
144
119
  let projectRefreshInFlight = false;
145
120
  let projectRefreshPending = false;
146
121
 
147
122
  const refresh = () => requestFooterRender?.();
148
123
 
124
+ const cleanupUi = (ctx?: ExtensionContext) => {
125
+ stopProjectRefreshInterval();
126
+ stopProjectRefreshInterval = () => {};
127
+ projectRefreshInFlight = false;
128
+ projectRefreshPending = false;
129
+ requestFooterRender = undefined;
130
+ if (ctx?.hasUI) {
131
+ ctx.ui.setFooter(undefined);
132
+ ctx.ui.setEditorComponent(undefined);
133
+ }
134
+ };
135
+
136
+ const refreshInteractiveState = (ctx: ExtensionContext, project = false) => {
137
+ if (!ctx.hasUI) return;
138
+ syncState(ctx);
139
+ if (project) scheduleProjectRefresh(ctx);
140
+ refresh();
141
+ };
142
+
149
143
  const syncState = (ctx: ExtensionContext) => {
150
144
  const totals = getUsageTotals(ctx);
151
145
  state.modelLabel = ctx.model?.id ?? "no-model";
@@ -199,6 +193,7 @@ export default function (pi: ExtensionAPI) {
199
193
  },
200
194
  invalidate() {},
201
195
  render(width: number): string[] {
196
+ if (width <= 0) return [""];
202
197
  const innerWidth = Math.max(1, width - 2);
203
198
  const cwdLabel = colorize(
204
199
  theme,
@@ -255,11 +250,12 @@ export default function (pi: ExtensionAPI) {
255
250
  const rightWidth = visibleWidth(right);
256
251
  const content =
257
252
  leftWidth >= innerWidth
258
- ? truncateToWidth(left, innerWidth)
253
+ ? truncateToWidth(left, innerWidth, "")
259
254
  : leftWidth + 1 + rightWidth <= innerWidth
260
255
  ? `${left}${" ".repeat(innerWidth - leftWidth - rightWidth)}${right}`
261
- : left;
262
- return [` ${content} `];
256
+ : truncateToWidth(left, innerWidth, "");
257
+ const framed = width > 2 ? ` ${truncateToWidth(content, width - 2, "")} ` : content;
258
+ return [truncateToWidth(framed, width, "")];
263
259
  },
264
260
  };
265
261
  });
@@ -286,11 +282,16 @@ export default function (pi: ExtensionAPI) {
286
282
  };
287
283
 
288
284
  const installUi = (ctx: ExtensionContext) => {
285
+ if (!ctx.hasUI) return;
289
286
  ensureConfigExists();
290
287
  currentConfig = loadConfig();
291
- patchUserMessageComponent(ctx.ui.theme);
292
288
  installFooter(ctx);
293
289
  installEditor(ctx);
290
+ stopProjectRefreshInterval();
291
+ stopProjectRefreshInterval = startProjectRefreshInterval(
292
+ currentConfig.projectRefreshIntervalMs,
293
+ () => scheduleProjectRefresh(ctx),
294
+ );
294
295
  scheduleProjectRefresh(ctx);
295
296
  refresh();
296
297
  };
@@ -299,41 +300,31 @@ export default function (pi: ExtensionAPI) {
299
300
  installUi(ctx);
300
301
  });
301
302
 
302
- pi.on("session_shutdown", async () => {
303
- restoreUserMessageComponent();
303
+ pi.on("session_shutdown", async (_event, ctx) => {
304
+ cleanupUi(ctx);
304
305
  });
305
306
 
306
307
  pi.on("agent_start", async (_event, ctx) => {
307
- syncState(ctx);
308
- refresh();
308
+ refreshInteractiveState(ctx);
309
309
  });
310
310
 
311
311
  pi.on("agent_end", async (_event, ctx) => {
312
- syncState(ctx);
313
- scheduleProjectRefresh(ctx);
314
- refresh();
312
+ refreshInteractiveState(ctx, true);
315
313
  });
316
314
 
317
315
  pi.on("model_select", async (_event, ctx) => {
318
- syncState(ctx);
319
- refresh();
316
+ refreshInteractiveState(ctx);
320
317
  });
321
318
 
322
319
  pi.on("message_end", async (_event, ctx) => {
323
- syncState(ctx);
324
- scheduleProjectRefresh(ctx);
325
- refresh();
320
+ refreshInteractiveState(ctx, true);
326
321
  });
327
322
 
328
323
  pi.on("tool_execution_end", async (_event, ctx) => {
329
- syncState(ctx);
330
- scheduleProjectRefresh(ctx);
331
- refresh();
324
+ refreshInteractiveState(ctx, true);
332
325
  });
333
326
 
334
327
  pi.on("session_compact", async (_event, ctx) => {
335
- syncState(ctx);
336
- scheduleProjectRefresh(ctx);
337
- refresh();
328
+ refreshInteractiveState(ctx, true);
338
329
  });
339
330
  }
@@ -0,0 +1,13 @@
1
+ export type StopProjectRefreshInterval = () => void;
2
+
3
+ export function startProjectRefreshInterval(
4
+ intervalMs: number,
5
+ refresh: () => void,
6
+ ): StopProjectRefreshInterval {
7
+ if (intervalMs <= 0) return () => {};
8
+
9
+ const timer = setInterval(refresh, intervalMs);
10
+ timer.unref?.();
11
+
12
+ return () => clearInterval(timer);
13
+ }