pi-zentui 0.18.1 → 0.20.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 +59 -14
- package/extensions/zentui/config.ts +187 -0
- package/extensions/zentui/format.ts +11 -0
- package/extensions/zentui/index.ts +128 -38
- package/extensions/zentui/interaction-summary.ts +1049 -0
- package/extensions/zentui/minimalist-editor.ts +10 -11
- package/extensions/zentui/settings-command.ts +806 -323
- package/extensions/zentui/settings-previews.ts +110 -0
- package/extensions/zentui/style.ts +62 -0
- package/extensions/zentui/ui.ts +58 -21
- package/extensions/zentui/working-line-messages.ts +24 -0
- package/extensions/zentui/working-line-spinners.ts +54 -0
- package/extensions/zentui/working-line.ts +1432 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -8,13 +8,14 @@ A Starship-inspired statusline and Opencode-style TUI for [Pi](https://pi.dev).
|
|
|
8
8
|
|
|
9
9
|
## What is this?
|
|
10
10
|
|
|
11
|
-
Zentui styles
|
|
11
|
+
Zentui styles four major Pi surfaces independently:
|
|
12
12
|
|
|
13
13
|
- **Editor** — selectable opencode, low-rail opencode, and minimalist input frames inspired by [Opencode](https://github.com/opencode-ai/opencode)
|
|
14
14
|
- **User messages** — selectable framed, framed copy-friendly, compact, and labeled transcript messages
|
|
15
|
+
- **Working line** — optional ownership and styling of Pi's complete in-progress row
|
|
15
16
|
- **[Starship](https://starship.rs/) footer** — current directory, Git, runtime, context, tokens, cost, and other configurable segments
|
|
16
17
|
|
|
17
|
-
Editor, User messages, and selector borders use
|
|
18
|
+
Editor, User messages, Working line, and selector borders use independent `enabled` fields. Footer uses one `style`: `native`, `starship`, or `hidden`. **Appearance** contains selector-border and icon settings.
|
|
18
19
|
|
|
19
20
|
## Features
|
|
20
21
|
|
|
@@ -40,7 +41,7 @@ Editor, User messages, and selector borders use an `enabled` field. Footer uses
|
|
|
40
41
|
- Opencode autocomplete rows retain Pi's original unframed trailing layout; Minimalist keeps autocomplete inside its rounded frame
|
|
41
42
|
- Configurable model, provider, thinking-level, accent, and border colors
|
|
42
43
|
|
|
43
|
-
Editor
|
|
44
|
+
Editor styles:
|
|
44
45
|
|
|
45
46
|
<h4 align="center"><code>opencode</code></h4>
|
|
46
47
|
|
|
@@ -81,6 +82,21 @@ User-message previews:
|
|
|
81
82
|
|
|
82
83
|

|
|
83
84
|
|
|
85
|
+
### Working line
|
|
86
|
+
|
|
87
|
+
When enabled, the optional Working line always owns and stylizes Pi's complete working-row message and indicator. It provides five fixed-width spinner presets: Braille Orbit, Star Bloom, ASCII Pinwheel, Claude-inspired, and three-cell Pulse. **Custom messages** defaults on and selects once per model turn from an editable, materialized 16-message list. Turning it off keeps the row owned and displays styled, animated `Working…` without random selection. An empty or invalid custom list safely uses the same fallback. The row can show the latest active **Tool**, interaction-wide **Elapsed** time, cumulative wall-clock **Thinking** time, and whole-interaction **Tokens**. Committed totals stay exact and provider-reported across tool loops, automatic retries, compaction retries, and queued continuations. During the current response, live output uses the native `↓N` convention whether it comes from provider usage or an estimate used while provider usage is unavailable or stale. Authoritative final usage always reconciles the response atomically; input is never estimated. Messages and tool labels are sanitized and width-bounded.
|
|
88
|
+
|
|
89
|
+
When Pi has fully settled and will not continue automatically, the default-on **Turn summary** appends a persistent, context-free transcript row such as ` Turn took 56s · thought for 10s · ↑7.1k ↓779`. Thought is cumulative wall-clock time from Pi's public thinking stream; overlapping blocks count once and zero is omitted. Output usage already includes reasoning tokens, so reasoning is not added separately. The summary always includes both token totals—even when live Tokens or Thinking is hidden or both totals are zero—and can be opted out of without affecting historical summaries. Turn summaries use the fixed Working-line high style and are inactive while the overall Working line is disabled.
|
|
90
|
+
|
|
91
|
+
Spinner glyph motion is always active. Classic and KITT move color across the message and segments by default; **Animate spinner color** optionally includes spinner cells and their separator in that sweep. Static colors the complete row uniformly and ignores text speed and spinner-color participation without changing their saved values. Content fits the 77-cell indicator payload while reserving the complete Tokens label first, then Message, Thought, Elapsed, and Tool allocation, and preserves the visual **Message · Tool · Elapsed · Thought · Tokens** order and complete 80-column Loader-row contract. Active thought appears immediately as `thinking 0s` and updates once per second; completed positive thought appears as `thought for Ns`. Rebuilds preserve the displayed spinner and visible color-sweep position. Pi's working-row APIs are global and unkeyed, so another extension may win by writing last.
|
|
92
|
+
|
|
93
|
+
| Setting | Default | Presets | Applies to |
|
|
94
|
+
| --- | ---: | --- | --- |
|
|
95
|
+
| `spinnerIntervalMs` | 100 ms | Fast 60 / Normal 100 / Slow 160 / Custom | Spinner glyph motion |
|
|
96
|
+
| `textIntervalMs` | 60 ms | Fast 40 / Normal 60 / Slow 100 / Custom | Classic/KITT color motion |
|
|
97
|
+
|
|
98
|
+
Both speeds accept safe integers from `30` through `1000` ms. Static ignores text speed without changing it. Classic/KITT combine both cadences through one Pi Loader interval; exact cycles are used within the 1024-frame/512-KiB limits. Pathological custom pairs use a bounded evenly distributed schedule: cycle totals round by at most half a spinner glyph cycle and half a text step, spinner wrap remains continuous, and text phase may reset once per bounded fallback array cycle. Legacy `intervalMs` is accepted only as migration input for `spinnerIntervalMs` when the canonical field is absent.
|
|
99
|
+
|
|
84
100
|
### Git Status Icons
|
|
85
101
|
|
|
86
102
|
| Icon | Meaning |
|
|
@@ -174,19 +190,20 @@ pi install git:github.com/lmilojevicc/pi-zentui
|
|
|
174
190
|
|
|
175
191
|
User config lives at `~/.pi/agent/zentui.json`. The file is optional: missing or invalid known values fall back to Zentui defaults, unknown keys are ignored at runtime, and `/zentui` can patch color-source settings, UI feature toggles, built-in footer segment visibility, and active third-party status placements.
|
|
176
192
|
|
|
177
|
-
The interactive `/zentui` menu is split into exactly
|
|
193
|
+
The interactive `/zentui` menu is split into exactly eight component-oriented sections, in this order. Use `Tab` and `Shift+Tab` to switch sections:
|
|
178
194
|
|
|
179
195
|
1. **Appearance** — selector-border enablement, style, and colors; icon mode.
|
|
180
|
-
2. **Editor** — editor enablement, style, colors, model label, border behavior, viewport indicators,
|
|
181
|
-
3. **User messages** — message enablement, `framed | framed-copy-friendly | compact | labeled` style selection (including **Framed (copy-friendly)**), and
|
|
182
|
-
4. **
|
|
183
|
-
5. **
|
|
184
|
-
6. **
|
|
185
|
-
7. **
|
|
196
|
+
2. **Editor** — editor enablement, style, colors, model label, border behavior, viewport indicators, settings for the selected editor style, and a static synthetic preview.
|
|
197
|
+
3. **User messages** — message enablement, `framed | framed-copy-friendly | compact | labeled` style selection (including **Framed (copy-friendly)**), colors, and a static synthetic Markdown preview.
|
|
198
|
+
4. **Working line** — ownership, settled Turn summary, spinner and text speeds, optional spinner-color motion, text animation, color source, custom-message toggle and editable list, Tool/Elapsed/Thinking/Tokens toggles, and animated preview.
|
|
199
|
+
5. **Footer** — `Native | Starship | Hidden` style selection. Starship additionally shows colors, model label, responsive layout, separator, context style, and path display.
|
|
200
|
+
6. **Segments** — visibility toggles for non-Git Starship segments.
|
|
201
|
+
7. **Git** — Starship Git segment and probe controls.
|
|
202
|
+
8. **Extensions** — Starship extension-status placement and color controls for active keys.
|
|
186
203
|
|
|
187
|
-
Editor
|
|
204
|
+
Editor, User messages, and Working line retain independent configuration. Editor and User-message previews use fixed synthetic content and remain visible while their component is disabled; the Working-line preview reflects its current configured sample, state, and animation. Each preview appears above its settings. Only the Working-line preview owns an animation timer. Footer's single style selects Pi's built-in Footer (`Native`), Zentui's Starship Footer, or an owned zero-row Footer (`Hidden`). Color and model-label rows update only their owning component.
|
|
188
205
|
|
|
189
|
-
Starship-specific Footer rows are shown only while Starship is selected. The **Segments**, **Git**, and **Extensions** sections remain available for preconfiguration under every Footer style. Free-form values such as custom formats, Opencode metadata format, raw colors/styles,
|
|
206
|
+
Starship-specific Footer rows are shown only while Starship is selected. The **Segments**, **Git**, and **Extensions** sections remain available for preconfiguration under every Footer style. Free-form values such as custom formats, Opencode metadata format, raw colors/styles, and inactive extension keys remain JSON-only; Working-line speed accepts validated custom milliseconds in `/zentui`.
|
|
190
207
|
|
|
191
208
|
Useful slash-command shortcuts:
|
|
192
209
|
|
|
@@ -202,6 +219,7 @@ Useful slash-command shortcuts:
|
|
|
202
219
|
/zentui statusline toggle
|
|
203
220
|
/zentui messages
|
|
204
221
|
/zentui user-messages
|
|
222
|
+
/zentui working-line
|
|
205
223
|
/zentui viewport-indicators enable
|
|
206
224
|
/zentui viewport-indicators disable
|
|
207
225
|
/zentui viewport-indicators toggle
|
|
@@ -257,6 +275,31 @@ Default config values — copy this and change any value you want:
|
|
|
257
275
|
"labeled": {}
|
|
258
276
|
}
|
|
259
277
|
},
|
|
278
|
+
"workingLine": {
|
|
279
|
+
"enabled": false,
|
|
280
|
+
"turnSummary": true,
|
|
281
|
+
"spinner": "star-bloom",
|
|
282
|
+
"spinnerIntervalMs": 100,
|
|
283
|
+
"animateSpinnerColor": false,
|
|
284
|
+
"textIntervalMs": 60,
|
|
285
|
+
"textAnimation": "classic",
|
|
286
|
+
"colorSource": "theme",
|
|
287
|
+
"messages": {
|
|
288
|
+
"custom": true,
|
|
289
|
+
"values": [
|
|
290
|
+
"Sautéing…", "Cooking…", "Ionizing…", "Zigzagging…",
|
|
291
|
+
"Razzle-dazzling…", "Photosynthesizing…", "Nucleating…", "Brewing…",
|
|
292
|
+
"Combobulating…", "Boogieing…", "Befuddling…", "Alchemizing…",
|
|
293
|
+
"Conjuring…", "Baking…", "Simmering…", "Blanching…"
|
|
294
|
+
]
|
|
295
|
+
},
|
|
296
|
+
"segments": {
|
|
297
|
+
"tool": true,
|
|
298
|
+
"elapsed": true,
|
|
299
|
+
"thought": true,
|
|
300
|
+
"tokens": true
|
|
301
|
+
}
|
|
302
|
+
},
|
|
260
303
|
"selectorBorders": {
|
|
261
304
|
"enabled": true,
|
|
262
305
|
"style": "zentui",
|
|
@@ -384,6 +427,8 @@ Default config values — copy this and change any value you want:
|
|
|
384
427
|
- `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`.
|
|
385
428
|
- `components.editor`: owns editor enablement, `opencode | opencode-copy-friendly | minimalist` style selection, color source, border mode, model label, viewport indicators, and all three editor-style configurations.
|
|
386
429
|
- `components.userMessages`: owns message enablement, `framed | framed-copy-friendly | compact | labeled` style selection, and color source. `framed-copy-friendly` remains Zentui-rendered; disabling the component delegates to Pi's native renderer.
|
|
430
|
+
- `components.workingLine`: `enabled` is the sole ownership switch. While enabled, Zentui owns both the Working-row message and indicator and renders the full row. It configures `braille | star-bloom | pinwheel | claude-inspired | pulse`, independent spinner/text speeds, optional Classic/KITT spinner-color participation, `classic | kitt | disabled` text animation, color source, the default-on `messages.custom` toggle and editable 16-value list, plus Tool/Elapsed/Thinking/Tokens segments. Thinking only controls the live row; measurement and final summaries continue while it is hidden. Custom-off and empty-list fallback both render owned `Working…`; Static keeps glyph motion but ignores text speed and spinner-color participation.
|
|
431
|
+
- Optional `colors.workingLineLow`, `colors.workingLineMid`, and `colors.workingLineHigh` override its palette. Without overrides, theme mode uses `dim`, `muted`, and `bold accent`; terminal mode uses `bright-black`, `cyan`, and `bold cyan`.
|
|
387
432
|
- `components.selectorBorders`: owns selector-border enablement, the fixed `zentui` style, and its color source.
|
|
388
433
|
- `components.footer`: owns `native | starship | hidden` style selection, Footer color source, Footer model label, and every Starship option under `styles.starship` (formats, segments, context thresholds, path, Git, and extension statuses). Native restores Pi's built-in Footer; Hidden installs an empty component with zero rows.
|
|
389
434
|
- Editor and Footer `modelLabel` values are independent and have separate controls in the **Editor** and **Footer** sections.
|
|
@@ -546,7 +591,7 @@ Pi 0.84 introduces a native fullscreen TUI with a sticky editor and Footer plus
|
|
|
546
591
|
}
|
|
547
592
|
```
|
|
548
593
|
|
|
549
|
-
You can also select fullscreen from Pi's `/settings` UI or start Pi with `--tui-mode fullscreen`. Zentui does not enable fullscreen automatically: Pi owns terminal layout, scrolling, and sticky placement, while Zentui supplies the configured editor and Footer components. Pi 0.80.
|
|
594
|
+
You can also select fullscreen from Pi's `/settings` UI or start Pi with `--tui-mode fullscreen`. Zentui does not enable fullscreen automatically: Pi owns terminal layout, scrolling, and sticky placement, while Zentui supplies the configured editor and Footer components. Pi 0.80.5–0.83 remain supported for Zentui styling, without native sticky placement.
|
|
550
595
|
|
|
551
596
|
## Acknowledgments
|
|
552
597
|
|
|
@@ -554,7 +599,7 @@ The minimalist frame's information hierarchy was inspired by [VinhLe1410/pi-cust
|
|
|
554
599
|
|
|
555
600
|
## Requirements
|
|
556
601
|
|
|
557
|
-
- [Pi](https://pi.dev) coding agent 0.80.
|
|
602
|
+
- [Pi](https://pi.dev) coding agent 0.80.5 or newer
|
|
558
603
|
- A [Nerd Font](https://www.nerdfonts.com/) for icons (or set `icons.mode` to `"ascii"`)
|
|
559
604
|
|
|
560
605
|
## Development
|
|
@@ -25,6 +25,8 @@ import {
|
|
|
25
25
|
resolveConfiguredIcons,
|
|
26
26
|
} from "./icons";
|
|
27
27
|
import { isSupportedColorSpec } from "./style";
|
|
28
|
+
import { normalizeWorkingLineMessages } from "./working-line";
|
|
29
|
+
import { PI_WORKING_LINE_MESSAGES } from "./working-line-messages";
|
|
28
30
|
|
|
29
31
|
export type ColorSpec = string;
|
|
30
32
|
export type ColorSource = "theme" | "terminal";
|
|
@@ -37,6 +39,13 @@ export type EditorStyle = "opencode" | "opencode-copy-friendly" | "minimalist";
|
|
|
37
39
|
export type UserMessageStyle = "framed" | "framed-copy-friendly" | "compact" | "labeled";
|
|
38
40
|
export type SelectorBorderStyle = "zentui";
|
|
39
41
|
export type FooterStyle = "native" | "starship" | "hidden";
|
|
42
|
+
export type WorkingLineSpinner =
|
|
43
|
+
| "braille"
|
|
44
|
+
| "star-bloom"
|
|
45
|
+
| "pinwheel"
|
|
46
|
+
| "claude-inspired"
|
|
47
|
+
| "pulse";
|
|
48
|
+
export type WorkingLineTextAnimation = "classic" | "kitt" | "disabled";
|
|
40
49
|
export type ComponentStyleOwner = "editor" | "userMessages" | "selectorBorders" | "footer";
|
|
41
50
|
export type MinimalistPathDisplayMode = "compact" | "project" | "full";
|
|
42
51
|
export type MinimalistContextFormat = "percent" | "percent-total";
|
|
@@ -185,9 +194,56 @@ export type FooterComponentConfig = {
|
|
|
185
194
|
};
|
|
186
195
|
};
|
|
187
196
|
|
|
197
|
+
export type WorkingLineMessagesConfig = {
|
|
198
|
+
custom: boolean;
|
|
199
|
+
values: string[];
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export type WorkingLineSegmentsConfig = {
|
|
203
|
+
tool: boolean;
|
|
204
|
+
elapsed: boolean;
|
|
205
|
+
thought: boolean;
|
|
206
|
+
tokens: boolean;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export const DEFAULT_WORKING_LINE_SPINNER_INTERVAL_MS = 100;
|
|
210
|
+
export const DEFAULT_WORKING_LINE_TEXT_INTERVAL_MS = 60;
|
|
211
|
+
export const MIN_WORKING_LINE_INTERVAL_MS = 30;
|
|
212
|
+
export const MAX_WORKING_LINE_INTERVAL_MS = 1000;
|
|
213
|
+
|
|
214
|
+
export function isValidWorkingLineIntervalMs(value: unknown): value is number {
|
|
215
|
+
return (
|
|
216
|
+
typeof value === "number" &&
|
|
217
|
+
Number.isSafeInteger(value) &&
|
|
218
|
+
value >= MIN_WORKING_LINE_INTERVAL_MS &&
|
|
219
|
+
value <= MAX_WORKING_LINE_INTERVAL_MS
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export type WorkingLineComponentConfig = {
|
|
224
|
+
enabled: boolean;
|
|
225
|
+
turnSummary: boolean;
|
|
226
|
+
spinner: WorkingLineSpinner;
|
|
227
|
+
spinnerIntervalMs: number;
|
|
228
|
+
animateSpinnerColor: boolean;
|
|
229
|
+
textIntervalMs: number;
|
|
230
|
+
textAnimation: WorkingLineTextAnimation;
|
|
231
|
+
colorSource: ColorSource;
|
|
232
|
+
messages: WorkingLineMessagesConfig;
|
|
233
|
+
segments: WorkingLineSegmentsConfig;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export type WorkingLineComponentPatch = Partial<
|
|
237
|
+
Omit<WorkingLineComponentConfig, "messages" | "segments">
|
|
238
|
+
> & {
|
|
239
|
+
messages?: Partial<WorkingLineMessagesConfig>;
|
|
240
|
+
segments?: Partial<WorkingLineSegmentsConfig>;
|
|
241
|
+
};
|
|
242
|
+
|
|
188
243
|
export type ComponentsConfig = {
|
|
189
244
|
editor: EditorComponentConfig;
|
|
190
245
|
userMessages: UserMessagesComponentConfig;
|
|
246
|
+
workingLine: WorkingLineComponentConfig;
|
|
191
247
|
selectorBorders: SelectorBordersComponentConfig;
|
|
192
248
|
footer: FooterComponentConfig;
|
|
193
249
|
};
|
|
@@ -266,6 +322,9 @@ export type PolishedTuiColors = {
|
|
|
266
322
|
editorThinkingMedium?: ColorSpec;
|
|
267
323
|
editorThinkingHigh?: ColorSpec;
|
|
268
324
|
editorThinkingXhigh?: ColorSpec;
|
|
325
|
+
workingLineLow?: ColorSpec;
|
|
326
|
+
workingLineMid?: ColorSpec;
|
|
327
|
+
workingLineHigh?: ColorSpec;
|
|
269
328
|
};
|
|
270
329
|
|
|
271
330
|
/**
|
|
@@ -413,6 +472,18 @@ const defaultComponents: ComponentsConfig = {
|
|
|
413
472
|
colorSource: "theme",
|
|
414
473
|
styles: { framed: {}, "framed-copy-friendly": {}, compact: {}, labeled: {} },
|
|
415
474
|
},
|
|
475
|
+
workingLine: {
|
|
476
|
+
enabled: false,
|
|
477
|
+
turnSummary: true,
|
|
478
|
+
spinner: "star-bloom",
|
|
479
|
+
spinnerIntervalMs: DEFAULT_WORKING_LINE_SPINNER_INTERVAL_MS,
|
|
480
|
+
animateSpinnerColor: false,
|
|
481
|
+
textIntervalMs: DEFAULT_WORKING_LINE_TEXT_INTERVAL_MS,
|
|
482
|
+
textAnimation: "classic",
|
|
483
|
+
colorSource: "theme",
|
|
484
|
+
messages: { custom: true, values: [...PI_WORKING_LINE_MESSAGES] },
|
|
485
|
+
segments: { tool: true, elapsed: true, thought: true, tokens: true },
|
|
486
|
+
},
|
|
416
487
|
selectorBorders: { enabled: true, style: "zentui", colorSource: "theme" },
|
|
417
488
|
footer: {
|
|
418
489
|
style: "starship",
|
|
@@ -646,6 +717,9 @@ function normalizeColors(record: Record<string, unknown>): Partial<PolishedTuiCo
|
|
|
646
717
|
editorThinkingMedium: colorValue(record, "editorThinkingMedium"),
|
|
647
718
|
editorThinkingHigh: colorValue(record, "editorThinkingHigh"),
|
|
648
719
|
editorThinkingXhigh: colorValue(record, "editorThinkingXhigh"),
|
|
720
|
+
workingLineLow: colorValue(record, "workingLineLow"),
|
|
721
|
+
workingLineMid: colorValue(record, "workingLineMid"),
|
|
722
|
+
workingLineHigh: colorValue(record, "workingLineHigh"),
|
|
649
723
|
});
|
|
650
724
|
}
|
|
651
725
|
|
|
@@ -1038,6 +1112,28 @@ function resolveUserMessagesSelection(
|
|
|
1038
1112
|
};
|
|
1039
1113
|
}
|
|
1040
1114
|
|
|
1115
|
+
function resolveWorkingLineMessages(messages: ConfigRecord): WorkingLineMessagesConfig {
|
|
1116
|
+
const preset = () => [...PI_WORKING_LINE_MESSAGES];
|
|
1117
|
+
const hasCanonicalCustom = hasOwn(messages, "custom");
|
|
1118
|
+
const custom = hasCanonicalCustom
|
|
1119
|
+
? parseBoolean(messages.custom, true)
|
|
1120
|
+
: messages.mode !== "native";
|
|
1121
|
+
if (hasCanonicalCustom) {
|
|
1122
|
+
return {
|
|
1123
|
+
custom,
|
|
1124
|
+
values: hasOwn(messages, "values") ? normalizeWorkingLineMessages(messages.values) : preset(),
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
const legacyValues = normalizeWorkingLineMessages(messages.values);
|
|
1128
|
+
if (messages.mode === "append") {
|
|
1129
|
+
return { custom, values: normalizeWorkingLineMessages([...preset(), ...legacyValues]) };
|
|
1130
|
+
}
|
|
1131
|
+
if (messages.mode === "replace" || messages.mode === "native") {
|
|
1132
|
+
return { custom, values: legacyValues.length > 0 ? legacyValues : preset() };
|
|
1133
|
+
}
|
|
1134
|
+
return { custom, values: hasOwn(messages, "values") ? legacyValues : preset() };
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1041
1137
|
function resolveComponents(config: ConfigRecord): ComponentsConfig {
|
|
1042
1138
|
const components = recordValue(config.components);
|
|
1043
1139
|
const editor = recordValue(components.editor);
|
|
@@ -1050,6 +1146,9 @@ function resolveComponents(config: ConfigRecord): ComponentsConfig {
|
|
|
1050
1146
|
const userMessages = recordValue(components.userMessages);
|
|
1051
1147
|
const userMessageStyles = recordValue(userMessages.styles);
|
|
1052
1148
|
const framed = recordValue(userMessageStyles.framed);
|
|
1149
|
+
const workingLine = recordValue(components.workingLine);
|
|
1150
|
+
const workingLineMessages = recordValue(workingLine.messages);
|
|
1151
|
+
const workingLineSegments = recordValue(workingLine.segments);
|
|
1053
1152
|
const selectorBorders = recordValue(components.selectorBorders);
|
|
1054
1153
|
const footer = recordValue(components.footer);
|
|
1055
1154
|
const footerStyles = recordValue(footer.styles);
|
|
@@ -1147,6 +1246,56 @@ function resolveComponents(config: ConfigRecord): ComponentsConfig {
|
|
|
1147
1246
|
labeled: {},
|
|
1148
1247
|
},
|
|
1149
1248
|
},
|
|
1249
|
+
workingLine: {
|
|
1250
|
+
enabled: parseBoolean(workingLine.enabled, defaultComponents.workingLine.enabled),
|
|
1251
|
+
turnSummary: parseBoolean(workingLine.turnSummary, defaultComponents.workingLine.turnSummary),
|
|
1252
|
+
spinner:
|
|
1253
|
+
workingLine.spinner === "braille" ||
|
|
1254
|
+
workingLine.spinner === "star-bloom" ||
|
|
1255
|
+
workingLine.spinner === "pinwheel" ||
|
|
1256
|
+
workingLine.spinner === "claude-inspired" ||
|
|
1257
|
+
workingLine.spinner === "pulse"
|
|
1258
|
+
? workingLine.spinner
|
|
1259
|
+
: defaultComponents.workingLine.spinner,
|
|
1260
|
+
spinnerIntervalMs: isValidWorkingLineIntervalMs(
|
|
1261
|
+
resolvedValue(workingLine, "spinnerIntervalMs", workingLine, "intervalMs"),
|
|
1262
|
+
)
|
|
1263
|
+
? (resolvedValue(workingLine, "spinnerIntervalMs", workingLine, "intervalMs") as number)
|
|
1264
|
+
: defaultComponents.workingLine.spinnerIntervalMs,
|
|
1265
|
+
animateSpinnerColor: parseBoolean(
|
|
1266
|
+
workingLine.animateSpinnerColor,
|
|
1267
|
+
defaultComponents.workingLine.animateSpinnerColor,
|
|
1268
|
+
),
|
|
1269
|
+
textIntervalMs: isValidWorkingLineIntervalMs(workingLine.textIntervalMs)
|
|
1270
|
+
? workingLine.textIntervalMs
|
|
1271
|
+
: defaultComponents.workingLine.textIntervalMs,
|
|
1272
|
+
textAnimation:
|
|
1273
|
+
workingLine.textAnimation === "classic" ||
|
|
1274
|
+
workingLine.textAnimation === "kitt" ||
|
|
1275
|
+
workingLine.textAnimation === "disabled"
|
|
1276
|
+
? workingLine.textAnimation
|
|
1277
|
+
: defaultComponents.workingLine.textAnimation,
|
|
1278
|
+
colorSource: parseColorSource(
|
|
1279
|
+
workingLine.colorSource,
|
|
1280
|
+
defaultComponents.workingLine.colorSource,
|
|
1281
|
+
),
|
|
1282
|
+
messages: resolveWorkingLineMessages(workingLineMessages),
|
|
1283
|
+
segments: {
|
|
1284
|
+
tool: parseBoolean(workingLineSegments.tool, defaultComponents.workingLine.segments.tool),
|
|
1285
|
+
elapsed: parseBoolean(
|
|
1286
|
+
workingLineSegments.elapsed,
|
|
1287
|
+
defaultComponents.workingLine.segments.elapsed,
|
|
1288
|
+
),
|
|
1289
|
+
thought: parseBoolean(
|
|
1290
|
+
workingLineSegments.thought,
|
|
1291
|
+
defaultComponents.workingLine.segments.thought,
|
|
1292
|
+
),
|
|
1293
|
+
tokens: parseBoolean(
|
|
1294
|
+
workingLineSegments.tokens,
|
|
1295
|
+
defaultComponents.workingLine.segments.tokens,
|
|
1296
|
+
),
|
|
1297
|
+
},
|
|
1298
|
+
},
|
|
1150
1299
|
selectorBorders: {
|
|
1151
1300
|
enabled: parseBoolean(
|
|
1152
1301
|
resolvedValue(selectorBorders, "enabled", features, "editor"),
|
|
@@ -1514,6 +1663,44 @@ export function saveUserMessagesComponentPatch(
|
|
|
1514
1663
|
);
|
|
1515
1664
|
}
|
|
1516
1665
|
|
|
1666
|
+
export function saveWorkingLineComponentPatch(
|
|
1667
|
+
patch: WorkingLineComponentPatch,
|
|
1668
|
+
path = configPath,
|
|
1669
|
+
): PolishedTuiConfig {
|
|
1670
|
+
return saveComponentsMutation(
|
|
1671
|
+
(components) => {
|
|
1672
|
+
const component = components.workingLine;
|
|
1673
|
+
if (patch.enabled !== undefined) component.enabled = patch.enabled;
|
|
1674
|
+
if (patch.turnSummary !== undefined) component.turnSummary = patch.turnSummary;
|
|
1675
|
+
if (patch.spinner !== undefined) component.spinner = patch.spinner;
|
|
1676
|
+
if (patch.spinnerIntervalMs !== undefined)
|
|
1677
|
+
component.spinnerIntervalMs = patch.spinnerIntervalMs;
|
|
1678
|
+
if (patch.animateSpinnerColor !== undefined)
|
|
1679
|
+
component.animateSpinnerColor = patch.animateSpinnerColor;
|
|
1680
|
+
if (patch.textIntervalMs !== undefined) component.textIntervalMs = patch.textIntervalMs;
|
|
1681
|
+
if (patch.textAnimation !== undefined) component.textAnimation = patch.textAnimation;
|
|
1682
|
+
if (patch.colorSource !== undefined) component.colorSource = patch.colorSource;
|
|
1683
|
+
if (patch.messages?.custom !== undefined) component.messages.custom = patch.messages.custom;
|
|
1684
|
+
if (patch.messages?.values !== undefined) {
|
|
1685
|
+
component.messages.values = normalizeWorkingLineMessages([...patch.messages.values]);
|
|
1686
|
+
}
|
|
1687
|
+
if (patch.segments?.tool !== undefined) component.segments.tool = patch.segments.tool;
|
|
1688
|
+
if (patch.segments?.elapsed !== undefined)
|
|
1689
|
+
component.segments.elapsed = patch.segments.elapsed;
|
|
1690
|
+
if (patch.segments?.thought !== undefined)
|
|
1691
|
+
component.segments.thought = patch.segments.thought;
|
|
1692
|
+
if (patch.segments?.tokens !== undefined) component.segments.tokens = patch.segments.tokens;
|
|
1693
|
+
},
|
|
1694
|
+
path,
|
|
1695
|
+
(record) => {
|
|
1696
|
+
const workingLine = recordValue(recordValue(record.components).workingLine);
|
|
1697
|
+
delete workingLine.intervalMs;
|
|
1698
|
+
const messages = recordValue(workingLine.messages);
|
|
1699
|
+
delete messages.mode;
|
|
1700
|
+
},
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1517
1704
|
export function saveSelectorBordersComponentPatch(
|
|
1518
1705
|
patch: Partial<SelectorBordersComponentConfig>,
|
|
1519
1706
|
path = configPath,
|
|
@@ -123,6 +123,17 @@ export function formatCount(value: number): string {
|
|
|
123
123
|
return `${Math.round(value / 1_000_000)}M`;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/** Human-readable whole-second duration used by settled and minimalist turn UI. */
|
|
127
|
+
export function formatElapsedDuration(durationMs: number): string {
|
|
128
|
+
const totalSeconds = Math.max(0, Math.floor(durationMs / 1000));
|
|
129
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
130
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
131
|
+
const seconds = totalSeconds % 60;
|
|
132
|
+
if (hours > 0) return `${hours}h ${minutes}m`;
|
|
133
|
+
if (minutes > 0) return `${minutes}m ${seconds}s`;
|
|
134
|
+
return `${seconds}s`;
|
|
135
|
+
}
|
|
136
|
+
|
|
126
137
|
export function formatProviderLabel(provider: string | undefined): string {
|
|
127
138
|
if (!provider) return "Unknown";
|
|
128
139
|
|