pi-zentui 0.11.0 → 0.13.0

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
@@ -21,7 +21,7 @@ Zentui brings two popular aesthetics to Pi:
21
21
  - `on branch` — git branch with icon
22
22
  - `[!?↑]` — git status indicators (modified, untracked, ahead/behind, stashed, etc.)
23
23
  - `via v5.5.0` — runtime detection with version and Starship-style Nerd Font runtime/language modules
24
- - Optional segments (off by default): `user@host`, current time, OS icon, session duration, and the **project package version** (e.g. `package.json` → `0.6.0`) — distinct from the runtime segment, which shows the installed toolchain
24
+ - Optional segments (off by default): session name, `user@host`, current time, OS icon, session duration, and the **project package version** (e.g. `package.json` → `0.6.0`) — distinct from the runtime segment, which shows the installed toolchain
25
25
  - Right side shows context usage, token counts, and cost
26
26
  - Built-in footer segments can be shown or hidden individually from `/zentui`
27
27
  - Fully custom Starship-style layout via a `footerFormat` template string — see [Footer Format Template](#footer-format-template)
@@ -157,8 +157,10 @@ Default config values — copy this and change any value you want:
157
157
  {
158
158
  "projectRefreshIntervalMs": 30000,
159
159
  "footerFormat": "",
160
+ "editorMetadataFormat": "$model $provider( $thinking)",
160
161
  "separator": "pipe",
161
162
  "contextStyle": "text",
163
+ "editorModelLabel": "id",
162
164
  "contextThresholds": {
163
165
  "warning": 70,
164
166
  "error": 90
@@ -194,6 +196,7 @@ Default config values — copy this and change any value you want:
194
196
  },
195
197
  "colors": {
196
198
  "cwd": "bold cyan",
199
+ "sessionName": "bold green",
197
200
  "gitBranch": "bold purple",
198
201
  "gitStatus": "bold red",
199
202
  "contextNormal": "bright-black",
@@ -236,6 +239,7 @@ Default config values — copy this and change any value you want:
236
239
  },
237
240
  "footerSegments": {
238
241
  "cwd": true,
242
+ "sessionName": true,
239
243
  "gitBranch": true,
240
244
  "gitStatus": true,
241
245
  "gitCounts": false,
@@ -276,6 +280,8 @@ Default config values — copy this and change any value you want:
276
280
  - Style values can be Starship/terminal strings (`bold purple`, `fg:202`, `#89b` / `#89b4fa`, `bg:blue fg:bright-green`) or Pi theme tokens (`accent`, `borderMuted`, `thinkingHigh`). Short `#rgb` hex values expand to `#rrggbb`.
277
281
  - `projectRefreshIntervalMs`: project status polling interval; `0` disables polling. Values `1..4999` clamp up to `5000` (minimum 5s); invalid/non-finite values fall back to `30000`.
278
282
  - `contextStyle`: `text` (default), `gauge`, or `text+gauge` for the context segment. Context usage refreshes during assistant streaming; token and cost totals remain canonical and finalize at turn boundaries.
283
+ - `editorModelLabel`: controls the model shown in the editor frame. `id` (default) shows the model id; `name` shows the model's display name (including custom `name` values set in `models.json`), falling back to the id when no name is set.
284
+ - `editorMetadataFormat`: JSON-only template for the left side of the editor metadata row. Missing, non-string, or empty values restore the default `$model $provider( $thinking)` layout; non-empty strings, including whitespace-only strings, are preserved. See [Editor Metadata Format](#editor-metadata-format) below.
279
285
  - `separator`: controls the default footer layout and extension-status connectors: `pipe` (default, ` | `), `dot` (` · `), `chevron` (` › `), or `none` (one space). Cycle it from the `/zentui` **Layout** tab. This selects the separator glyph; `colors.separator` controls its color. Custom `footerFormat` literals and `$sep` keep their existing behavior.
280
286
  - `contextThresholds`: `{ warning, error }` percentages (default `70` / `90`) that select contextNormal / contextWarning / contextError colors.
281
287
  - `pathDisplay`: controls how the cwd/`$cwd` path is shown. `mode` is `basename` (default, last segment only) or `full` (path with home contracted to `~`). In `full` mode, `depth` keeps only the last N trailing directories (`0` = entire path after `~`, max `5`); when parents are dropped the path is prefixed with `…/` (Starship-style). The `/zentui` **Layout** tab cycles path mode and path depth (`0`–`5`; depth is ignored for basename). Example: `~/Projects/foo/bar` with `depth: 2` → `…/foo/bar`.
@@ -283,7 +289,7 @@ Default config values — copy this and change any value you want:
283
289
  - `icons`: every shown icon key is configurable; omit any key to use the Zentui default. `icons.mode` is `auto` | `nerd` | `ascii` (default `auto`, same glyphs as nerd). ASCII mode swaps in plain fallbacks for statusline icons and runtime symbols — useful without a Nerd Font. Custom per-icon strings always win over mode defaults. Custom `icons.os` always wins; when left at the mode default, Zentui maps the OS icon by platform. `rail` sets the vertical glyph drawn as the left rail of the active editor frame and previous user messages when `copyFriendly` is disabled (default `│`; any single Unicode vertical or block glyph). `editorPrompt` controls an optional copy-friendly editor prompt glyph; the default is `""` so copy-friendly mode stays rail-free.
284
290
  - `colorSources`: `theme` maps styles through Pi theme tokens; `terminal` emits terminal colors. `/zentui` switches these sources; manual JSON controls specific style values.
285
291
  - `features`: `editor` enables Zentui's custom editor, selector borders, and previous-message chrome. `statusLine` enables Zentui's custom footer/status line. `copyFriendly` hides editor and previous-message rail glyphs so native terminal selection copies less chrome. All three can be changed from `/zentui` or direct slash-command arguments.
286
- - `footerSegments`: show or hide individual built-in footer segments (`cwd`, `gitBranch`, `gitStatus`, `gitCounts`, `gitCommit`, `gitMetrics`, `runtime`, `packageVersion`, `sessionDuration`, `username`, `time`, `os`, `context`, `tokens`, `cost`). Toggle them from the `Built-in segments` tab in `/zentui`.
292
+ - `footerSegments`: show or hide individual built-in footer segments (`cwd`, `sessionName`, `gitBranch`, `gitStatus`, `gitCounts`, `gitCommit`, `gitMetrics`, `runtime`, `packageVersion`, `sessionDuration`, `username`, `time`, `os`, `context`, `tokens`, `cost`). Toggle them from the `Built-in segments` tab in `/zentui`.
287
293
  - `footerFormat`: optional Starship-style template string that fully controls the footer layout. When set, it overrides `footerSegments`. See [Footer Format Template](#footer-format-template) below. The `/zentui` **Layout** tab configures context style, separator, path display mode/depth, branch length, and icon mode; set or clear custom formats with `/zentui format`.
288
294
  - `gitCommit`: Starship [`git_commit`](https://starship.rs/config/#git-commit)-style options for the `gitCommit` footer segment. `hashLength` (default `7`, clamped to `4`–`40`) controls the short-hash display length. `onlyDetached` (default `true`) shows the hash mainly on detached HEAD. `showTag` (default `true`) appends an exact-match tag (`git describe --tags --exact-match HEAD`). The tag probe piggybacks on the existing git refresh — it only runs when both the segment and `showTag` are on, and misses/failures degrade silently.
289
295
  - `gitMetrics`: Starship [`git_metrics`](https://starship.rs/config/#git-metrics)-style options for the `gitMetrics` footer segment. Uses `git diff HEAD --numstat` (staged + unstaged combined — the Starship “total dirty” view) to show aggregate `+added −deleted` line counts. `onlyNonzero` (default `true`) omits each zero component independently and hides the segment entirely at `0/0`. `ignoreSubmodules` (default `false`) adds `--ignore-submodules=all`. The numstat diff piggybacks on the existing git refresh and uses a hard 2s timeout; a metrics-only failure degrades silently without discarding fresh branch/status data. On very large monorepos the diff may lag or be omitted on timeout.
@@ -296,6 +302,31 @@ Default config values — copy this and change any value you want:
296
302
 
297
303
  Tip: when using copy-friendly mode, setting Pi's `editorPaddingX` to `1` in `~/.pi/agent/settings.json` keeps a small left gutter without copying a rail glyph.
298
304
 
305
+ ## Editor Metadata Format
306
+
307
+ Set `editorMetadataFormat` in `~/.pi/agent/zentui.json` to customize the left side of the editor metadata row:
308
+
309
+ ```json
310
+ {
311
+ "editorMetadataFormat": "$model_name ($model_id)( · $provider)( · $thinking)( · $session_name)"
312
+ }
313
+ ```
314
+
315
+ The syntax follows the relevant `footerFormat` conventions: `$variable` and `${variable}` references, literal text and spaces, and conditional groups `( ... )` that disappear when all variables inside are empty. Unknown variables and `$fill` render empty; `$fill` never creates an editor layout zone because the right side remains reserved for structural Vim status.
316
+
317
+ | Token | Renders |
318
+ | --------------- | --------------------------------------------------------------------------------------------- |
319
+ | `$model` | label selected by `editorModelLabel` (`id`, or name with ID fallback) |
320
+ | `$model_id` | active Pi model ID |
321
+ | `$model_name` | active Pi model display name; empty when no name is set |
322
+ | `$provider` | provider label using Zentui's existing formatting |
323
+ | `$thinking` | current thinking level; empty when thinking is `off` |
324
+ | `$session_name` | current Pi session name; empty when unnamed |
325
+
326
+ Model variables use `editorModel`, provider uses `editorProvider`, and thinking uses the matching `editorThinking*` style. Literal text and `$session_name` use the neutral editor border theme style. The template controls spacing. ANSI/VT sequences, control characters, and line-breaking whitespace are sanitized before rendering without collapsing ordinary spaces.
327
+
328
+ Missing, non-string, or empty values use the default `$model $provider( $thinking)`. A non-empty format that resolves to no visible metadata keeps the normal blank spacer and metadata rows so the editor frame height remains stable. This option is configured only through JSON in its first version; `/zentui format` continues to control the footer only.
329
+
299
330
  ## Footer Format Template
300
331
 
301
332
  For full control, set a Starship-style `footerFormat` template string. It supports `$variable` and `${variable}` tokens, a special `$fill` token that splits the line into left and right zones, and conditional groups `( ... )` that drop entirely when every nested variable is empty. When set, it overrides the built-in `footerSegments` layout; when empty or omitted, the segment layout above is used.
@@ -304,7 +335,7 @@ A second `$fill` creates a **centered middle zone** — content between the two
304
335
 
305
336
  ```json
306
337
  {
307
- "footerFormat": "$os $username $cwd( on $git_branch)( $git_status)( via $runtime)$fill($context)($sep$tokens)($sep$cost)($sep$time)"
338
+ "footerFormat": "$os $username $cwd($sep$session_name)( on $git_branch)( $git_status)( via $runtime)$fill($context)($sep$tokens)($sep$cost)($sep$time)"
308
339
  }
309
340
  ```
310
341
 
@@ -321,6 +352,7 @@ Center the branch between directory and cost:
321
352
  | Token | Aliases | Renders |
322
353
  | ------------------- | ------------ | ------------------------------------------------------------------- |
323
354
  | `$cwd` | `$directory` | current directory |
355
+ | `$session_name` | | current Pi session name |
324
356
  | `$git_branch` | `$branch` | git branch with icon |
325
357
  | `$git_status` | `$status` | `[!?↑]` status block |
326
358
  | `$git_state` | `$state` | `REBASING` / `MERGING` / … (optional `n/m`) |
@@ -354,6 +386,7 @@ Center the branch between directory and cost:
354
386
  - Literal text (`on branch`, `using`, `\|`, spaces) is rendered verbatim — you control all spacing.
355
387
  - Each variable renders its core value only (no `on`/`via` prefixes); add those words as literal text.
356
388
  - Conditional groups: wrap optional pieces in parentheses, e.g. `$cwd( on $git_branch)($git_status)$fill($context)`. If every `$var` inside a group is empty, the whole group (including its literals) is dropped.
389
+ - `$session_name` is available whenever `footerFormat` is set, independently of `footerSegments.sessionName`; use a conditional group such as `($sep$session_name)` so unnamed sessions leave no separator.
357
390
  - Unknown `$variables` render empty.
358
391
  - Set or clear at runtime: `/zentui format "<template>"` and `/zentui format clear`.
359
392
 
@@ -409,7 +442,7 @@ Mouse wheel scrolling is enabled by default when the fixed editor is on. Disable
409
442
 
410
443
  ## Requirements
411
444
 
412
- - [Pi](https://pi.dev) coding agent 0.80 or newer
445
+ - [Pi](https://pi.dev) coding agent 0.80.3 or newer
413
446
  - A [Nerd Font](https://www.nerdfonts.com/) for icons (or set `icons.mode` to `"ascii"`)
414
447
 
415
448
  ## Development
@@ -32,6 +32,7 @@ export type { IconMode } from "./icons";
32
32
 
33
33
  export type ContextStyle = "text" | "gauge" | "text+gauge";
34
34
  export type SeparatorStyle = "pipe" | "dot" | "chevron" | "none";
35
+ export type ModelLabelSource = "id" | "name";
35
36
 
36
37
  export type ContextThresholds = {
37
38
  warning: number;
@@ -66,6 +67,7 @@ export type UiFeaturesConfig = {
66
67
 
67
68
  export type FooterSegmentsConfig = {
68
69
  cwd: boolean;
70
+ sessionName: boolean;
69
71
  gitBranch: boolean;
70
72
  gitStatus: boolean;
71
73
  gitCounts: boolean;
@@ -120,18 +122,22 @@ export type ExtensionStatusesConfig = {
120
122
 
121
123
  const DEFAULT_PROJECT_REFRESH_INTERVAL_MS = 30_000;
122
124
  const MIN_PROJECT_REFRESH_INTERVAL_MS = 5_000;
125
+ export const DEFAULT_EDITOR_METADATA_FORMAT = "$model $provider( $thinking)";
123
126
 
124
127
  export type PolishedTuiConfig = {
125
128
  projectRefreshIntervalMs: number;
126
129
  footerFormat: string;
130
+ editorMetadataFormat: string;
127
131
  separator: SeparatorStyle;
128
132
  contextStyle: ContextStyle;
133
+ editorModelLabel: ModelLabelSource;
129
134
  contextThresholds: ContextThresholds;
130
135
  pathDisplay: PathDisplayConfig;
131
136
  gitBranch: GitBranchConfig;
132
137
  icons: ResolvedIcons;
133
138
  colors: {
134
139
  cwd: ColorSpec;
140
+ sessionName: ColorSpec;
135
141
  gitBranch: ColorSpec;
136
142
  gitStatus: ColorSpec;
137
143
  contextNormal: ColorSpec;
@@ -177,6 +183,7 @@ export type PolishedTuiConfig = {
177
183
  */
178
184
  export const FOOTER_FORMAT_VARIABLES = [
179
185
  "cwd",
186
+ "session_name",
180
187
  "git_branch",
181
188
  "git_status",
182
189
  "git_state",
@@ -218,8 +225,10 @@ export const configPath = join(getAgentDir(), "zentui.json");
218
225
  export const defaultConfig: PolishedTuiConfig = {
219
226
  projectRefreshIntervalMs: DEFAULT_PROJECT_REFRESH_INTERVAL_MS,
220
227
  footerFormat: "",
228
+ editorMetadataFormat: DEFAULT_EDITOR_METADATA_FORMAT,
221
229
  separator: "pipe",
222
230
  contextStyle: "text",
231
+ editorModelLabel: "id",
223
232
  contextThresholds: { warning: 70, error: 90 },
224
233
  pathDisplay: { mode: "basename", depth: 0 },
225
234
  gitBranch: { maxLength: "full" },
@@ -229,6 +238,7 @@ export const defaultConfig: PolishedTuiConfig = {
229
238
  },
230
239
  colors: {
231
240
  cwd: "bold cyan",
241
+ sessionName: "bold green",
232
242
  gitBranch: "bold purple",
233
243
  gitStatus: "bold red",
234
244
  contextNormal: "bright-black",
@@ -260,6 +270,7 @@ export const defaultConfig: PolishedTuiConfig = {
260
270
  },
261
271
  footerSegments: {
262
272
  cwd: true,
273
+ sessionName: true,
263
274
  gitBranch: true,
264
275
  gitStatus: true,
265
276
  gitCounts: false,
@@ -322,6 +333,11 @@ function parseContextStyle(value: unknown): ContextStyle {
322
333
  return defaultConfig.contextStyle;
323
334
  }
324
335
 
336
+ function parseEditorModelLabel(value: unknown): ModelLabelSource {
337
+ if (value === "id" || value === "name") return value;
338
+ return defaultConfig.editorModelLabel;
339
+ }
340
+
325
341
  export function isSeparatorStyle(value: unknown): value is SeparatorStyle {
326
342
  return value === "pipe" || value === "dot" || value === "chevron" || value === "none";
327
343
  }
@@ -431,6 +447,7 @@ function normalizeIconOverrides(record: Record<string, unknown>): Partial<IconGl
431
447
  function normalizeColors(record: Record<string, unknown>): Partial<PolishedTuiConfig["colors"]> {
432
448
  return definedColors({
433
449
  cwd: colorValue(record, "cwd") ?? colorValue(record, "cwdText"),
450
+ sessionName: colorValue(record, "sessionName"),
434
451
  gitBranch: colorValue(record, "gitBranch") ?? colorValue(record, "git"),
435
452
  gitStatus: colorValue(record, "gitStatus"),
436
453
  contextNormal: colorValue(record, "contextNormal"),
@@ -482,6 +499,7 @@ function normalizeUiFeatures(record: Record<string, unknown>): UiFeaturesConfig
482
499
  function normalizeFooterSegments(record: Record<string, unknown>): FooterSegmentsConfig {
483
500
  return {
484
501
  cwd: footerSegmentValue(record, "cwd"),
502
+ sessionName: footerSegmentValue(record, "sessionName"),
485
503
  gitBranch: footerSegmentValue(record, "gitBranch"),
486
504
  gitStatus: footerSegmentValue(record, "gitStatus"),
487
505
  gitCounts: footerSegmentValue(record, "gitCounts"),
@@ -593,6 +611,7 @@ function isUiFeatureKey(value: string): value is keyof UiFeaturesConfig {
593
611
  function isFooterSegmentKey(value: string): value is keyof FooterSegmentsConfig {
594
612
  return (
595
613
  value === "cwd" ||
614
+ value === "sessionName" ||
596
615
  value === "gitBranch" ||
597
616
  value === "gitStatus" ||
598
617
  value === "gitCounts" ||
@@ -753,11 +772,17 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
753
772
  const fixedEditor = isRecord(config.fixedEditor)
754
773
  ? normalizeFixedEditorConfig(config.fixedEditor as Record<string, unknown>)
755
774
  : defaultConfig.fixedEditor;
775
+ const editorMetadataFormat = stringValue(config, "editorMetadataFormat");
756
776
  return {
757
777
  projectRefreshIntervalMs: parseProjectRefreshIntervalMs(config.projectRefreshIntervalMs),
758
778
  footerFormat: stringValue(config, "footerFormat") ?? "",
779
+ editorMetadataFormat:
780
+ editorMetadataFormat && editorMetadataFormat.length > 0
781
+ ? editorMetadataFormat
782
+ : DEFAULT_EDITOR_METADATA_FORMAT,
759
783
  separator: parseSeparatorStyle(config.separator),
760
784
  contextStyle: parseContextStyle(config.contextStyle),
785
+ editorModelLabel: parseEditorModelLabel(config.editorModelLabel),
761
786
  contextThresholds: parseContextThresholds(config.contextThresholds),
762
787
  pathDisplay: parsePathDisplay(config.pathDisplay),
763
788
  gitBranch,
@@ -0,0 +1,259 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import type { PolishedTuiConfig } from "./config";
3
+ import { type FormatToken, parseFooterFormat } from "./footer-format";
4
+ import { EDITOR_ACCENT_FALLBACK, renderStyleForSourceOrFallback, safeThemeFg } from "./style";
5
+
6
+ export type EditorMetadataValues = {
7
+ model: string;
8
+ modelId: string;
9
+ modelName: string;
10
+ provider: string;
11
+ thinking: string;
12
+ sessionName: string;
13
+ };
14
+
15
+ type RenderedTokens = {
16
+ styled: string;
17
+ hasDynamic: boolean;
18
+ hasNonEmptyDynamic: boolean;
19
+ };
20
+
21
+ const ESC = 0x1b;
22
+ const BEL = 0x07;
23
+ const CAN = 0x18;
24
+ const SUB = 0x1a;
25
+ const C1_DCS = 0x90;
26
+ const C1_CSI = 0x9b;
27
+ const C1_ST = 0x9c;
28
+ const C1_OSC = 0x9d;
29
+ const C1_SOS = 0x98;
30
+ const C1_PM = 0x9e;
31
+ const C1_APC = 0x9f;
32
+
33
+ function consumeCsi(value: string, start: number): number {
34
+ for (let index = start; index < value.length; index++) {
35
+ const code = value.charCodeAt(index);
36
+ if (code === CAN || code === SUB) return index + 1;
37
+ if (code >= 0x40 && code <= 0x7e) return index + 1;
38
+ }
39
+ return value.length;
40
+ }
41
+
42
+ function consumeControlString(value: string, start: number, allowBel: boolean): number {
43
+ for (let index = start; index < value.length; index++) {
44
+ const code = value.charCodeAt(index);
45
+ if (code === CAN || code === SUB) return index + 1;
46
+ if (allowBel && code === BEL) return index + 1;
47
+ if (code === C1_ST) return index + 1;
48
+ if (code === ESC && value.charCodeAt(index + 1) === 0x5c) return index + 2;
49
+ }
50
+ return value.length;
51
+ }
52
+
53
+ function consumeEscape(value: string, start: number): number {
54
+ if (start + 1 >= value.length) return value.length;
55
+ const next = value.charCodeAt(start + 1);
56
+ if (next === 0x5b) return consumeCsi(value, start + 2);
57
+ if (next === 0x5d) return consumeControlString(value, start + 2, true);
58
+ if (next === 0x50 || next === 0x58 || next === 0x5e || next === 0x5f) {
59
+ return consumeControlString(value, start + 2, false);
60
+ }
61
+
62
+ let index = start + 1;
63
+ while (index < value.length) {
64
+ const code = value.charCodeAt(index);
65
+ if (code >= 0x20 && code <= 0x2f) {
66
+ index += 1;
67
+ continue;
68
+ }
69
+ return code >= 0x30 && code <= 0x7e ? index + 1 : index;
70
+ }
71
+ return value.length;
72
+ }
73
+
74
+ function isNormalizedWhitespace(code: number): boolean {
75
+ return (
76
+ code === 0x09 ||
77
+ code === 0x0a ||
78
+ code === 0x0b ||
79
+ code === 0x0c ||
80
+ code === 0x0d ||
81
+ code === 0x85 ||
82
+ code === 0x2028 ||
83
+ code === 0x2029
84
+ );
85
+ }
86
+
87
+ export function sanitizeEditorMetadataText(value: string): string {
88
+ let sanitized = "";
89
+ for (let index = 0; index < value.length; ) {
90
+ const code = value.charCodeAt(index);
91
+ if (code === ESC) {
92
+ index = consumeEscape(value, index);
93
+ continue;
94
+ }
95
+ if (code === C1_CSI) {
96
+ index = consumeCsi(value, index + 1);
97
+ continue;
98
+ }
99
+ if (code === C1_OSC) {
100
+ index = consumeControlString(value, index + 1, true);
101
+ continue;
102
+ }
103
+ if (code === C1_DCS || code === C1_SOS || code === C1_PM || code === C1_APC) {
104
+ index = consumeControlString(value, index + 1, false);
105
+ continue;
106
+ }
107
+ if (isNormalizedWhitespace(code)) {
108
+ sanitized += " ";
109
+ do index += 1;
110
+ while (index < value.length && isNormalizedWhitespace(value.charCodeAt(index)));
111
+ continue;
112
+ }
113
+ if (code < 0x20 || (code >= 0x7f && code <= 0x9f)) {
114
+ index += 1;
115
+ continue;
116
+ }
117
+ sanitized += value[index];
118
+ index += 1;
119
+ }
120
+ return sanitized;
121
+ }
122
+
123
+ function editorThinkingStyle(config: PolishedTuiConfig, level: string): string | undefined {
124
+ switch (level.toLowerCase()) {
125
+ case "minimal":
126
+ return config.colors.editorThinkingMinimal ?? config.colors.editorThinking;
127
+ case "low":
128
+ return config.colors.editorThinkingLow ?? config.colors.editorThinking;
129
+ case "medium":
130
+ return config.colors.editorThinkingMedium ?? config.colors.editorThinking;
131
+ case "high":
132
+ return config.colors.editorThinkingHigh ?? config.colors.editorThinking;
133
+ case "xhigh":
134
+ return config.colors.editorThinkingXhigh ?? config.colors.editorThinking;
135
+ default:
136
+ return config.colors.editorThinking;
137
+ }
138
+ }
139
+
140
+ function renderVariable(
141
+ name: string,
142
+ values: EditorMetadataValues,
143
+ uiTheme: Theme,
144
+ config: PolishedTuiConfig,
145
+ ): { plain: string; styled: string } {
146
+ const colorSource = config.colorSources.editor;
147
+ const thinking = values.thinking.toLowerCase() === "off" ? "" : values.thinking;
148
+ const raw =
149
+ name === "model"
150
+ ? values.model
151
+ : name === "model_id"
152
+ ? values.modelId
153
+ : name === "model_name"
154
+ ? values.modelName
155
+ : name === "provider"
156
+ ? values.provider
157
+ : name === "thinking"
158
+ ? thinking
159
+ : name === "session_name"
160
+ ? values.sessionName
161
+ : "";
162
+ const plain = sanitizeEditorMetadataText(raw);
163
+ if (!plain) return { plain: "", styled: "" };
164
+
165
+ if (name === "model" || name === "model_id" || name === "model_name") {
166
+ return {
167
+ plain,
168
+ styled: renderStyleForSourceOrFallback(
169
+ uiTheme,
170
+ colorSource,
171
+ config.colors.editorModel,
172
+ EDITOR_ACCENT_FALLBACK,
173
+ plain,
174
+ ),
175
+ };
176
+ }
177
+ if (name === "provider") {
178
+ return {
179
+ plain,
180
+ styled: renderStyleForSourceOrFallback(
181
+ uiTheme,
182
+ colorSource,
183
+ config.colors.editorProvider,
184
+ "text",
185
+ plain,
186
+ ),
187
+ };
188
+ }
189
+ if (name === "thinking") {
190
+ return {
191
+ plain,
192
+ styled: renderStyleForSourceOrFallback(
193
+ uiTheme,
194
+ colorSource,
195
+ editorThinkingStyle(config, plain),
196
+ "muted",
197
+ plain,
198
+ ),
199
+ };
200
+ }
201
+ if (name === "session_name") {
202
+ return { plain, styled: safeThemeFg(uiTheme, "border", plain) };
203
+ }
204
+ return { plain: "", styled: "" };
205
+ }
206
+
207
+ function renderTokens(
208
+ tokens: FormatToken[],
209
+ values: EditorMetadataValues,
210
+ uiTheme: Theme,
211
+ config: PolishedTuiConfig,
212
+ ): RenderedTokens {
213
+ let styled = "";
214
+ let hasDynamic = false;
215
+ let hasNonEmptyDynamic = false;
216
+
217
+ for (const token of tokens) {
218
+ if (token.kind === "text") {
219
+ const plain = sanitizeEditorMetadataText(token.value);
220
+ if (plain) styled += safeThemeFg(uiTheme, "border", plain);
221
+ continue;
222
+ }
223
+ if (token.kind === "fill") {
224
+ hasDynamic = true;
225
+ continue;
226
+ }
227
+ if (token.kind === "var") {
228
+ hasDynamic = true;
229
+ const rendered = renderVariable(token.name, values, uiTheme, config);
230
+ styled += rendered.styled;
231
+ if (rendered.plain) hasNonEmptyDynamic = true;
232
+ continue;
233
+ }
234
+
235
+ const rendered = renderTokens(token.tokens, values, uiTheme, config);
236
+ const visible = !rendered.hasDynamic || rendered.hasNonEmptyDynamic;
237
+ hasDynamic = true;
238
+ if (visible) {
239
+ styled += rendered.styled;
240
+ hasNonEmptyDynamic = true;
241
+ }
242
+ }
243
+
244
+ return { styled, hasDynamic, hasNonEmptyDynamic };
245
+ }
246
+
247
+ export function renderEditorMetadataFormat(
248
+ format: string,
249
+ values: EditorMetadataValues,
250
+ uiTheme: Theme,
251
+ config: PolishedTuiConfig,
252
+ ): string {
253
+ return renderTokens(
254
+ parseFooterFormat(sanitizeEditorMetadataText(format)),
255
+ values,
256
+ uiTheme,
257
+ config,
258
+ ).styled;
259
+ }
@@ -2,7 +2,11 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
3
  import type { PolishedTuiConfig, SeparatorStyle } from "./config";
4
4
  import { FOOTER_FORMAT_ALIASES } from "./config";
5
- import { collectExtensionStatusSegments, type ExtensionStatusSegment } from "./extension-status";
5
+ import {
6
+ collectExtensionStatusSegments,
7
+ type ExtensionStatusSegment,
8
+ sanitizeExtensionStatusText,
9
+ } from "./extension-status";
6
10
  import { parseFooterFormat, renderFormatSplit, stripOrphanSeparators } from "./footer-format";
7
11
  import {
8
12
  buildContextDisplayLabel,
@@ -195,6 +199,16 @@ export function installFooter(
195
199
  depth: config.pathDisplay.depth,
196
200
  }),
197
201
  );
202
+ const needsSessionName = config.footerFormat
203
+ ? /(?:\$session_name\b|\$\{session_name\})/.test(config.footerFormat)
204
+ : config.footerSegments.sessionName;
205
+ const sessionName = needsSessionName
206
+ ? sanitizeExtensionStatusText(ctx.sessionManager.getSessionName() ?? "")
207
+ : "";
208
+ const sessionNameLabel = sessionName
209
+ ? renderStyleForSource(theme, colorSource, config.colors.sessionName, sessionName)
210
+ : "";
211
+ const builtInSessionNameLabel = sessionNameLabel ? `in ${sessionNameLabel}` : "";
198
212
  const branch = state.branch;
199
213
  const branchText = branch
200
214
  ? formatGitBranchText(branch, config.gitBranch.maxLength)
@@ -263,6 +277,8 @@ export function installFooter(
263
277
  switch (canonical) {
264
278
  case "cwd":
265
279
  return cwdLabel;
280
+ case "session_name":
281
+ return sessionNameLabel;
266
282
  case "git_branch":
267
283
  return branchText
268
284
  ? gitIcon
@@ -491,6 +507,7 @@ export function installFooter(
491
507
  osSegment,
492
508
  usernameSegment,
493
509
  config.footerSegments.cwd ? cwdLabel : "",
510
+ config.footerSegments.sessionName ? builtInSessionNameLabel : "",
494
511
  branchLabel,
495
512
  gitCommitLabel,
496
513
  gitMetricsLabel,
@@ -120,7 +120,7 @@ export default function (pi: ExtensionAPI) {
120
120
  const getThinkingLevel = () =>
121
121
  sessionLifecycle.isCurrent() ? pi.getThinkingLevel() : ("off" as const);
122
122
  const syncFooterState = (ctx: ExtensionContext) =>
123
- syncState(state, ctx, currentConfig.icons.cacheHit);
123
+ syncState(state, ctx, currentConfig.icons.cacheHit, currentConfig.editorModelLabel);
124
124
 
125
125
  type ProjectRefreshTarget = { cwd: string; generation: number };
126
126
  const refreshProjectState = async ({ cwd, generation }: ProjectRefreshTarget) => {
@@ -244,7 +244,10 @@ export default function (pi: ExtensionAPI) {
244
244
  getCurrentConfig,
245
245
  () => ({
246
246
  modelLabel: state.modelLabel,
247
+ modelId: state.modelId,
248
+ modelName: state.modelName,
247
249
  providerLabel: state.providerLabel,
250
+ sessionName: ctx.sessionManager.getSessionName() ?? "",
248
251
  }),
249
252
  getThinkingLevel,
250
253
  )) as ZentuiEditorFactory;
@@ -264,7 +267,10 @@ export default function (pi: ExtensionAPI) {
264
267
  getCurrentConfig,
265
268
  () => ({
266
269
  modelLabel: state.modelLabel,
270
+ modelId: state.modelId,
271
+ modelName: state.modelName,
267
272
  providerLabel: state.providerLabel,
273
+ sessionName: ctx.sessionManager.getSessionName() ?? "",
268
274
  }),
269
275
  getThinkingLevel,
270
276
  )) as ZentuiEditorFactory;
@@ -536,6 +542,7 @@ export default function (pi: ExtensionAPI) {
536
542
  syncInteractiveState(event, ctx);
537
543
  });
538
544
  pi.on("thinking_level_select", syncInteractiveState);
545
+ pi.on("session_info_changed", syncInteractiveState);
539
546
  pi.on("message_update", (event) => {
540
547
  liveContext.update(event.message);
541
548
  });
@@ -124,6 +124,7 @@ const featureSettingDescriptions: Record<FeatureSettingId, string> = {
124
124
 
125
125
  const footerSegmentSettingLabels: Record<FooterSegmentSettingId, string> = {
126
126
  cwd: "Current directory",
127
+ sessionName: "Session name",
127
128
  gitBranch: "Git branch",
128
129
  gitStatus: "Git status",
129
130
  gitCounts: "Git counts",
@@ -142,6 +143,7 @@ const footerSegmentSettingLabels: Record<FooterSegmentSettingId, string> = {
142
143
 
143
144
  const footerSegmentSettingDescriptions: Record<FooterSegmentSettingId, string> = {
144
145
  cwd: "Show or hide the current working directory segment on the left.",
146
+ sessionName: "Show or hide the current Pi session name on the left, after the current directory.",
145
147
  gitBranch: "Show or hide the git branch name on the left.",
146
148
  gitStatus: "Show or hide git status icons and ahead/behind markers.",
147
149
  gitCounts:
@@ -207,6 +209,7 @@ function isFeatureSettingId(value: string): value is FeatureSettingId {
207
209
  function isFooterSegmentSettingId(value: string): value is FooterSegmentSettingId {
208
210
  return (
209
211
  value === "cwd" ||
212
+ value === "sessionName" ||
210
213
  value === "gitBranch" ||
211
214
  value === "gitStatus" ||
212
215
  value === "gitCounts" ||
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { ModelLabelSource } from "./config";
2
3
  import {
3
4
  buildContextLabel,
4
5
  buildCostLabel,
@@ -12,6 +13,8 @@ import type { RuntimeInfo } from "./runtime";
12
13
 
13
14
  export type FooterState = GitStatusSummary & {
14
15
  modelLabel: string;
16
+ modelId: string;
17
+ modelName: string;
15
18
  providerLabel: string;
16
19
  contextLabel: string;
17
20
  tokenLabel: string;
@@ -24,6 +27,8 @@ export type FooterState = GitStatusSummary & {
24
27
  export function createInitialState(gitDefaults: GitStatusSummary): FooterState {
25
28
  return {
26
29
  modelLabel: "no-model",
30
+ modelId: "",
31
+ modelName: "",
27
32
  providerLabel: "Unknown",
28
33
  contextLabel: "--",
29
34
  tokenLabel: "↑0 ↓0",
@@ -35,9 +40,17 @@ export function createInitialState(gitDefaults: GitStatusSummary): FooterState {
35
40
  };
36
41
  }
37
42
 
38
- export function syncState(state: FooterState, ctx: ExtensionContext, cacheHitIcon: string): void {
43
+ export function syncState(
44
+ state: FooterState,
45
+ ctx: ExtensionContext,
46
+ cacheHitIcon: string,
47
+ modelLabelSource: ModelLabelSource,
48
+ ): void {
39
49
  const totals = getUsageTotals(ctx);
40
- state.modelLabel = ctx.model?.id ?? "no-model";
50
+ const m = ctx.model;
51
+ state.modelId = m?.id ?? "";
52
+ state.modelName = m?.name ?? "";
53
+ state.modelLabel = (modelLabelSource === "name" ? m?.name || m?.id : m?.id) ?? "no-model";
41
54
  state.providerLabel = formatProviderLabel(ctx.model?.provider);
42
55
  state.contextLabel = buildContextLabel(ctx);
43
56
  state.tokenLabel = buildTokenLabel(totals, cacheHitIcon);
@@ -9,6 +9,7 @@ import {
9
9
  visibleWidth,
10
10
  } from "@earendil-works/pi-tui";
11
11
  import type { PolishedTuiConfig } from "./config";
12
+ import { renderEditorMetadataFormat } from "./editor-metadata-format";
12
13
  import {
13
14
  EDITOR_ACCENT_FALLBACK,
14
15
  EDITOR_BORDER_FALLBACK,
@@ -16,6 +17,13 @@ import {
16
17
  safeThemeFg,
17
18
  } from "./style";
18
19
 
20
+ const SPLIT_POLISHED_FRAME: unique symbol = Symbol.for("pi-zentui.polished-frame");
21
+
22
+ type PolishedFrameSplit = {
23
+ editorLines: string[];
24
+ trailingLines: string[];
25
+ };
26
+
19
27
  type AutocompleteEditorInternals = {
20
28
  autocompleteList?: Pick<Component, "render">;
21
29
  isShowingAutocomplete?: () => boolean;
@@ -42,11 +50,15 @@ type WrappedEditor = EditorComponent &
42
50
  setAutocompleteProvider?: (provider: AutocompleteProvider) => void;
43
51
  setPaddingX?: (padding: number) => void;
44
52
  setAutocompleteMaxVisible?: (maxVisible: number) => void;
53
+ [SPLIT_POLISHED_FRAME]?: (lines: string[]) => PolishedFrameSplit | undefined;
45
54
  };
46
55
 
47
56
  type EditorMeta = {
48
57
  modelLabel: string;
58
+ modelId?: string;
59
+ modelName?: string;
49
60
  providerLabel: string;
61
+ sessionName?: string;
50
62
  };
51
63
 
52
64
  type PolishedFrameOptions = {
@@ -56,9 +68,9 @@ type PolishedFrameOptions = {
56
68
  uiTheme: Theme;
57
69
  config: PolishedTuiConfig;
58
70
  modelMeta: EditorMeta;
59
- previousModelMeta?: EditorMeta;
60
71
  thinkingLevel: string | undefined;
61
72
  rightStatus?: string;
73
+ splitBaseFrame?: (lines: string[]) => PolishedFrameSplit | undefined;
62
74
  };
63
75
 
64
76
  function clampRenderedLines(lines: string[], width: number): string[] {
@@ -72,23 +84,6 @@ function fillLine(content: string, width: number): string {
72
84
  return `${truncated}${pad}`;
73
85
  }
74
86
 
75
- function editorThinkingStyle(config: PolishedTuiConfig, level: string): string | undefined {
76
- switch (level.toLowerCase()) {
77
- case "minimal":
78
- return config.colors.editorThinkingMinimal ?? config.colors.editorThinking;
79
- case "low":
80
- return config.colors.editorThinkingLow ?? config.colors.editorThinking;
81
- case "medium":
82
- return config.colors.editorThinkingMedium ?? config.colors.editorThinking;
83
- case "high":
84
- return config.colors.editorThinkingHigh ?? config.colors.editorThinking;
85
- case "xhigh":
86
- return config.colors.editorThinkingXhigh ?? config.colors.editorThinking;
87
- default:
88
- return config.colors.editorThinking;
89
- }
90
- }
91
-
92
87
  function copyFriendlyPrompt(config: PolishedTuiConfig, uiTheme: Theme, reset: string): string {
93
88
  const promptIcon = config.icons.editorPrompt;
94
89
  return promptIcon
@@ -137,7 +132,7 @@ function plainRenderedText(line: string): string {
137
132
  return line
138
133
  .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
139
134
  .replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
140
- .replace(/\[[/?][^\]]+\]/g, "");
135
+ .replace(/\[\/?[^\]]+\]/g, "");
141
136
  }
142
137
 
143
138
  function isHorizontalBorder(line: string): boolean {
@@ -145,70 +140,67 @@ function isHorizontalBorder(line: string): boolean {
145
140
  return plain.length > 0 && /^─+$/.test(plain);
146
141
  }
147
142
 
148
- function isRenderedModelMetaLine(line: string, modelMeta: EditorMeta): boolean {
149
- const plain = plainRenderedText(line);
150
- return plain.includes(modelMeta.modelLabel) && plain.includes(modelMeta.providerLabel);
151
- }
152
-
153
- function matchesAnyModelMeta(
154
- line: string,
155
- modelMeta: EditorMeta,
156
- previousMeta?: EditorMeta,
157
- ): boolean {
158
- if (isRenderedModelMetaLine(line, modelMeta)) return true;
159
- if (previousMeta && isRenderedModelMetaLine(line, previousMeta)) return true;
160
- return false;
161
- }
162
-
163
- function hasRenderedModelMetaLine(
164
- lines: string[],
165
- modelMeta: EditorMeta,
166
- previousMeta?: EditorMeta,
167
- ): boolean {
168
- return lines.some((line) => matchesAnyModelMeta(line, modelMeta, previousMeta));
169
- }
170
-
171
- function isAlreadyPolishedFrame(
143
+ function unwrapPolishedFrameOnly(
172
144
  lines: string[],
173
- modelMeta: EditorMeta,
174
- previousMeta?: EditorMeta,
175
- ): boolean {
176
- return (
177
- lines.length >= 3 &&
178
- isHorizontalBorder(lines[0] ?? "") &&
179
- isHorizontalBorder(lines.at(-1) ?? "") &&
180
- hasRenderedModelMetaLine(lines.slice(1, -1), modelMeta, previousMeta)
181
- );
145
+ config: PolishedTuiConfig,
146
+ uiTheme: Theme,
147
+ ): string[] | undefined {
148
+ if (
149
+ lines.length < 5 ||
150
+ !isHorizontalBorder(lines[0] ?? "") ||
151
+ !isHorizontalBorder(lines.at(-1) ?? "")
152
+ )
153
+ return undefined;
154
+
155
+ const interior = lines.slice(1, -1);
156
+ if (interior.length < 3) return undefined;
157
+
158
+ if (config.features.copyFriendly) {
159
+ if (
160
+ plainRenderedText(interior[0] ?? "").trim() !== "" ||
161
+ plainRenderedText(interior.at(-2) ?? "").trim() !== "" ||
162
+ !(interior.at(-1) ?? "").startsWith(" ")
163
+ )
164
+ return undefined;
165
+
166
+ const { prompt, promptWidth } = getEditorChromeWidths(config, uiTheme, "\x1b[0m");
167
+ const continuation = " ".repeat(promptWidth);
168
+ const content = interior.slice(1, -2);
169
+ const unwrapped: string[] = [];
170
+ for (let index = 0; index < content.length; index++) {
171
+ const prefix = index === 0 ? prompt : continuation;
172
+ const line = content[index] ?? "";
173
+ if (prefix && !line.startsWith(prefix)) return undefined;
174
+ unwrapped.push(prefix ? line.slice(prefix.length) : line);
175
+ }
176
+ return unwrapped;
177
+ }
178
+
179
+ const { rail } = getEditorChromeWidths(config, uiTheme, "\x1b[0m");
180
+ if (!rail || interior.some((line) => !line.startsWith(rail))) return undefined;
181
+ const unrailed = interior.map((line) => line.slice(rail.length));
182
+ if (
183
+ plainRenderedText(unrailed[0] ?? "").trim() !== "" ||
184
+ plainRenderedText(unrailed.at(-2) ?? "").trim() !== ""
185
+ )
186
+ return undefined;
187
+ return unrailed.slice(1, -2);
182
188
  }
183
189
 
184
- function removeRenderedModelMetaLines(
190
+ function splitPolishedFrame(
185
191
  lines: string[],
186
- modelMeta: EditorMeta,
187
- previousMeta?: EditorMeta,
188
- ): string[] {
189
- const result: string[] = [];
190
- for (let index = 0; index < lines.length; index++) {
191
- const line = lines[index] ?? "";
192
- if (matchesAnyModelMeta(line, modelMeta, previousMeta)) continue;
193
-
194
- const plain = plainRenderedText(line).trim();
195
- const previousWasMeta =
196
- index > 0 && matchesAnyModelMeta(lines[index - 1] ?? "", modelMeta, previousMeta);
197
- const nextIsMeta =
198
- index < lines.length - 1 &&
199
- matchesAnyModelMeta(lines[index + 1] ?? "", modelMeta, previousMeta);
200
- if (!plain && (previousWasMeta || nextIsMeta)) continue;
201
-
202
- result.push(line);
203
- }
204
- return result;
205
- }
206
-
207
- function removeStalePolishedLeadingSpacer(lines: string[], shouldRemove: boolean): string[] {
208
- if (!shouldRemove || lines.length === 0) return lines;
209
- const firstLine = lines[0] ?? "";
210
- if (plainRenderedText(firstLine).trim()) return lines;
211
- return lines.slice(1);
192
+ config: PolishedTuiConfig,
193
+ uiTheme: Theme,
194
+ ): PolishedFrameSplit | undefined {
195
+ if (!isHorizontalBorder(lines[0] ?? "")) return undefined;
196
+ for (let bottomIndex = lines.length - 1; bottomIndex >= 4; bottomIndex--) {
197
+ if (!isHorizontalBorder(lines[bottomIndex] ?? "")) continue;
198
+ const editorLines = unwrapPolishedFrameOnly(lines.slice(0, bottomIndex + 1), config, uiTheme);
199
+ if (editorLines) {
200
+ return { editorLines, trailingLines: lines.slice(bottomIndex + 1) };
201
+ }
202
+ }
203
+ return undefined;
212
204
  }
213
205
 
214
206
  function vimModeColor(mode: string): string {
@@ -244,9 +236,9 @@ function renderPolishedFrame({
244
236
  uiTheme,
245
237
  config,
246
238
  modelMeta,
247
- previousModelMeta,
248
239
  thinkingLevel,
249
240
  rightStatus,
241
+ splitBaseFrame,
250
242
  }: PolishedFrameOptions): string[] {
251
243
  if (width <= 2) return clampRenderedLines(baseRendered, width);
252
244
 
@@ -262,56 +254,37 @@ function renderPolishedFrame({
262
254
 
263
255
  if (baseRendered.length < 2) return clampRenderedLines(baseRendered, width);
264
256
 
257
+ const ownedFrame = splitBaseFrame?.(baseRendered);
265
258
  const { autocompleteList } = autocompleteSource;
266
259
  const autocompleteCount =
267
- isShowingAutocomplete && typeof autocompleteList?.render === "function"
260
+ !ownedFrame && isShowingAutocomplete && typeof autocompleteList?.render === "function"
268
261
  ? autocompleteList.render(innerWidth).length
269
262
  : 0;
270
263
  const editorFrame =
271
- autocompleteCount > 0 && autocompleteCount < baseRendered.length
264
+ !ownedFrame && autocompleteCount > 0 && autocompleteCount < baseRendered.length
272
265
  ? baseRendered.slice(0, -autocompleteCount)
273
266
  : baseRendered;
274
- const autocompleteLines =
275
- autocompleteCount > 0 && autocompleteCount < baseRendered.length
267
+ const autocompleteLines = ownedFrame
268
+ ? ownedFrame.trailingLines
269
+ : autocompleteCount > 0 && autocompleteCount < baseRendered.length
276
270
  ? baseRendered.slice(-autocompleteCount)
277
271
  : [];
278
272
  if (editorFrame.length < 2) return clampRenderedLines(baseRendered, width);
279
273
 
280
- const stalePolishedFrame = isAlreadyPolishedFrame(editorFrame, modelMeta, previousModelMeta);
281
- const editorLines = removeStalePolishedLeadingSpacer(
282
- removeRenderedModelMetaLines(editorFrame.slice(1, -1), modelMeta, previousModelMeta),
283
- stalePolishedFrame,
284
- );
285
- const model = renderStyleForSourceOrFallback(
274
+ const editorLines = ownedFrame?.editorLines ?? editorFrame.slice(1, -1);
275
+ const meta = renderEditorMetadataFormat(
276
+ config.editorMetadataFormat,
277
+ {
278
+ model: modelMeta.modelLabel,
279
+ modelId: modelMeta.modelId ?? "",
280
+ modelName: modelMeta.modelName ?? "",
281
+ provider: modelMeta.providerLabel,
282
+ thinking: thinkingLevel ?? "",
283
+ sessionName: modelMeta.sessionName ?? "",
284
+ },
286
285
  uiTheme,
287
- colorSource,
288
- config.colors.editorModel,
289
- EDITOR_ACCENT_FALLBACK,
290
- modelMeta.modelLabel,
291
- );
292
- const provider = renderStyleForSourceOrFallback(
293
- uiTheme,
294
- colorSource,
295
- config.colors.editorProvider,
296
- "text",
297
- modelMeta.providerLabel,
286
+ config,
298
287
  );
299
- const renderedModelMeta = [model, provider]
300
- .filter(Boolean)
301
- .join(safeThemeFg(uiTheme, "borderMuted", " "));
302
- const metaParts = [renderedModelMeta];
303
- if (thinkingLevel && thinkingLevel !== "off") {
304
- metaParts.push(
305
- renderStyleForSourceOrFallback(
306
- uiTheme,
307
- colorSource,
308
- editorThinkingStyle(config, thinkingLevel),
309
- "muted",
310
- thinkingLevel,
311
- ),
312
- );
313
- }
314
- const meta = metaParts.filter(Boolean).join(safeThemeFg(uiTheme, "border", " "));
315
288
  const copyFriendlyMeta = composeMetadataLine(meta, rightStatus, Math.max(0, width - 1));
316
289
  const railedMeta = composeMetadataLine(meta, rightStatus, innerWidth);
317
290
 
@@ -358,7 +331,6 @@ export class PolishedEditor extends CustomEditor {
358
331
  private readonly getThinkingLevel: () => string | undefined;
359
332
  private readonly getConfig: () => PolishedTuiConfig;
360
333
  private readonly uiTheme: Theme;
361
- private previousModelMeta?: EditorMeta;
362
334
 
363
335
  constructor(
364
336
  tui: TUI,
@@ -394,17 +366,17 @@ export class PolishedEditor extends CustomEditor {
394
366
  uiTheme: this.uiTheme,
395
367
  config,
396
368
  modelMeta,
397
- previousModelMeta: this.previousModelMeta,
398
369
  thinkingLevel: this.getThinkingLevel(),
399
370
  });
400
- this.previousModelMeta = modelMeta;
401
371
  return result;
402
372
  }
373
+
374
+ [SPLIT_POLISHED_FRAME](lines: string[]): PolishedFrameSplit | undefined {
375
+ return splitPolishedFrame(lines, this.getConfig(), this.uiTheme);
376
+ }
403
377
  }
404
378
 
405
379
  export class WrappedPolishedEditor implements EditorComponent {
406
- private previousModelMeta?: EditorMeta;
407
-
408
380
  constructor(
409
381
  private readonly base: WrappedEditor,
410
382
  private readonly uiTheme: Theme,
@@ -499,19 +471,21 @@ export class WrappedPolishedEditor implements EditorComponent {
499
471
  const rendered = this.base.render(innerWidth);
500
472
  const vimStatus = readVimStatus(this.base, this.uiTheme);
501
473
  const modelMeta = this.getModelMeta();
502
- const result = renderPolishedFrame({
474
+ return renderPolishedFrame({
503
475
  width,
504
476
  baseRendered: rendered,
505
477
  autocompleteSource: this.base,
506
478
  uiTheme: this.uiTheme,
507
479
  config,
508
480
  modelMeta,
509
- previousModelMeta: this.previousModelMeta,
510
481
  thinkingLevel: this.getThinkingLevel(),
511
482
  rightStatus: vimStatus,
483
+ splitBaseFrame: this.base[SPLIT_POLISHED_FRAME]?.bind(this.base),
512
484
  });
513
- this.previousModelMeta = modelMeta;
514
- return result;
485
+ }
486
+
487
+ [SPLIT_POLISHED_FRAME](lines: string[]): PolishedFrameSplit | undefined {
488
+ return splitPolishedFrame(lines, this.getConfig(), this.uiTheme);
515
489
  }
516
490
 
517
491
  invalidate(): void {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-zentui",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "A Starship-inspired statusline and Opencode-style TUI for Pi.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -35,9 +35,9 @@
35
35
  "prepublishOnly": "npm run verify && npm run pack:check"
36
36
  },
37
37
  "peerDependencies": {
38
- "@earendil-works/pi-ai": ">=0.80",
39
- "@earendil-works/pi-coding-agent": ">=0.80",
40
- "@earendil-works/pi-tui": ">=0.80"
38
+ "@earendil-works/pi-ai": ">=0.80.3",
39
+ "@earendil-works/pi-coding-agent": ">=0.80.3",
40
+ "@earendil-works/pi-tui": ">=0.80.3"
41
41
  },
42
42
  "pi": {
43
43
  "extensions": [