@underactive/pi-topping 0.7.1 → 0.7.2
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 +12 -5
- package/package.json +3 -3
- package/src/format.ts +10 -5
- package/src/nvidia-green.ts +37 -0
- package/src/preview.ts +9 -2
- package/src/prompt-decorator.ts +4 -2
- package/src/session.ts +23 -10
- package/src/settings.ts +10 -3
- package/src/toppings.ts +52 -38
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
We garnish our pies. It seemed rude not to extend Pi the same courtesy. This is a [Pi](https://github.com/earendil-works/pi-coding-agent) extension that adds some quality of life improvements to Pi's UX, while looking good in the process:
|
|
4
4
|
|
|
5
|
-
**User Prompt** — high-vis bordered prompt box with configurable border color and style, pi icon toggle, timestamp, and provider/model label. Prompts submitted while Pi is working (steer / `Alt+Enter` follow-up) are left undecorated so Pi's native `Steering:` / `Follow-up:` queue rows still render.
|
|
5
|
+
**User Prompt** — high-vis bordered prompt box with configurable border color and style, pi icon toggle, timestamp, and provider/model label. The box, timestamp, and provider/model label are display-only; Pi sends your prompt text unchanged to the model, so they do not affect LLM prompt caching. Prompts submitted while Pi is working (steer / `Alt+Enter` follow-up) are left undecorated so Pi's native `Steering:` / `Follow-up:` queue rows still render.
|
|
6
6
|
|
|
7
7
|

|
|
8
8
|
|
|
@@ -10,7 +10,7 @@ We garnish our pies. It seemed rude not to extend Pi the same courtesy. This is
|
|
|
10
10
|
|
|
11
11
|

|
|
12
12
|
|
|
13
|
-
**Completion Marker** — end-of-turn marker with icon, randomized verb (or `Worked`; matching past tense when Randomize "Worked" text is on), token consumption display, and optional marker-style, border-style, and border-color decorations spanning the full terminal width. The default `elite` style trails the summary with tapering dashes; `bookend` fills the remaining width to a closing corner glyph. Hooks into Pi's `agent_settled` event.
|
|
13
|
+
**Completion Marker** — end-of-turn marker with icon, randomized verb (or `Worked`; matching past tense when Randomize "Worked" text is on), token consumption display, and optional marker-style, border-style, and border-color decorations spanning the full terminal width. It can append the response model when it differs from the selected model, with its own color option. The default `elite` style trails the summary with tapering dashes; `bookend` fills the remaining width to a closing corner glyph. Hooks into Pi's `agent_settled` event.
|
|
14
14
|
|
|
15
15
|

|
|
16
16
|
|
|
@@ -22,6 +22,10 @@ pi install npm:@underactive/pi-topping
|
|
|
22
22
|
|
|
23
23
|
Restart Pi (or run `/reload`) to pick it up.
|
|
24
24
|
|
|
25
|
+
### Sibling toppings
|
|
26
|
+
|
|
27
|
+
The sibling-topping setup flow currently ships behind the `PI_TOPPING_SIBLING_SETUP=1` environment flag. When enabled, run `/topping-setup` in TUI mode to selectively install missing Pi Topping sibling extensions or uninstall sibling packages recorded in Pi's user settings. Installation rows are selected by default; removal rows are unchecked by default. Restart Pi after applying package changes. Use `/topping-setup disable-side-toppings-check` to suppress missing-topping notices, or `enable-side-toppings-check` to restore them.
|
|
28
|
+
|
|
25
29
|
## Settings
|
|
26
30
|
|
|
27
31
|
Run `/topping-settings` (TUI only) to customize your toppings. Settings persist to
|
|
@@ -61,7 +65,7 @@ Run `/topping-settings` (TUI only) to customize your toppings. Settings persist
|
|
|
61
65
|
║ [■] Token rate dimmed OFF ║
|
|
62
66
|
║ [■] Response model ON ║
|
|
63
67
|
║ [■] Response model color ‹ accent › ║
|
|
64
|
-
║ [■] Response model dimmed
|
|
68
|
+
║ [■] Response model dimmed OFF ║
|
|
65
69
|
║ ║
|
|
66
70
|
╟─ Elements Order ──────────────────────────────────────────────╢
|
|
67
71
|
║ [ ] Animated spinner ║
|
|
@@ -81,6 +85,9 @@ Run `/topping-settings` (TUI only) to customize your toppings. Settings persist
|
|
|
81
85
|
║ [■] Randomize “Worked” text ON ║
|
|
82
86
|
║ [■] Tokens spent ON ║
|
|
83
87
|
║ [■] Mid-turn inputs ON ║
|
|
88
|
+
║ [■] Response model ON ║
|
|
89
|
+
║ [■] Response model color ‹ muted › ║
|
|
90
|
+
║ [■] Response model dimmed OFF ║
|
|
84
91
|
║ ║
|
|
85
92
|
╟─ Word Packs ──────────────────────────────────────────────────╢
|
|
86
93
|
║ [ ] Doctor Who OFF ║
|
|
@@ -98,7 +105,7 @@ Run `/topping-settings` (TUI only) to customize your toppings. Settings persist
|
|
|
98
105
|
╟───────────────────────────────────────────────────────────────╢
|
|
99
106
|
║ ↑↓ move PgUp/PgDn page ←→ select ║
|
|
100
107
|
║ ␣ toggle ⏎ apply esc cancel ║
|
|
101
|
-
╚═══════════════════════════════════════════════════════[ 9/
|
|
108
|
+
╚═══════════════════════════════════════════════════════[ 9/54 ]╝
|
|
102
109
|
```
|
|
103
110
|
|
|
104
111
|
### Word packs
|
|
@@ -134,7 +141,7 @@ full brightness for 1.5 seconds, then fades through five theme-aware shades to t
|
|
|
134
141
|
the next 0.25 seconds before returning to the placeholder; a new count restores full brightness and
|
|
135
142
|
restarts the cycle.
|
|
136
143
|
|
|
137
|
-
The response model is captured from assistant responses, displayed as its sanitized value with no label only when it meaningfully differs from the selected model, and defaults to the final loader detail after output tokens. Equivalent local paths and decorated filenames are suppressed. It can be hidden, recolored, permanently dimmed, or reordered. After the agent settles, its final value remains visible for 3 seconds, fades through five theme-aware shades over 0.5 seconds, then clears; new work cancels the pending fade.
|
|
144
|
+
The response model is captured from assistant responses, displayed as its sanitized value with no label only when it meaningfully differs from the selected model, and defaults to the final loader detail after output tokens. Equivalent local paths and decorated filenames are suppressed. It can be hidden, recolored, permanently dimmed, or reordered. When the active provider is `switchyard`, the loader and its post-settlement hold/fade use NVIDIA green (`#84c51a`, matching pi-topping-statusline) regardless of the configured Response model color; the dim setting still applies. After the agent settles, its final value remains visible for 3 seconds, fades through five theme-aware shades over 0.5 seconds, then clears; new work cancels the pending fade.
|
|
138
145
|
|
|
139
146
|
## Requirements
|
|
140
147
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@underactive/pi-topping",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "A Pi extension that adds quality of life improvements to Pi's UX: a high-vis user prompt, a shimmering working loader with token activity meter and elapsed timer, and an end-of-turn completion marker",
|
|
5
5
|
"keywords": ["pi-package", "pi-extension"],
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"@earendil-works/pi-tui": "*"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
34
|
-
"@earendil-works/pi-tui": "0.
|
|
33
|
+
"@earendil-works/pi-coding-agent": "0.86.0",
|
|
34
|
+
"@earendil-works/pi-tui": "0.86.0",
|
|
35
35
|
"@types/node": "^24.0.0",
|
|
36
36
|
"typescript": "^5.9.3"
|
|
37
37
|
},
|
package/src/format.ts
CHANGED
|
@@ -44,8 +44,13 @@ export type LoaderElement = "spinner" | "text" | "meter" | "elapsed" | "tokens"
|
|
|
44
44
|
/** Default first-use working-indicator element order. */
|
|
45
45
|
export const DEFAULT_LOADER_ORDER: readonly LoaderElement[] = ["spinner", "text", "meter", "tokenRate", "elapsed", "tokens", "responseModel"];
|
|
46
46
|
|
|
47
|
-
/** Elements share a detail group;
|
|
48
|
-
const
|
|
47
|
+
/** Elements share a detail group; "raw" details arrive pre-colored and must not be dimmed. */
|
|
48
|
+
const DETAIL_STYLES = new Map<LoaderElement, "muted" | "raw">([
|
|
49
|
+
["elapsed", "muted"],
|
|
50
|
+
["tokens", "muted"],
|
|
51
|
+
["tokenRate", "raw"],
|
|
52
|
+
["responseModel", "raw"],
|
|
53
|
+
]);
|
|
49
54
|
|
|
50
55
|
const TOKEN_UNITS = [
|
|
51
56
|
{ threshold: 10_000, divisor: 1_000, decimals: 1, suffix: "k" },
|
|
@@ -169,9 +174,9 @@ export function buildWorkingMessage(
|
|
|
169
174
|
for (const element of order) {
|
|
170
175
|
const value = parts[element];
|
|
171
176
|
if (!value) continue;
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
details.push(
|
|
177
|
+
const style = DETAIL_STYLES.get(element);
|
|
178
|
+
if (style) {
|
|
179
|
+
details.push(style === "raw" ? value : theme.fg("muted", value));
|
|
175
180
|
continue;
|
|
176
181
|
}
|
|
177
182
|
flushDetails();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { getThinkingLevelColorizer, type ThinkingLevel } from "./format.ts";
|
|
3
|
+
import type { ThinkingLevelColor, ThinkingLevelSettingColor } from "./settings.ts";
|
|
4
|
+
|
|
5
|
+
/** Provider id whose working-loader response model uses NVIDIA green. */
|
|
6
|
+
export const SWITCHYARD_PROVIDER = "switchyard";
|
|
7
|
+
|
|
8
|
+
/** Bright NVIDIA green shared with pi-topping-statusline's Switchyard border. */
|
|
9
|
+
export const NVIDIA_GREEN_HEX = "#84c51a";
|
|
10
|
+
|
|
11
|
+
const NVIDIA_GREEN_RGB = [1, 3, 5].map((offset) => Number.parseInt(NVIDIA_GREEN_HEX.slice(offset, offset + 2), 16));
|
|
12
|
+
const NVIDIA_GREEN_ANSI = `\x1b[38;2;${NVIDIA_GREEN_RGB.join(";")}m`;
|
|
13
|
+
|
|
14
|
+
export function isSwitchyardProvider(provider: string | undefined): boolean {
|
|
15
|
+
return provider?.trim().toLowerCase() === SWITCHYARD_PROVIDER;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function colorizeNvidiaGreen(text: string): string {
|
|
19
|
+
return `${NVIDIA_GREEN_ANSI}${text}\x1b[39m`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build the working-loader response-model colorizer.
|
|
24
|
+
*
|
|
25
|
+
* Switchyard always uses NVIDIA green; other providers retain the configured
|
|
26
|
+
* theme/thinking-level color.
|
|
27
|
+
*/
|
|
28
|
+
export function getResponseModelColorizer(
|
|
29
|
+
theme: Pick<Theme, "fg" | "getThinkingBorderColor">,
|
|
30
|
+
color: ThinkingLevelColor | ThinkingLevelSettingColor,
|
|
31
|
+
thinkingLevel: ThinkingLevel | undefined,
|
|
32
|
+
provider: string | undefined,
|
|
33
|
+
): (text: string) => string {
|
|
34
|
+
return isSwitchyardProvider(provider)
|
|
35
|
+
? colorizeNvidiaGreen
|
|
36
|
+
: getThinkingLevelColorizer(theme, color, thinkingLevel);
|
|
37
|
+
}
|
package/src/preview.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { DEFAULT_PREVIEW_WIDTH, type PreviewResult } from "./menu.ts";
|
|
3
3
|
import { ActivityMeter, rateToLevel } from "./activity-meter.ts";
|
|
4
4
|
import { buildWorkingMessage, DEFAULT_WORKING_WORD, dimAttribute, ELAPSED_INTERVAL_MS, formatElapsed, formatTokenRate, formatTokens, getThinkingLevelColorizer, isFullyDefaultAppearance, METER_INTERVAL_MS, SHIMMER_INTERVAL_MS, shimmerString, SPINNER_FRAME_MS, SPINNER_FRAMES } from "./format.ts";
|
|
5
|
+
import { getResponseModelColorizer } from "./nvidia-green.ts";
|
|
5
6
|
import { buildCompletionMarkerContent, buildCompletionMarkerLine, buildPromptBoxLines } from "./prompt-decorator.ts";
|
|
6
7
|
import { DEFAULT_SETTINGS, fromCycleDirection, fromCycleSpeed, isBorderStyle, isDoneMarkerBorderColor, isDoneMarkerBorderStyle, isDoneMarkerStyle, isPromptBorderColor, isSpinnerColor, isThinkingLevelColor, LOADER_ORDER_ID, MENU_ENTRIES, parseLoaderOrder } from "./settings.ts";
|
|
7
8
|
import { isWordPackEnabled, selectWorkingTextSelection, wordPacksPath, type WordPack } from "./word-packs.ts";
|
|
@@ -62,7 +63,7 @@ export class PreviewRenderer {
|
|
|
62
63
|
const order = parseLoaderOrder(values[LOADER_ORDER_ID]);
|
|
63
64
|
let responseModelColor = DEFAULT_SETTINGS.decorations.responseModelColor;
|
|
64
65
|
if (isThinkingLevelColor(values.responseModelColor)) responseModelColor = values.responseModelColor;
|
|
65
|
-
const responseModelColorizer =
|
|
66
|
+
const responseModelColorizer = getResponseModelColorizer(this.#ctx.ui.theme, responseModelColor, this.#ctx.thinkingLevel, this.#ctx.model?.provider);
|
|
66
67
|
const responseModelColored = features.responseModel ? responseModelColorizer("test-model") : "";
|
|
67
68
|
const responseModel = responseModelColored && values.responseModelDimmed === true ? dimAttribute(responseModelColored) : responseModelColored;
|
|
68
69
|
if (isFullyDefaultAppearance(features, decorations)) {
|
|
@@ -173,7 +174,13 @@ export class PreviewRenderer {
|
|
|
173
174
|
const details: string[] = [];
|
|
174
175
|
if (values.doneMarkerTokens === true) details.push("↓ 949 tokens");
|
|
175
176
|
if (values.doneMarkerInputs === true) details.push("2 mid-turn inputs");
|
|
176
|
-
|
|
177
|
+
let model = "";
|
|
178
|
+
if (values.doneMarkerModel === true) {
|
|
179
|
+
const color = isThinkingLevelColor(values.doneMarkerModelColor) ? values.doneMarkerModelColor : DEFAULT_SETTINGS.decorations.doneMarkerModelColor;
|
|
180
|
+
const modelColored = getThinkingLevelColorizer(theme, color, this.#ctx.thinkingLevel)("test-model");
|
|
181
|
+
model = values.doneMarkerModelDimmed === true ? dimAttribute(modelColored) : modelColored;
|
|
182
|
+
}
|
|
183
|
+
const content = buildCompletionMarkerContent(theme, icon, word, "2m 52s", details, model);
|
|
177
184
|
const borderStyle = isDoneMarkerBorderStyle(values.doneMarkerBorderStyle) ? values.doneMarkerBorderStyle : "none";
|
|
178
185
|
const borderColor = isDoneMarkerBorderColor(values.doneMarkerBorderColor) ? values.doneMarkerBorderColor : DEFAULT_SETTINGS.decorations.doneMarkerBorderColor;
|
|
179
186
|
const markerStyle = isDoneMarkerStyle(values.doneMarkerStyle) ? values.doneMarkerStyle : DEFAULT_SETTINGS.decorations.doneMarkerStyle;
|
package/src/prompt-decorator.ts
CHANGED
|
@@ -41,16 +41,18 @@ type PromptTheme = {
|
|
|
41
41
|
|
|
42
42
|
const COMPLETION_MARKER_TRAIL_RUNS = [6, 4, 2, 1] as const;
|
|
43
43
|
|
|
44
|
-
/** Assemble the completion-marker content: an optional icon, then the dim `word for elapsed (details)` summary. */
|
|
44
|
+
/** Assemble the completion-marker content: an optional icon, then the dim `word for elapsed (details)` summary, then an optional pre-styled model suffix. */
|
|
45
45
|
export function buildCompletionMarkerContent(
|
|
46
46
|
theme: PromptTheme,
|
|
47
47
|
icon: string,
|
|
48
48
|
word: string,
|
|
49
49
|
elapsed: string,
|
|
50
50
|
details: string[],
|
|
51
|
+
model = "",
|
|
51
52
|
): string {
|
|
52
53
|
const tail = details.length ? ` (${details.join(" · ")})` : "";
|
|
53
|
-
|
|
54
|
+
const summary = theme.fg("dim", `${icon ? " " : ""}${word} for ${elapsed}${tail}`);
|
|
55
|
+
return `${icon}${summary}${model ? `${theme.fg("dim", " · ")}${model}` : ""}`;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
/** Build one decorated completion-marker line, clipped to the available width. */
|
package/src/session.ts
CHANGED
|
@@ -40,12 +40,13 @@ import {
|
|
|
40
40
|
} from "./format.ts";
|
|
41
41
|
import { isSiblingSetupEnabled } from "./flags.ts";
|
|
42
42
|
import { showMenu } from "./menu.ts";
|
|
43
|
+
import { getResponseModelColorizer } from "./nvidia-green.ts";
|
|
43
44
|
import { registerSetupCommand } from "./setup-command.ts";
|
|
44
45
|
import { applyMenuResult, buildMenuSections, loadSettings, saveSettings, type ThinkingLevelColor } from "./settings.ts";
|
|
45
|
-
import {
|
|
46
|
+
import { announceMissingToppingsOnce, type MissingToppingsEntryData, renderMissingToppingsEntry, SETUP_ENTRY_TYPE } from "./toppings.ts";
|
|
46
47
|
import { PreviewRenderer } from "./preview.ts";
|
|
47
48
|
import { buildCompletionMarkerContent, buildCompletionMarkerLine, PROMPT_BOX_TYPE, promptBoxRenderer, type PromptBoxDetails } from "./prompt-decorator.ts";
|
|
48
|
-
import { modelsResemble, stripControlChars } from "./util.ts";
|
|
49
|
+
import { isPlainObject, modelsResemble, stripControlChars } from "./util.ts";
|
|
49
50
|
import { loadBundledWordPacks, loadUserWordPacks, pickWorkingTextSelection, type WorkingTextSelection, type WordPack } from "./word-packs.ts";
|
|
50
51
|
|
|
51
52
|
type MessageStartEvent = Extract<ExtensionEvent, { type: "message_start" }>;
|
|
@@ -77,6 +78,7 @@ interface DoneEntryData {
|
|
|
77
78
|
tokens?: number;
|
|
78
79
|
midTurnInputs?: number;
|
|
79
80
|
thinkingLevel?: ThinkingLevel;
|
|
81
|
+
model?: string;
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
interface SessionState {
|
|
@@ -148,6 +150,10 @@ function waitingLabel(waiting: NonNullable<SessionState["waiting"]>): string {
|
|
|
148
150
|
return title ? `Waiting: ${title}` : WAITING_LABELS[waiting.kind];
|
|
149
151
|
}
|
|
150
152
|
|
|
153
|
+
function responseModelOf(message: unknown): unknown {
|
|
154
|
+
return isPlainObject(message) ? message.responseModel : undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
151
157
|
/** Owns mutable extension state and Pi lifecycle registrations. */
|
|
152
158
|
export class SessionManager {
|
|
153
159
|
#counter = new StreamingWordCounter();
|
|
@@ -175,7 +181,7 @@ export class SessionManager {
|
|
|
175
181
|
this.#state.activityMeter.setDirection(this.#settings.decorations.meterDirection);
|
|
176
182
|
if (this.usable(ctx)) {
|
|
177
183
|
this.applyIndicator(ctx);
|
|
178
|
-
if (isSiblingSetupEnabled())
|
|
184
|
+
if (isSiblingSetupEnabled()) announceMissingToppingsOnce(this.#pi);
|
|
179
185
|
}
|
|
180
186
|
};
|
|
181
187
|
|
|
@@ -232,7 +238,7 @@ export class SessionManager {
|
|
|
232
238
|
if (this.usable(ctx) && event.message.role === "assistant") {
|
|
233
239
|
this.#state.liveTokens = 0;
|
|
234
240
|
this.#counter.reset();
|
|
235
|
-
this.updateResponseModel((event.message
|
|
241
|
+
this.updateResponseModel(responseModelOf(event.message));
|
|
236
242
|
}
|
|
237
243
|
};
|
|
238
244
|
|
|
@@ -247,7 +253,7 @@ export class SessionManager {
|
|
|
247
253
|
this.#state.liveTokens += this.#counter.count(assistantEvent.delta, assistantEvent.type);
|
|
248
254
|
}
|
|
249
255
|
const rawResponseModel = (assistantEvent as { partial?: { responseModel?: unknown } } | undefined)?.partial?.responseModel
|
|
250
|
-
?? (event.message
|
|
256
|
+
?? responseModelOf(event.message);
|
|
251
257
|
this.updateResponseModel(rawResponseModel);
|
|
252
258
|
};
|
|
253
259
|
|
|
@@ -268,7 +274,7 @@ export class SessionManager {
|
|
|
268
274
|
}
|
|
269
275
|
this.#state.liveTokens = 0;
|
|
270
276
|
this.#counter.reset();
|
|
271
|
-
this.updateResponseModel((event.message
|
|
277
|
+
this.updateResponseModel(responseModelOf(event.message));
|
|
272
278
|
};
|
|
273
279
|
|
|
274
280
|
#onToolExecutionStart = async (_e: ToolExecutionStartEvent, ctx: ExtensionContext): Promise<void> => {
|
|
@@ -324,6 +330,7 @@ export class SessionManager {
|
|
|
324
330
|
tokens: this.#state.confirmTokens,
|
|
325
331
|
midTurnInputs: this.#state.midTurnInputs,
|
|
326
332
|
thinkingLevel: ctx.thinkingLevel,
|
|
333
|
+
model: this.#state.responseModel || undefined,
|
|
327
334
|
});
|
|
328
335
|
}
|
|
329
336
|
this.#currentCtx = null;
|
|
@@ -346,11 +353,16 @@ export class SessionManager {
|
|
|
346
353
|
details.push(`${entry.data.midTurnInputs} mid-turn input${entry.data.midTurnInputs === 1 ? "" : "s"}`);
|
|
347
354
|
}
|
|
348
355
|
const icon = features.doneMarkerIcon ? theme.fg("text", this.#settings.decorations.useNerdFont ? "" : "π") : "";
|
|
349
|
-
const
|
|
356
|
+
const thinkingLevel = isThinkingLevel(entry.data.thinkingLevel) ? entry.data.thinkingLevel : undefined;
|
|
357
|
+
const rawModel = typeof entry.data.model === "string" ? stripControlChars(entry.data.model).trim() : "";
|
|
358
|
+
const modelColored = features.doneMarkerModel && rawModel
|
|
359
|
+
? getThinkingLevelColorizer(theme, this.#settings.decorations.doneMarkerModelColor, thinkingLevel)(rawModel)
|
|
360
|
+
: "";
|
|
361
|
+
const model = modelColored && this.#settings.decorations.doneMarkerModelDimmed ? dimAttribute(modelColored) : modelColored;
|
|
362
|
+
const markerContent = buildCompletionMarkerContent(theme, icon, word, formatElapsed(elapsedMs), details, model);
|
|
350
363
|
const borderStyle = this.#settings.decorations.doneMarkerBorderStyle;
|
|
351
364
|
const borderColor = this.#settings.decorations.doneMarkerBorderColor;
|
|
352
365
|
const markerStyle = this.#settings.decorations.doneMarkerStyle;
|
|
353
|
-
const thinkingLevel = isThinkingLevel(entry.data.thinkingLevel) ? entry.data.thinkingLevel : undefined;
|
|
354
366
|
if (borderStyle === "none") return new Text(markerContent);
|
|
355
367
|
|
|
356
368
|
let cachedWidth: number | undefined;
|
|
@@ -383,6 +395,7 @@ export class SessionManager {
|
|
|
383
395
|
this.#pi.on("ui_prompt_end", this.#onUIPromptEnd);
|
|
384
396
|
this.#pi.on("session_shutdown", this.#onSessionShutdown);
|
|
385
397
|
this.#pi.registerEntryRenderer<DoneEntryData>(DONE_ENTRY_TYPE, (entry, _o, theme) => this.#renderDoneEntry(entry, theme));
|
|
398
|
+
if (isSiblingSetupEnabled()) this.#pi.registerEntryRenderer<MissingToppingsEntryData>(SETUP_ENTRY_TYPE, (entry, _o, theme) => renderMissingToppingsEntry(entry, theme));
|
|
386
399
|
this.#pi.registerMessageRenderer<PromptBoxDetails>(PROMPT_BOX_TYPE, promptBoxRenderer);
|
|
387
400
|
this.#pi.registerCommand("topping-settings", {
|
|
388
401
|
description: "Configure prompt decoration, working-loader features/order, and completion-marker settings.",
|
|
@@ -487,7 +500,7 @@ export class SessionManager {
|
|
|
487
500
|
private startResponseModelFade(ctx: ExtensionContext, model: string, color: ThinkingLevelColor, dimmed: boolean): void {
|
|
488
501
|
this.cancelResponseModelFade(ctx);
|
|
489
502
|
const generation = this.#state.responseModelFadeGeneration;
|
|
490
|
-
const colorizer =
|
|
503
|
+
const colorizer = getResponseModelColorizer(ctx.ui.theme, color, ctx.thinkingLevel, ctx.model?.provider);
|
|
491
504
|
const render = (shade?: number): void => {
|
|
492
505
|
const colored = shade === undefined
|
|
493
506
|
? colorizer(model)
|
|
@@ -581,7 +594,7 @@ export class SessionManager {
|
|
|
581
594
|
? getThinkingLevelColorizer(ctx.ui.theme, decorations.spinnerColor, ctx.thinkingLevel)(SPINNER_FRAMES[Math.floor(now / SPINNER_FRAME_MS) % SPINNER_FRAMES.length]!)
|
|
582
595
|
: "";
|
|
583
596
|
const responseModelColored = features.responseModel && state.responseModel
|
|
584
|
-
?
|
|
597
|
+
? getResponseModelColorizer(ctx.ui.theme, decorations.responseModelColor, ctx.thinkingLevel, ctx.model?.provider)(state.responseModel)
|
|
585
598
|
: "";
|
|
586
599
|
const responseModel = responseModelColored && decorations.responseModelDimmed ? dimAttribute(responseModelColored) : responseModelColored;
|
|
587
600
|
if (isFullyDefaultAppearance(features, decorations)) {
|
package/src/settings.ts
CHANGED
|
@@ -73,6 +73,8 @@ export interface DecoratorSettings {
|
|
|
73
73
|
doneMarkerBorderStyle: DoneMarkerBorderStyle;
|
|
74
74
|
doneMarkerBorderColor: DoneMarkerBorderColor;
|
|
75
75
|
doneMarkerStyle: DoneMarkerStyle;
|
|
76
|
+
doneMarkerModelColor: ThinkingLevelColor;
|
|
77
|
+
doneMarkerModelDimmed: boolean;
|
|
76
78
|
spinnerColor: SpinnerColor;
|
|
77
79
|
spinnerColorEnabled: boolean;
|
|
78
80
|
meterColor: ThinkingLevelColor;
|
|
@@ -99,14 +101,15 @@ export interface DecoratorSettings {
|
|
|
99
101
|
randomizeDoneMarker: boolean;
|
|
100
102
|
doneMarkerTokens: boolean;
|
|
101
103
|
doneMarkerInputs: boolean;
|
|
104
|
+
doneMarkerModel: boolean;
|
|
102
105
|
};
|
|
103
106
|
loaderOrder: LoaderElement[];
|
|
104
107
|
wordPacks: Record<string, boolean>;
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
export const DEFAULT_SETTINGS: DecoratorSettings = {
|
|
108
|
-
decorations: { animatedSpinner: true, shimmer: true, shimmerInverted: false, shimmerDirection: "ltr", shimmerDirectionEnabled: true, shimmerSpeed: "normal", shimmerSpeedEnabled: true, tokenActivityMonitor: true, meterDirection: "rtl", meterDirectionEnabled: true, decorateUserPrompt: true, borderColor: "thinking-level", borderColorEnabled: true, borderStyle: "double", borderStyleEnabled: true, doneMarkerBorderStyle: "none", doneMarkerBorderColor: "thinking-level", doneMarkerStyle: "elite", spinnerColor: "thinking-level", spinnerColorEnabled: true, meterColor: "accent", meterColorEnabled: true, meterDimmed: false, tokenRateColor: "warning", tokenRateDimmed: false, responseModelColor: "accent", responseModelDimmed: false, promptIcon: true, promptTimestamp: true, promptProvider: true, promptModel: true, useNerdFont: true },
|
|
109
|
-
features: { substituteDefaultMessage: true, elapsedTime: true, outputTokens: true, tokenRate: true, responseModel: true, doneMarker: true, doneMarkerIcon: true, randomizeDoneMarker: true, doneMarkerTokens: true, doneMarkerInputs: true },
|
|
111
|
+
decorations: { animatedSpinner: true, shimmer: true, shimmerInverted: false, shimmerDirection: "ltr", shimmerDirectionEnabled: true, shimmerSpeed: "normal", shimmerSpeedEnabled: true, tokenActivityMonitor: true, meterDirection: "rtl", meterDirectionEnabled: true, decorateUserPrompt: true, borderColor: "thinking-level", borderColorEnabled: true, borderStyle: "double", borderStyleEnabled: true, doneMarkerBorderStyle: "none", doneMarkerBorderColor: "thinking-level", doneMarkerStyle: "elite", doneMarkerModelColor: "muted", doneMarkerModelDimmed: false, spinnerColor: "thinking-level", spinnerColorEnabled: true, meterColor: "accent", meterColorEnabled: true, meterDimmed: false, tokenRateColor: "warning", tokenRateDimmed: false, responseModelColor: "accent", responseModelDimmed: false, promptIcon: true, promptTimestamp: true, promptProvider: true, promptModel: true, useNerdFont: true },
|
|
112
|
+
features: { substituteDefaultMessage: true, elapsedTime: true, outputTokens: true, tokenRate: true, responseModel: true, doneMarker: true, doneMarkerIcon: true, randomizeDoneMarker: true, doneMarkerTokens: true, doneMarkerInputs: true, doneMarkerModel: true },
|
|
110
113
|
loaderOrder: [...DEFAULT_LOADER_ORDER],
|
|
111
114
|
wordPacks: {},
|
|
112
115
|
};
|
|
@@ -154,7 +157,7 @@ function mergeGroup<T extends Record<string, boolean | string>>(defaults: T, par
|
|
|
154
157
|
else if (key === "borderColor" && isPromptBorderColor(value)) valid = value;
|
|
155
158
|
else if (key === "doneMarkerBorderColor" && value === "default") valid = "thinking-level";
|
|
156
159
|
else if (key === "doneMarkerBorderColor" && isDoneMarkerBorderColor(value)) valid = value;
|
|
157
|
-
else if ((key === "meterColor" || key === "tokenRateColor" || key === "responseModelColor") && isThinkingLevelColor(value)) valid = value;
|
|
160
|
+
else if ((key === "meterColor" || key === "tokenRateColor" || key === "responseModelColor" || key === "doneMarkerModelColor") && isThinkingLevelColor(value)) valid = value;
|
|
158
161
|
else if (key === "borderStyle" && isBorderStyle(value)) valid = value;
|
|
159
162
|
else if (key === "doneMarkerBorderStyle" && isDoneMarkerBorderStyle(value)) valid = value;
|
|
160
163
|
else if (key === "doneMarkerStyle" && isDoneMarkerStyle(value)) valid = value;
|
|
@@ -221,6 +224,9 @@ export const MENU_ENTRIES: readonly MenuEntry[] = [
|
|
|
221
224
|
{ id: "randomizeDoneMarker", label: "Randomize “Worked” text", section: "Completion Marker", group: "features", key: "randomizeDoneMarker" },
|
|
222
225
|
{ id: "doneMarkerTokens", label: "Tokens spent", section: "Completion Marker", group: "features", key: "doneMarkerTokens" },
|
|
223
226
|
{ id: "doneMarkerInputs", label: "Mid-turn inputs", section: "Completion Marker", group: "features", key: "doneMarkerInputs" },
|
|
227
|
+
{ id: "doneMarkerModel", label: "Response model", section: "Completion Marker", group: "features", key: "doneMarkerModel" },
|
|
228
|
+
{ id: "doneMarkerModelColor", label: "Response model color", section: "Completion Marker", group: "decorations", key: "doneMarkerModelColor", cycleValues: THINKING_LEVEL_COLOR_VALUES, cycleValueLabels: THINKING_LEVEL_CYCLE_LABELS },
|
|
229
|
+
{ id: "doneMarkerModelDimmed", label: "Response model dimmed", section: "Completion Marker", group: "decorations", key: "doneMarkerModelDimmed" },
|
|
224
230
|
{ id: "useNerdFont", label: "Use NerdFont icons", section: "Options", group: "decorations", key: "useNerdFont" },
|
|
225
231
|
];
|
|
226
232
|
|
|
@@ -247,6 +253,7 @@ function setDecorationCycleValue(decorations: DecorationSettings, key: keyof Dec
|
|
|
247
253
|
case "meterColor":
|
|
248
254
|
case "tokenRateColor":
|
|
249
255
|
case "responseModelColor":
|
|
256
|
+
case "doneMarkerModelColor":
|
|
250
257
|
if (isThinkingLevelColor(value)) decorations[key] = value;
|
|
251
258
|
return;
|
|
252
259
|
case "doneMarkerBorderColor":
|
package/src/toppings.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getAgentDir, type
|
|
2
|
-
import {
|
|
1
|
+
import { getAgentDir, type CustomEntry, type ExtensionAPI, type Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth, type Component } from "@earendil-works/pi-tui";
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { isSetupCheckDisabled } from "./setup-check.ts";
|
|
@@ -65,6 +65,12 @@ interface SettingsToppingState {
|
|
|
65
65
|
extensionActive: boolean;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
export const SETUP_ENTRY_TYPE = "pi-topping-setup";
|
|
69
|
+
|
|
70
|
+
export interface MissingToppingsEntryData {
|
|
71
|
+
readonly toppings: ReadonlyArray<Pick<Topping, "pkg" | "provides">>;
|
|
72
|
+
}
|
|
73
|
+
|
|
68
74
|
let setupNoticeHandled = false;
|
|
69
75
|
|
|
70
76
|
function stringEntries(value: unknown): string[] {
|
|
@@ -153,52 +159,60 @@ export function findMissingToppings(pi: ExtensionAPI): ToppingStatus[] {
|
|
|
153
159
|
return detectToppings(pi).filter((status) => !status.active);
|
|
154
160
|
}
|
|
155
161
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
+
function renderMissingToppings(
|
|
163
|
+
toppings: MissingToppingsEntryData["toppings"],
|
|
164
|
+
theme: Pick<Theme, "fg">,
|
|
165
|
+
width: number,
|
|
166
|
+
): string[] {
|
|
167
|
+
const safeWidth = Math.max(0, width);
|
|
162
168
|
const text = (value: string): string => theme.fg("text", value);
|
|
163
169
|
const accent = (value: string): string => theme.fg("accent", value);
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
...
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
})),
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
170
|
+
const fit = (line: string): string => truncateToWidth(line, safeWidth, "");
|
|
171
|
+
const rule = theme.fg("warning", "─".repeat(safeWidth));
|
|
172
|
+
return [
|
|
173
|
+
rule,
|
|
174
|
+
fit(text(`pi-topping: ${toppings.length} topping extension${toppings.length === 1 ? "" : "s"} missing`)),
|
|
175
|
+
"",
|
|
176
|
+
...toppings.map(({ pkg, provides }) => fit(`${text("• ")}${accent(pkg)}${text(` — ${provides}`)}`)),
|
|
177
|
+
"",
|
|
178
|
+
fit(`${text("Run ")}${accent("/topping-setup")}${text(" to selectively install them")}`),
|
|
179
|
+
fit(`${text("Run ")}${accent("/topping-setup disable-side-toppings-check")}${text(" to suppress this message")}`),
|
|
180
|
+
rule,
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function renderMissingToppingsEntry(
|
|
185
|
+
entry: CustomEntry<MissingToppingsEntryData>,
|
|
186
|
+
theme: Theme,
|
|
187
|
+
): Component | undefined {
|
|
188
|
+
if (!entry.data || !Array.isArray(entry.data.toppings)) return undefined;
|
|
189
|
+
let cachedWidth: number | undefined;
|
|
190
|
+
let cachedLines: string[] | undefined;
|
|
191
|
+
return {
|
|
192
|
+
render(width: number): string[] {
|
|
193
|
+
if (cachedWidth !== width) {
|
|
194
|
+
cachedWidth = width;
|
|
195
|
+
cachedLines = renderMissingToppings(entry.data!.toppings, theme, width);
|
|
196
|
+
}
|
|
197
|
+
return cachedLines!;
|
|
178
198
|
},
|
|
179
|
-
{
|
|
180
|
-
|
|
181
|
-
|
|
199
|
+
invalidate(): void {
|
|
200
|
+
cachedWidth = undefined;
|
|
201
|
+
cachedLines = undefined;
|
|
182
202
|
},
|
|
183
|
-
|
|
184
|
-
const titleWidth = visibleWidth(title.plain);
|
|
185
|
-
const width = Math.max(titleWidth + 6, ...body.map((line) => visibleWidth(line.plain) + 4));
|
|
186
|
-
const blankRow = `${warning("│")}${" ".repeat(width - 2)}${warning("│")}`;
|
|
187
|
-
return [
|
|
188
|
-
`${warning("╭─")} ${title.styled} ${warning("─".repeat(width - titleWidth - 5))}${warning("╮")}`,
|
|
189
|
-
blankRow,
|
|
190
|
-
...body.map((line) => `${warning("│")} ${line.styled}${" ".repeat(width - visibleWidth(line.plain) - 4)} ${warning("│")}`),
|
|
191
|
-
blankRow,
|
|
192
|
-
`${warning("╰")}${warning("─".repeat(width - 2))}${warning("╯")}`,
|
|
193
|
-
].join("\n");
|
|
203
|
+
};
|
|
194
204
|
}
|
|
195
205
|
|
|
196
|
-
export function
|
|
206
|
+
export function announceMissingToppingsOnce(pi: ExtensionAPI): void {
|
|
197
207
|
if (setupNoticeHandled) return;
|
|
198
208
|
setupNoticeHandled = true;
|
|
199
209
|
if (isSetupCheckDisabled()) return;
|
|
200
210
|
const missing = findMissingToppings(pi);
|
|
201
|
-
if (missing.length > 0)
|
|
211
|
+
if (missing.length > 0) {
|
|
212
|
+
pi.appendEntry<MissingToppingsEntryData>(SETUP_ENTRY_TYPE, {
|
|
213
|
+
toppings: missing.map(({ topping }) => ({ pkg: topping.pkg, provides: topping.provides })),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
202
216
|
}
|
|
203
217
|
|
|
204
218
|
/** Test-only reset for the once-per-process session-start notice. */
|