@underactive/pi-topping 0.7.1 → 0.8.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 +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 +24 -12
- package/src/settings.ts +10 -3
- package/src/toppings.ts +52 -38
- package/src/flags.ts +0 -19
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
|
+
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.
|
|
3
|
+
"version": "0.8.0",
|
|
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
|
@@ -38,14 +38,14 @@ import {
|
|
|
38
38
|
StreamingWordCounter,
|
|
39
39
|
type ThinkingLevel,
|
|
40
40
|
} from "./format.ts";
|
|
41
|
-
import { isSiblingSetupEnabled } from "./flags.ts";
|
|
42
41
|
import { showMenu } from "./menu.ts";
|
|
42
|
+
import { getResponseModelColorizer } from "./nvidia-green.ts";
|
|
43
43
|
import { registerSetupCommand } from "./setup-command.ts";
|
|
44
44
|
import { applyMenuResult, buildMenuSections, loadSettings, saveSettings, type ThinkingLevelColor } from "./settings.ts";
|
|
45
|
-
import {
|
|
45
|
+
import { announceMissingToppingsOnce, type MissingToppingsEntryData, renderMissingToppingsEntry, SETUP_ENTRY_TYPE } from "./toppings.ts";
|
|
46
46
|
import { PreviewRenderer } from "./preview.ts";
|
|
47
47
|
import { buildCompletionMarkerContent, buildCompletionMarkerLine, PROMPT_BOX_TYPE, promptBoxRenderer, type PromptBoxDetails } from "./prompt-decorator.ts";
|
|
48
|
-
import { modelsResemble, stripControlChars } from "./util.ts";
|
|
48
|
+
import { isPlainObject, modelsResemble, stripControlChars } from "./util.ts";
|
|
49
49
|
import { loadBundledWordPacks, loadUserWordPacks, pickWorkingTextSelection, type WorkingTextSelection, type WordPack } from "./word-packs.ts";
|
|
50
50
|
|
|
51
51
|
type MessageStartEvent = Extract<ExtensionEvent, { type: "message_start" }>;
|
|
@@ -77,6 +77,7 @@ interface DoneEntryData {
|
|
|
77
77
|
tokens?: number;
|
|
78
78
|
midTurnInputs?: number;
|
|
79
79
|
thinkingLevel?: ThinkingLevel;
|
|
80
|
+
model?: string;
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
interface SessionState {
|
|
@@ -148,6 +149,10 @@ function waitingLabel(waiting: NonNullable<SessionState["waiting"]>): string {
|
|
|
148
149
|
return title ? `Waiting: ${title}` : WAITING_LABELS[waiting.kind];
|
|
149
150
|
}
|
|
150
151
|
|
|
152
|
+
function responseModelOf(message: unknown): unknown {
|
|
153
|
+
return isPlainObject(message) ? message.responseModel : undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
151
156
|
/** Owns mutable extension state and Pi lifecycle registrations. */
|
|
152
157
|
export class SessionManager {
|
|
153
158
|
#counter = new StreamingWordCounter();
|
|
@@ -175,7 +180,7 @@ export class SessionManager {
|
|
|
175
180
|
this.#state.activityMeter.setDirection(this.#settings.decorations.meterDirection);
|
|
176
181
|
if (this.usable(ctx)) {
|
|
177
182
|
this.applyIndicator(ctx);
|
|
178
|
-
|
|
183
|
+
announceMissingToppingsOnce(this.#pi);
|
|
179
184
|
}
|
|
180
185
|
};
|
|
181
186
|
|
|
@@ -232,7 +237,7 @@ export class SessionManager {
|
|
|
232
237
|
if (this.usable(ctx) && event.message.role === "assistant") {
|
|
233
238
|
this.#state.liveTokens = 0;
|
|
234
239
|
this.#counter.reset();
|
|
235
|
-
this.updateResponseModel((event.message
|
|
240
|
+
this.updateResponseModel(responseModelOf(event.message));
|
|
236
241
|
}
|
|
237
242
|
};
|
|
238
243
|
|
|
@@ -247,7 +252,7 @@ export class SessionManager {
|
|
|
247
252
|
this.#state.liveTokens += this.#counter.count(assistantEvent.delta, assistantEvent.type);
|
|
248
253
|
}
|
|
249
254
|
const rawResponseModel = (assistantEvent as { partial?: { responseModel?: unknown } } | undefined)?.partial?.responseModel
|
|
250
|
-
?? (event.message
|
|
255
|
+
?? responseModelOf(event.message);
|
|
251
256
|
this.updateResponseModel(rawResponseModel);
|
|
252
257
|
};
|
|
253
258
|
|
|
@@ -268,7 +273,7 @@ export class SessionManager {
|
|
|
268
273
|
}
|
|
269
274
|
this.#state.liveTokens = 0;
|
|
270
275
|
this.#counter.reset();
|
|
271
|
-
this.updateResponseModel((event.message
|
|
276
|
+
this.updateResponseModel(responseModelOf(event.message));
|
|
272
277
|
};
|
|
273
278
|
|
|
274
279
|
#onToolExecutionStart = async (_e: ToolExecutionStartEvent, ctx: ExtensionContext): Promise<void> => {
|
|
@@ -324,6 +329,7 @@ export class SessionManager {
|
|
|
324
329
|
tokens: this.#state.confirmTokens,
|
|
325
330
|
midTurnInputs: this.#state.midTurnInputs,
|
|
326
331
|
thinkingLevel: ctx.thinkingLevel,
|
|
332
|
+
model: this.#state.responseModel || undefined,
|
|
327
333
|
});
|
|
328
334
|
}
|
|
329
335
|
this.#currentCtx = null;
|
|
@@ -346,11 +352,16 @@ export class SessionManager {
|
|
|
346
352
|
details.push(`${entry.data.midTurnInputs} mid-turn input${entry.data.midTurnInputs === 1 ? "" : "s"}`);
|
|
347
353
|
}
|
|
348
354
|
const icon = features.doneMarkerIcon ? theme.fg("text", this.#settings.decorations.useNerdFont ? "" : "π") : "";
|
|
349
|
-
const
|
|
355
|
+
const thinkingLevel = isThinkingLevel(entry.data.thinkingLevel) ? entry.data.thinkingLevel : undefined;
|
|
356
|
+
const rawModel = typeof entry.data.model === "string" ? stripControlChars(entry.data.model).trim() : "";
|
|
357
|
+
const modelColored = features.doneMarkerModel && rawModel
|
|
358
|
+
? getThinkingLevelColorizer(theme, this.#settings.decorations.doneMarkerModelColor, thinkingLevel)(rawModel)
|
|
359
|
+
: "";
|
|
360
|
+
const model = modelColored && this.#settings.decorations.doneMarkerModelDimmed ? dimAttribute(modelColored) : modelColored;
|
|
361
|
+
const markerContent = buildCompletionMarkerContent(theme, icon, word, formatElapsed(elapsedMs), details, model);
|
|
350
362
|
const borderStyle = this.#settings.decorations.doneMarkerBorderStyle;
|
|
351
363
|
const borderColor = this.#settings.decorations.doneMarkerBorderColor;
|
|
352
364
|
const markerStyle = this.#settings.decorations.doneMarkerStyle;
|
|
353
|
-
const thinkingLevel = isThinkingLevel(entry.data.thinkingLevel) ? entry.data.thinkingLevel : undefined;
|
|
354
365
|
if (borderStyle === "none") return new Text(markerContent);
|
|
355
366
|
|
|
356
367
|
let cachedWidth: number | undefined;
|
|
@@ -383,12 +394,13 @@ export class SessionManager {
|
|
|
383
394
|
this.#pi.on("ui_prompt_end", this.#onUIPromptEnd);
|
|
384
395
|
this.#pi.on("session_shutdown", this.#onSessionShutdown);
|
|
385
396
|
this.#pi.registerEntryRenderer<DoneEntryData>(DONE_ENTRY_TYPE, (entry, _o, theme) => this.#renderDoneEntry(entry, theme));
|
|
397
|
+
this.#pi.registerEntryRenderer<MissingToppingsEntryData>(SETUP_ENTRY_TYPE, (entry, _o, theme) => renderMissingToppingsEntry(entry, theme));
|
|
386
398
|
this.#pi.registerMessageRenderer<PromptBoxDetails>(PROMPT_BOX_TYPE, promptBoxRenderer);
|
|
387
399
|
this.#pi.registerCommand("topping-settings", {
|
|
388
400
|
description: "Configure prompt decoration, working-loader features/order, and completion-marker settings.",
|
|
389
401
|
handler: async (_a, ctx) => this.showSettings(ctx),
|
|
390
402
|
});
|
|
391
|
-
|
|
403
|
+
registerSetupCommand(this.#pi);
|
|
392
404
|
}
|
|
393
405
|
|
|
394
406
|
private usable(ctx: ExtensionContext): boolean {
|
|
@@ -487,7 +499,7 @@ export class SessionManager {
|
|
|
487
499
|
private startResponseModelFade(ctx: ExtensionContext, model: string, color: ThinkingLevelColor, dimmed: boolean): void {
|
|
488
500
|
this.cancelResponseModelFade(ctx);
|
|
489
501
|
const generation = this.#state.responseModelFadeGeneration;
|
|
490
|
-
const colorizer =
|
|
502
|
+
const colorizer = getResponseModelColorizer(ctx.ui.theme, color, ctx.thinkingLevel, ctx.model?.provider);
|
|
491
503
|
const render = (shade?: number): void => {
|
|
492
504
|
const colored = shade === undefined
|
|
493
505
|
? colorizer(model)
|
|
@@ -581,7 +593,7 @@ export class SessionManager {
|
|
|
581
593
|
? getThinkingLevelColorizer(ctx.ui.theme, decorations.spinnerColor, ctx.thinkingLevel)(SPINNER_FRAMES[Math.floor(now / SPINNER_FRAME_MS) % SPINNER_FRAMES.length]!)
|
|
582
594
|
: "";
|
|
583
595
|
const responseModelColored = features.responseModel && state.responseModel
|
|
584
|
-
?
|
|
596
|
+
? getResponseModelColorizer(ctx.ui.theme, decorations.responseModelColor, ctx.thinkingLevel, ctx.model?.provider)(state.responseModel)
|
|
585
597
|
: "";
|
|
586
598
|
const responseModel = responseModelColored && decorations.responseModelDimmed ? dimAttribute(responseModelColored) : responseModelColored;
|
|
587
599
|
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. */
|
package/src/flags.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Feature gates for unreleased functionality.
|
|
3
|
-
*
|
|
4
|
-
* Sibling-topping detection and the `/topping-setup` command ship dark: the
|
|
5
|
-
* default below stays `false` until the flow is ready for the public. Setting
|
|
6
|
-
* `PI_TOPPING_SIBLING_SETUP` to any value other than `0`, `false`, `off`, or
|
|
7
|
-
* `no` (case-insensitive) opts a session in without touching code.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const SIBLING_SETUP_DEFAULT = false;
|
|
11
|
-
|
|
12
|
-
const FALSEY_FLAG_VALUES = new Set(["0", "false", "off", "no"]);
|
|
13
|
-
|
|
14
|
-
/** Whether the sibling-topping setup flow is registered and advertised. */
|
|
15
|
-
export function isSiblingSetupEnabled(): boolean {
|
|
16
|
-
const value = process.env.PI_TOPPING_SIBLING_SETUP?.trim().toLowerCase();
|
|
17
|
-
if (value === undefined || value === "") return SIBLING_SETUP_DEFAULT;
|
|
18
|
-
return !FALSEY_FLAG_VALUES.has(value);
|
|
19
|
-
}
|