@underactive/pi-topping 0.10.0 → 0.11.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 +25 -3
- package/package.json +4 -4
- package/src/output-style.ts +172 -0
- package/src/preview.ts +6 -1
- package/src/prompt-decorator.ts +6 -3
- package/src/session.ts +27 -7
- package/src/settings.ts +31 -4
package/README.md
CHANGED
|
@@ -10,10 +10,12 @@ 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. It can append the response model when it differs from the selected model, with its own color option. It can also append per-call service-tier counts (e.g. `flex tier ×3`), but only for calls whose provider reports a tier; this requires Pi ≥0.99.0. 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. It can also append per-call service-tier counts (e.g. `flex tier ×3`), but only for calls whose provider reports a tier; this requires Pi ≥0.99.0. 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. On Pi ≥1.1.0, a cancelled run (for example, one stopped with Escape) shows `Interrupted` instead of the random verb, with the word and any border in the warning color; time, tokens, and other enabled details still appear. **Show completion marker** also controls this marker. Older Pi versions show the normal marker for cancelled runs.
|
|
14
14
|
|
|
15
15
|

|
|
16
16
|
|
|
17
|
+
**Output Styles** — choose prompt-instruction styles such as Explanatory or Simplified Technical English to guide how the model responds. Styles are instructions, not output formatters.
|
|
18
|
+
|
|
17
19
|
## Install
|
|
18
20
|
|
|
19
21
|
```bash
|
|
@@ -104,15 +106,30 @@ Run `/topping-settings` (TUI only) to customize your toppings. Settings persist
|
|
|
104
106
|
║ ║
|
|
105
107
|
║ [■] Include default/random “Working” text ON ║
|
|
106
108
|
║ ║
|
|
109
|
+
╟─ Output Style ────────────────────────────────────────────────╢
|
|
110
|
+
║ [■] Style ‹ Default › ║
|
|
111
|
+
║ ║
|
|
107
112
|
╟─ Options ─────────────────────────────────────────────────────╢
|
|
108
113
|
║ [■] Use NerdFont icons ON ║
|
|
109
114
|
║ [ ] Response model footer OFF ║
|
|
110
115
|
╟───────────────────────────────────────────────────────────────╢
|
|
111
116
|
║ ↑↓ move PgUp/PgDn page ←→ select ║
|
|
112
117
|
║ ␣ toggle ⏎ apply esc cancel ║
|
|
113
|
-
╚═══════════════════════════════════════════════════════[ 9/
|
|
118
|
+
╚═══════════════════════════════════════════════════════[ 9/59 ]╝
|
|
114
119
|
```
|
|
115
120
|
|
|
121
|
+
### Output styles
|
|
122
|
+
|
|
123
|
+
Choose an output style with `←`/`→` in the Output Style section of `/topping-settings`. The selected style changes the model's prompt instructions from your next message.
|
|
124
|
+
- **Explanatory** requests brief Insight blocks before and after coding. This is a prompt instruction, not a formatter, so a turn may have no Insight.
|
|
125
|
+
- **Simplified Technical English** instructs the model to write all English prose (not code or quoted text) using ASD-STE100 Simplified Technical English (controlled language rules, active voice, simple tenses, short sentences, and small noun clusters).
|
|
126
|
+
|
|
127
|
+
While a non-default style is active, each prompt also carries a one-line hidden reminder of the style; the model receives it, but the transcript does not show it.
|
|
128
|
+
|
|
129
|
+
Output styles also apply in print and RPC modes. Set `"outputStyle": "explanatory"` (or `"simplified"`) in `~/.pi/agent/pi-topping/settings.json`; if you edit the file while an interactive session is running, use `/reload` to load the change. Switching styles changes one system-prompt section. Pi appends that change to the conversation, so providers that support mid-conversation prompt updates keep their cached prefix; other providers may re-read the full prompt once.
|
|
130
|
+
|
|
131
|
+
The Explanatory prompt is copied word for word from Claude Code's output styles, with two Pi clarifications: a lead-in that frames the instructions for Pi, and an exception allowing Insight blocks despite Pi's conciseness rule. The Simplified Technical English prompt adapts the ASD-STE100 writing rules but does not include the STE dictionary of approved words. A more specific instruction from you, your project instructions, a skill, or the file being edited takes precedence over it. When migrating from `pi-topping-insights`, its saved style is imported until you next save `/topping-settings`. Remove the old extension to avoid duplicate hidden reminders. `/insights` is gone; use `/topping-settings`.
|
|
132
|
+
|
|
116
133
|
### Word packs
|
|
117
134
|
|
|
118
135
|
Base activity words are included by default. All word packs, including the shipped **Doctor Who**, **Firefly**, **Hitchhiker's Guide**, **The Lord of the Rings**, **The Matrix**, **Portal**, **SimCity**, **Star Trek**, and **Star Wars** packs and custom packs, are disabled by default. Enable packs in `/topping-settings` to add their entries to the same uniformly selected pool. With at least one pack enabled, turn off **Include default/random “Working” text** to draw only from enabled packs; with none enabled the option is locked on. When Randomize “Worked” text is on, the completion marker uses the selected entry’s matching past tense; otherwise it uses `Worked`.
|
|
@@ -132,6 +149,8 @@ Bundled packs live in [`wordpacks/`](wordpacks/). Copy `wordpacks/doctor-who.jso
|
|
|
132
149
|
|
|
133
150
|
Pack IDs must start with a lowercase letter, contain only lowercase letters, digits, and `-`, and be at most 64 characters. Each pack needs a non-empty `name` and at least one word with non-empty `present_tense` and `past_tense` strings. An optional `description` appears in the settings preview when the pack row is selected, and an optional `attribution` records the source. Invalid entries are ignored. Packs reload when a session starts and whenever `/topping-settings` opens, not while a turn is running. Preferences for missing packs are retained so they apply when the pack returns.
|
|
134
151
|
|
|
152
|
+
### Colors and loader details
|
|
153
|
+
|
|
135
154
|
Every color setting offers `thinking-level` (shown as `thinkingLevel` in `/topping-settings`); it is the default for the animated spinner, the prompt border, and the completion marker border. The spinner, token activity monitor, token rate, and response model follow Pi's current thinking level. The prompt border uses the level captured when the prompt was submitted, and the completion marker uses the level captured when the turn settled, so earlier entries keep their color after you change level. When hand-editing `settings.json`, write `thinking-level`; the legacy `default` spelling is only recognised for `spinnerColor` and `doneMarkerBorderColor`.
|
|
136
155
|
|
|
137
156
|
Under **Elements Order**, press `␣` to grab a row, then `↑`/`↓` to slide that element
|
|
@@ -153,6 +172,8 @@ The response model is captured from assistant responses and raw Responses-API st
|
|
|
153
172
|
## Requirements
|
|
154
173
|
|
|
155
174
|
- [Pi](https://github.com/earendil-works/pi-coding-agent). Pi ≥0.84.4 is required for the waiting-for-input loader state; older Pi versions keep the animated loader.
|
|
175
|
+
- Output styles need a Pi version that exposes structured system-prompt sections (tested on Pi 1.0.3 and 1.1.0); on older hosts the style setting has no effect.
|
|
176
|
+
- Pi ≥1.1.0 shows `Interrupted` for cancelled runs; older versions show the normal completion marker.
|
|
156
177
|
- On Pi ≥0.85.0, the working indicator is embedded in the default editor border and long loaders are truncated to the border width instead of wrapping; custom editors that do not opt in keep the standalone wrapping indicator.
|
|
157
178
|
- Node.js ≥22.19.0
|
|
158
179
|
|
|
@@ -164,7 +185,7 @@ npm test
|
|
|
164
185
|
npm run typecheck
|
|
165
186
|
```
|
|
166
187
|
|
|
167
|
-
Tests cover the word/shimmer rendering, prompt box and completion marker rendering, activity meter behavior and its configurable upper bound, timer resets, settings persistence and schema migrations, word-pack parsing, loading, and default-text selection, response-model loader and footer behavior, served-model and service-tier extraction, the `menu.ts` component and its typed number entry, the settings menu wiring end to end, and the `/topping-setup` command's install and uninstall flows.
|
|
188
|
+
Tests cover the word/shimmer rendering, prompt box and completion marker rendering, activity meter behavior and its configurable upper bound, timer resets, settings persistence and schema migrations, word-pack parsing, loading, and default-text selection, output-style selection and prompt integration, response-model loader and footer behavior, served-model and service-tier extraction, the `menu.ts` component and its typed number entry, the settings menu wiring end to end, and the `/topping-setup` command's install and uninstall flows.
|
|
168
189
|
|
|
169
190
|
## Limitations
|
|
170
191
|
|
|
@@ -172,6 +193,7 @@ Tests cover the word/shimmer rendering, prompt box and completion marker renderi
|
|
|
172
193
|
- Elapsed time normally starts when you submit a prompt; for auto-continuations without that event, it starts slightly later instead.
|
|
173
194
|
- If another extension also customizes the working message, whichever one runs last wins.
|
|
174
195
|
- Service-tier details and Responses-API served-model/backend labels depend on Pi's `provider_stream_event`; raw events require Pi ≥0.99.0 and are unavailable on older Pi hosts and SDK adapters that do not expose them.
|
|
196
|
+
- Output styles have no effect on hosts without structured system-prompt sections (see Requirements).
|
|
175
197
|
|
|
176
198
|
## Author
|
|
177
199
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@underactive/pi-topping",
|
|
3
|
-
"version": "0.
|
|
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,
|
|
3
|
+
"version": "0.11.0",
|
|
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, an end-of-turn completion marker, and selectable output styles",
|
|
5
5
|
"keywords": ["pi-package", "pi-extension"],
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Eric Sison",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"@earendil-works/pi-tui": "*"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@earendil-works/pi-coding-agent": "1.0
|
|
34
|
-
"@earendil-works/pi-tui": "1.0
|
|
33
|
+
"@earendil-works/pi-coding-agent": "1.1.0",
|
|
34
|
+
"@earendil-works/pi-tui": "1.1.0",
|
|
35
35
|
"@types/node": "^24.0.0",
|
|
36
36
|
"typescript": "^5.9.3"
|
|
37
37
|
},
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { BeforeAgentStartEventResult } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export const OUTPUT_STYLE_IDS = ["default", "explanatory", "simplified"] as const;
|
|
4
|
+
export type OutputStyleId = (typeof OUTPUT_STYLE_IDS)[number];
|
|
5
|
+
|
|
6
|
+
export interface OutputStyle {
|
|
7
|
+
label: string;
|
|
8
|
+
/** Settings-preview line; keep within 60 columns so the menu doesn't truncate it. */
|
|
9
|
+
description: string;
|
|
10
|
+
/** System-prompt section and hidden reminder; omitted for Default, which leaves Pi's prompt unchanged. */
|
|
11
|
+
prompt?: { section: string; reminder: string };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Matches figures.star from Claude Code: a fallback glyph on Windows, and ★ elsewhere.
|
|
15
|
+
export const INSIGHT_STAR = process.platform === "win32" ? "✶" : "★";
|
|
16
|
+
|
|
17
|
+
// Ported verbatim from claude-code/src/constants/outputStyles.ts:30-39.
|
|
18
|
+
export const EXPLANATORY_FEATURE_PROMPT = `
|
|
19
|
+
## Insights
|
|
20
|
+
In order to encourage learning, before and after writing code, always provide brief educational explanations about implementation choices using (with backticks):
|
|
21
|
+
"\`${INSIGHT_STAR} Insight ─────────────────────────────────────\`
|
|
22
|
+
[2-3 key educational points]
|
|
23
|
+
\`─────────────────────────────────────────────────\`"
|
|
24
|
+
|
|
25
|
+
These insights should be included in the conversation, not in the codebase. You should generally focus on interesting insights that are specific to the codebase or the code you just wrote, rather than general programming concepts.`;
|
|
26
|
+
|
|
27
|
+
export const OUTPUT_STYLE_SECTION = "output_style";
|
|
28
|
+
export const OUTPUT_STYLE_MESSAGE_TYPE = "pi-topping-output-style";
|
|
29
|
+
|
|
30
|
+
export const EXPLANATORY_SECTION = `# Output Style: Explanatory
|
|
31
|
+
Respond to the user according to this Output Style, which describes how you should respond to user queries.
|
|
32
|
+
You are an interactive CLI tool that helps users with software engineering tasks. In addition to software engineering tasks, you should provide educational insights about the codebase along the way.
|
|
33
|
+
|
|
34
|
+
You should be clear and educational, providing helpful explanations while remaining focused on the task. Balance educational content with task completion. When providing insights, you may exceed typical length constraints, but remain focused and relevant.
|
|
35
|
+
Insight blocks are the one exception to the "Be concise in your responses" rule; keep everything outside them concise.
|
|
36
|
+
|
|
37
|
+
# Explanatory Style Active
|
|
38
|
+
${EXPLANATORY_FEATURE_PROMPT}`;
|
|
39
|
+
|
|
40
|
+
export const EXPLANATORY_REMINDER = "<system-reminder>\nExplanatory output style is active. Remember to follow the specific guidelines for this style.\n</system-reminder>";
|
|
41
|
+
|
|
42
|
+
export const SIMPLIFIED_TECHNICAL_ENGLISH_SECTION = `You are an interactive CLI tool that helps users with software engineering tasks.
|
|
43
|
+
|
|
44
|
+
Write all English in ASD-STE100 Simplified Technical English. STE is a controlled
|
|
45
|
+
language. The aerospace industry built it so that a reader who cannot ask a follow-up
|
|
46
|
+
question still reads the text one way only. Its rules are countable, so check your
|
|
47
|
+
prose against them as you write it.
|
|
48
|
+
|
|
49
|
+
## Precedence
|
|
50
|
+
|
|
51
|
+
These rules set the default shape of the English you write. Any more specific
|
|
52
|
+
instruction takes precedence on whatever it addresses. This includes an instruction
|
|
53
|
+
from the user, from project instructions, from an invoked skill, or from an established
|
|
54
|
+
convention in the file you edit. Where the more specific instruction is silent, these
|
|
55
|
+
rules apply.
|
|
56
|
+
|
|
57
|
+
Follow the more specific instruction without comment. Do not cite this style as a
|
|
58
|
+
reason to override it. Do not ask permission.
|
|
59
|
+
|
|
60
|
+
This exception applies to an explicit instruction only. Do not relax these rules
|
|
61
|
+
because a topic feels casual or because other prose seems friendlier.
|
|
62
|
+
|
|
63
|
+
## Never apply these rules to
|
|
64
|
+
|
|
65
|
+
- Code. This includes identifiers, syntax, and string literals.
|
|
66
|
+
- Quoted material. This includes error output, command output, file contents, and
|
|
67
|
+
another person's words. To rewrite a quotation is falsification, not simplification.
|
|
68
|
+
- Text where the exact wording carries the meaning. This includes a command to run, an
|
|
69
|
+
API name, a config key, and an exact error string.
|
|
70
|
+
|
|
71
|
+
## Rules
|
|
72
|
+
|
|
73
|
+
| Rule | Limit |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| Noun clusters | Maximum 3 words stacked as a modifier. Break a longer stack apart and name the relationship. |
|
|
76
|
+
| Main clause first | State the subject and the main verb before any qualifier. Move a relative clause to after the main verb where you can. |
|
|
77
|
+
| Sentence length | Maximum 20 words for an instruction or a procedure. Maximum 25 words for descriptive text. |
|
|
78
|
+
| One instruction per sentence | Do not join two instructions with "and" or "then". |
|
|
79
|
+
| Active voice | Use the passive voice in descriptive text only, and only when the actor is unknown or irrelevant. |
|
|
80
|
+
| Simple tenses only | Use the infinitive, the imperative, the simple present, the simple past, and the simple future. Use a past participle as an adjective only. Do not use the present perfect, the past perfect, or a compound auxiliary. |
|
|
81
|
+
| No \`-ing\` verb forms | Use an \`-ing\` word as a technical noun, or as part of one, only. |
|
|
82
|
+
| No hedge stacking | Do not chain modal verbs, as in "may have been caused by". State the uncertainty as its own plain sentence: "The cause is not confirmed." |
|
|
83
|
+
| One word, one meaning | Use one term for one concept and repeat it. Do not rotate synonyms for the same idea. |
|
|
84
|
+
| Plainest available word | Prefer the short common word to the formal or rare word. |
|
|
85
|
+
| Define domain terms | Define a term that is not common English at first use. Do not carry undefined shorthand forward. |
|
|
86
|
+
| No ellipsis | Keep the subject, the verb, and the article explicit, even when the sentence reads longer. |
|
|
87
|
+
| Paragraphs | One topic. Maximum 6 sentences. |
|
|
88
|
+
| Vertical lists | Use a numbered or bulleted list for 3 or more steps or conditions. |
|
|
89
|
+
|
|
90
|
+
## Project vocabulary
|
|
91
|
+
|
|
92
|
+
STE permits a project to define its own approved vocabulary of technical nouns and
|
|
93
|
+
verbs. A \`CONTEXT.md\` file at a repository root is that vocabulary.
|
|
94
|
+
|
|
95
|
+
If the project has a \`CONTEXT.md\`, use its terms exactly as it defines them, in the
|
|
96
|
+
part of speech it defines. Never substitute a synonym for a term it defines. Never use
|
|
97
|
+
a word that its \`_Avoid_\` lines reject. Do not redefine its terms inline, because the
|
|
98
|
+
glossary is the definition.
|
|
99
|
+
|
|
100
|
+
If the project has no \`CONTEXT.md\`, do not invent one. Do not present any term as
|
|
101
|
+
already established. The rules above apply without change: define a term at first use,
|
|
102
|
+
prefer the plainest word, and use one term for one concept.
|
|
103
|
+
|
|
104
|
+
## Length is not terseness
|
|
105
|
+
|
|
106
|
+
The caps apply to each sentence, not to the response. Clarity is the goal, not
|
|
107
|
+
concision. A long answer in short sentences is correct.
|
|
108
|
+
|
|
109
|
+
Never drop a fact, a condition, a caveat, or a scope qualifier to meet a limit. Split
|
|
110
|
+
the sentence instead.`;
|
|
111
|
+
|
|
112
|
+
export const SIMPLIFIED_TECHNICAL_ENGLISH_REMINDER = "<system-reminder>\nSimplified Technical English output style is active. Remember to follow the specific guidelines for this style.\n</system-reminder>";
|
|
113
|
+
|
|
114
|
+
export const OUTPUT_STYLES: Readonly<Record<OutputStyleId, OutputStyle>> = {
|
|
115
|
+
default: {
|
|
116
|
+
label: "Default",
|
|
117
|
+
description: "pi completes coding tasks efficiently and concisely",
|
|
118
|
+
},
|
|
119
|
+
explanatory: {
|
|
120
|
+
label: "Explanatory",
|
|
121
|
+
description: "pi explains its implementation choices and codebase patterns",
|
|
122
|
+
prompt: { section: EXPLANATORY_SECTION, reminder: EXPLANATORY_REMINDER },
|
|
123
|
+
},
|
|
124
|
+
simplified: {
|
|
125
|
+
label: "Simplified Technical English",
|
|
126
|
+
description: "pi writes simple prose in Simplified Technical English",
|
|
127
|
+
prompt: { section: SIMPLIFIED_TECHNICAL_ENGLISH_SECTION, reminder: SIMPLIFIED_TECHNICAL_ENGLISH_REMINDER },
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export function isOutputStyleId(value: unknown): value is OutputStyleId {
|
|
132
|
+
return typeof value === "string" && OUTPUT_STYLE_IDS.some(id => id === value);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Map the old pi-topping-insights style spelling to the current lowercase id. */
|
|
136
|
+
export function fromLegacyOutputStyle(value: unknown): OutputStyleId | undefined {
|
|
137
|
+
if (value === "default") return "default";
|
|
138
|
+
if (value === "Explanatory") return "explanatory";
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Apply a style's prompt section and return its hidden reminder. Default removes the section only
|
|
144
|
+
* when it holds one of these styles' text, so another extension's section survives. Hosts without
|
|
145
|
+
* structured prompt sections get no change.
|
|
146
|
+
*/
|
|
147
|
+
export function applyOutputStyle(
|
|
148
|
+
sections: Record<string, string> | undefined,
|
|
149
|
+
id: OutputStyleId,
|
|
150
|
+
): BeforeAgentStartEventResult | undefined {
|
|
151
|
+
if (!sections) return undefined;
|
|
152
|
+
|
|
153
|
+
const style = OUTPUT_STYLES[id];
|
|
154
|
+
if (!style.prompt) {
|
|
155
|
+
if (OUTPUT_STYLE_IDS.some(styleId => {
|
|
156
|
+
const ownedSection = OUTPUT_STYLES[styleId].prompt?.section;
|
|
157
|
+
return ownedSection !== undefined && ownedSection === sections[OUTPUT_STYLE_SECTION];
|
|
158
|
+
})) {
|
|
159
|
+
delete sections[OUTPUT_STYLE_SECTION];
|
|
160
|
+
}
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
sections[OUTPUT_STYLE_SECTION] = style.prompt.section;
|
|
165
|
+
return {
|
|
166
|
+
message: {
|
|
167
|
+
customType: OUTPUT_STYLE_MESSAGE_TYPE,
|
|
168
|
+
content: style.prompt.reminder,
|
|
169
|
+
display: false,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
package/src/preview.ts
CHANGED
|
@@ -4,9 +4,10 @@ import { ActivityMeter, METER_UPPER_BOUND_TPS_MAX, METER_UPPER_BOUND_TPS_MIN, ra
|
|
|
4
4
|
import { DEFAULT_WORKING_WORD, dimAttribute, ELAPSED_INTERVAL_MS, formatTokenRate, getThinkingLevelColorizer, METER_INTERVAL_MS, SHIMMER_INTERVAL_MS, SPINNER_FRAME_MS } from "./format.ts";
|
|
5
5
|
import { buildLoaderMessage } from "./loader-message.ts";
|
|
6
6
|
import { getResponseModelColorizer } from "./nvidia-green.ts";
|
|
7
|
+
import { isOutputStyleId, OUTPUT_STYLES } from "./output-style.ts";
|
|
7
8
|
import { buildCompletionMarkerContent, buildCompletionMarkerLine, buildPromptBoxLines } from "./prompt-decorator.ts";
|
|
8
9
|
import { formatServiceTierSummary } from "./service-tier.ts";
|
|
9
|
-
import { DEFAULT_SETTINGS, fromCycleDirection, fromCycleMeterUpperBound, fromCycleSpeed, isBorderStyle, isDoneMarkerBorderColor, isDoneMarkerBorderStyle, isDoneMarkerStyle, isPromptBorderColor, isSpinnerColor, isThinkingLevelColor, LOADER_ORDER_ID, MENU_ENTRIES, parseLoaderOrder } from "./settings.ts";
|
|
10
|
+
import { DEFAULT_SETTINGS, fromCycleDirection, fromCycleMeterUpperBound, fromCycleSpeed, isBorderStyle, isDoneMarkerBorderColor, isDoneMarkerBorderStyle, isDoneMarkerStyle, isPromptBorderColor, isSpinnerColor, isThinkingLevelColor, LOADER_ORDER_ID, MENU_ENTRIES, OUTPUT_STYLE_ID, parseLoaderOrder } from "./settings.ts";
|
|
10
11
|
import { isWordPackEnabled, selectWorkingTextSelection, WORD_PACK_MENU_PREFIX, wordPacksPath, type WordPack } from "./word-packs.ts";
|
|
11
12
|
// Simulated load for the menu preview: a 2.4s cosine wave peaking at 46 tps for the meter,
|
|
12
13
|
// flat 28 tps for the token readouts.
|
|
@@ -33,6 +34,10 @@ export class PreviewRenderer {
|
|
|
33
34
|
if (PROMPT_IDS.has(activeItemId ?? "")) return this.promptPreview(values, width);
|
|
34
35
|
if (MARKER_IDS.has(activeItemId ?? "")) return this.markerPreview(values, width);
|
|
35
36
|
if (activeItemId === "useNerdFont") return { lines: ["", `Icon preview: ${values.useNerdFont ? "" : "π"}`, ""] };
|
|
37
|
+
if (activeItemId === OUTPUT_STYLE_ID) {
|
|
38
|
+
const id = isOutputStyleId(values[OUTPUT_STYLE_ID]) ? values[OUTPUT_STYLE_ID] : DEFAULT_SETTINGS.outputStyle;
|
|
39
|
+
return { lines: ["", OUTPUT_STYLES[id].description, this.#ctx.ui.theme.fg("dim", "Applies from your next message")] };
|
|
40
|
+
}
|
|
36
41
|
if (activeItemId === "showResponseModelFooter") {
|
|
37
42
|
if (values.showResponseModelFooter !== true) return { lines: ["", "Response model footer is disabled.", ""] };
|
|
38
43
|
const color = isThinkingLevelColor(values.responseModelColor) ? values.responseModelColor : DEFAULT_SETTINGS.decorations.responseModelColor;
|
package/src/prompt-decorator.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MessageRenderer } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { MessageRenderer, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
3
3
|
import { getThinkingLevelColorizer, isThinkingLevel, type ThinkingLevel } from "./format.ts";
|
|
4
4
|
import { DEFAULT_SETTINGS, isBorderStyle, isPromptBorderColor, type BorderStyle, type DoneMarkerBorderColor, type DoneMarkerBorderStyle, type DoneMarkerStyle, type PromptBorderColor } from "./settings.ts";
|
|
@@ -41,7 +41,7 @@ type PromptTheme = {
|
|
|
41
41
|
|
|
42
42
|
const COMPLETION_MARKER_TRAIL_RUNS = [6, 4, 2, 1] as const;
|
|
43
43
|
|
|
44
|
-
/** Assemble the completion
|
|
44
|
+
/** Assemble the completion summary with an optional word color and pre-styled model suffix. */
|
|
45
45
|
export function buildCompletionMarkerContent(
|
|
46
46
|
theme: PromptTheme,
|
|
47
47
|
icon: string,
|
|
@@ -49,9 +49,12 @@ export function buildCompletionMarkerContent(
|
|
|
49
49
|
elapsed: string,
|
|
50
50
|
details: string[],
|
|
51
51
|
model = "",
|
|
52
|
+
wordColor: ThemeColor = "dim",
|
|
52
53
|
): string {
|
|
53
54
|
const tail = details.length ? ` (${details.join(" · ")})` : "";
|
|
54
|
-
const summary =
|
|
55
|
+
const summary = wordColor === "dim"
|
|
56
|
+
? theme.fg("dim", `${icon ? " " : ""}${word} for ${elapsed}${tail}`)
|
|
57
|
+
: `${icon ? theme.fg("dim", " ") : ""}${theme.fg(wordColor, word)}${theme.fg("dim", ` for ${elapsed}${tail}`)}`;
|
|
55
58
|
return `${icon}${summary}${model ? `${theme.fg("dim", " · ")}${model}` : ""}`;
|
|
56
59
|
}
|
|
57
60
|
|
package/src/session.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AgentSettledEvent,
|
|
3
3
|
AgentStartEvent,
|
|
4
|
+
BeforeAgentStartEvent,
|
|
5
|
+
BeforeAgentStartEventResult,
|
|
4
6
|
CustomEntry,
|
|
5
7
|
ExtensionAPI,
|
|
6
8
|
ExtensionCommandContext,
|
|
@@ -40,6 +42,7 @@ import {
|
|
|
40
42
|
} from "./format.ts";
|
|
41
43
|
import { buildLoaderMessage } from "./loader-message.ts";
|
|
42
44
|
import { showMenu } from "./menu.ts";
|
|
45
|
+
import { applyOutputStyle, OUTPUT_STYLES } from "./output-style.ts";
|
|
43
46
|
import { getResponseModelColorizer } from "./nvidia-green.ts";
|
|
44
47
|
import { registerSetupCommand } from "./setup-command.ts";
|
|
45
48
|
import { applyMenuResult, buildMenuSections, loadSettings, saveSettings, type ThinkingLevelColor } from "./settings.ts";
|
|
@@ -79,6 +82,7 @@ const TOKEN_RATE_HOLD_MS = 1_500;
|
|
|
79
82
|
const TOKEN_RATE_FADE_MS = 250;
|
|
80
83
|
|
|
81
84
|
interface DoneEntryData {
|
|
85
|
+
aborted?: boolean;
|
|
82
86
|
word: string;
|
|
83
87
|
elapsedMs: number;
|
|
84
88
|
tokens?: number;
|
|
@@ -221,13 +225,21 @@ export class SessionManager {
|
|
|
221
225
|
}
|
|
222
226
|
};
|
|
223
227
|
|
|
228
|
+
/** Unlike the UI handlers, this runs in every mode: the output style shapes the model's prompt. */
|
|
229
|
+
#onBeforeAgentStart = (event: BeforeAgentStartEvent): BeforeAgentStartEventResult | undefined =>
|
|
230
|
+
applyOutputStyle(event.systemPromptOptions?.sections, this.#settings.outputStyle);
|
|
231
|
+
|
|
224
232
|
#onInput = async (event: InputEvent, ctx: ExtensionContext): Promise<InputEventResult | void> => {
|
|
225
233
|
this.#currentCtx = ctx;
|
|
226
234
|
if (!this.usable(ctx)) return;
|
|
227
235
|
|
|
228
236
|
if (!event.streamingBehavior && !this.#state.busy) this.resetTurn(Date.now());
|
|
229
237
|
else if (event.streamingBehavior && event.source !== "extension") this.#state.midTurnInputs++;
|
|
230
|
-
if (
|
|
238
|
+
if (
|
|
239
|
+
this.#settings.decorations.decorateUserPrompt &&
|
|
240
|
+
OUTPUT_STYLES[this.#settings.outputStyle].prompt === undefined &&
|
|
241
|
+
this.shouldDecorate(event)
|
|
242
|
+
) {
|
|
231
243
|
try {
|
|
232
244
|
await this.#pi.sendMessage<PromptBoxDetails>(
|
|
233
245
|
{
|
|
@@ -356,7 +368,7 @@ export class SessionManager {
|
|
|
356
368
|
this.tick();
|
|
357
369
|
};
|
|
358
370
|
|
|
359
|
-
#onAgentSettled = async (
|
|
371
|
+
#onAgentSettled = async (e: AgentSettledEvent, ctx: ExtensionContext): Promise<void> => {
|
|
360
372
|
this.#currentCtx = ctx;
|
|
361
373
|
if (!this.usable(ctx)) return;
|
|
362
374
|
this.commitServiceTierCall();
|
|
@@ -379,8 +391,10 @@ export class SessionManager {
|
|
|
379
391
|
resetTokenRateState(this.#state);
|
|
380
392
|
this.#state.lastMessage = NOT_SENT;
|
|
381
393
|
if (this.#settings.features.doneMarker && hadPrompt) {
|
|
394
|
+
const aborted = e.aborted === true;
|
|
382
395
|
this.#pi.appendEntry<DoneEntryData>(DONE_ENTRY_TYPE, {
|
|
383
|
-
|
|
396
|
+
...(aborted ? { aborted: true } : {}),
|
|
397
|
+
word: aborted ? "Interrupted" : this.#settings.features.randomizeDoneMarker ? this.#state.workingText.pastTense : "Worked",
|
|
384
398
|
elapsedMs,
|
|
385
399
|
tokens: this.#state.confirmTokens,
|
|
386
400
|
midTurnInputs: this.#state.midTurnInputs,
|
|
@@ -401,6 +415,7 @@ export class SessionManager {
|
|
|
401
415
|
|
|
402
416
|
#renderDoneEntry(entry: CustomEntry<DoneEntryData>, theme: Theme): Component | undefined {
|
|
403
417
|
if (!entry.data) return undefined;
|
|
418
|
+
const aborted = entry.data.aborted === true;
|
|
404
419
|
const word = typeof entry.data.word === "string" ? stripControlChars(entry.data.word) : "Worked";
|
|
405
420
|
const elapsedMs = typeof entry.data.elapsedMs === "number" && Number.isFinite(entry.data.elapsedMs) ? entry.data.elapsedMs : 0;
|
|
406
421
|
const features = this.#settings.features;
|
|
@@ -420,9 +435,9 @@ export class SessionManager {
|
|
|
420
435
|
? getThinkingLevelColorizer(theme, this.#settings.decorations.doneMarkerModelColor, thinkingLevel)(rawModel)
|
|
421
436
|
: "";
|
|
422
437
|
const model = modelColored && this.#settings.decorations.doneMarkerModelDimmed ? dimAttribute(modelColored) : modelColored;
|
|
423
|
-
const markerContent = buildCompletionMarkerContent(theme, icon, word, formatElapsed(elapsedMs), details, model);
|
|
438
|
+
const markerContent = buildCompletionMarkerContent(theme, icon, word, formatElapsed(elapsedMs), details, model, aborted ? "warning" : "dim");
|
|
424
439
|
const borderStyle = this.#settings.decorations.doneMarkerBorderStyle;
|
|
425
|
-
const borderColor = this.#settings.decorations.doneMarkerBorderColor;
|
|
440
|
+
const borderColor = aborted ? "warning" : this.#settings.decorations.doneMarkerBorderColor;
|
|
426
441
|
const markerStyle = this.#settings.decorations.doneMarkerStyle;
|
|
427
442
|
if (borderStyle === "none") return new Text(markerContent);
|
|
428
443
|
|
|
@@ -445,6 +460,7 @@ export class SessionManager {
|
|
|
445
460
|
|
|
446
461
|
install(): void {
|
|
447
462
|
this.#pi.on("session_start", this.#onSessionStart);
|
|
463
|
+
this.#pi.on("before_agent_start", this.#onBeforeAgentStart);
|
|
448
464
|
this.#pi.on("input", this.#onInput);
|
|
449
465
|
this.#pi.on("agent_start", this.#onAgentStart);
|
|
450
466
|
this.#pi.on("message_start", this.#onMessageStart);
|
|
@@ -470,7 +486,7 @@ export class SessionManager {
|
|
|
470
486
|
this.#pi.registerEntryRenderer<MissingToppingsEntryData>(SETUP_ENTRY_TYPE, (entry, _o, theme) => renderMissingToppingsEntry(entry, theme));
|
|
471
487
|
this.#pi.registerMessageRenderer<PromptBoxDetails>(PROMPT_BOX_TYPE, promptBoxRenderer);
|
|
472
488
|
this.#pi.registerCommand("topping-settings", {
|
|
473
|
-
description: "Configure prompt
|
|
489
|
+
description: "Configure the prompt box, working loader, completion marker, word packs, and output style.",
|
|
474
490
|
handler: async (_a, ctx) => this.showSettings(ctx),
|
|
475
491
|
});
|
|
476
492
|
registerSetupCommand(this.#pi);
|
|
@@ -789,6 +805,7 @@ export class SessionManager {
|
|
|
789
805
|
this.#userPacks = loadUserWordPacks();
|
|
790
806
|
this.#allPacks = [...this.#bundledPacks, ...this.#userPacks];
|
|
791
807
|
const before = this.indicatorFingerprint();
|
|
808
|
+
const styleBefore = this.#settings.outputStyle;
|
|
792
809
|
const preview = new PreviewRenderer(ctx, this.#allPacks);
|
|
793
810
|
const result = await showMenu<Record<string, boolean | string>>(ctx, {
|
|
794
811
|
title: "Pi Topping: Settings",
|
|
@@ -818,6 +835,9 @@ export class SessionManager {
|
|
|
818
835
|
this.startTimer();
|
|
819
836
|
this.tick();
|
|
820
837
|
}
|
|
821
|
-
|
|
838
|
+
const notification = styleBefore === this.#settings.outputStyle
|
|
839
|
+
? "Pi Topping settings saved"
|
|
840
|
+
: `Pi Topping settings saved · ${OUTPUT_STYLES[this.#settings.outputStyle].label} output style applies from your next message`;
|
|
841
|
+
ctx.ui.notify(notification, "info");
|
|
822
842
|
}
|
|
823
843
|
}
|
package/src/settings.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { DEFAULT_METER_UPPER_BOUND_TPS, METER_UPPER_BOUND_TPS_MAX, METER_UPPER_BOUND_TPS_MIN } from "./activity-meter.ts";
|
|
5
5
|
import { DEFAULT_LOADER_ORDER, type LoaderElement } from "./format.ts";
|
|
6
|
+
import { fromLegacyOutputStyle, isOutputStyleId, OUTPUT_STYLE_IDS, OUTPUT_STYLES, type OutputStyleId } from "./output-style.ts";
|
|
6
7
|
import type { MenuNumberEntry, MenuSection } from "./menu.ts";
|
|
7
8
|
import { isWordPackEnabled, isWordPackId, WORD_PACK_MENU_PREFIX, type WordPack } from "./word-packs.ts";
|
|
8
9
|
import { isPlainObject } from "./util.ts";
|
|
@@ -125,6 +126,7 @@ export interface DecoratorSettings {
|
|
|
125
126
|
};
|
|
126
127
|
loaderOrder: LoaderElement[];
|
|
127
128
|
wordPacks: Record<string, boolean>;
|
|
129
|
+
outputStyle: OutputStyleId;
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
export const DEFAULT_SETTINGS: DecoratorSettings = {
|
|
@@ -132,10 +134,13 @@ export const DEFAULT_SETTINGS: DecoratorSettings = {
|
|
|
132
134
|
features: { substituteDefaultMessage: true, elapsedTime: true, outputTokens: true, tokenRate: true, responseModel: true, responseModelFooter: false, doneMarker: true, doneMarkerIcon: true, randomizeDoneMarker: true, doneMarkerTokens: true, doneMarkerInputs: true, doneMarkerServiceTier: true, doneMarkerModel: true, includeDefaultWorkingText: true },
|
|
133
135
|
loaderOrder: [...DEFAULT_LOADER_ORDER],
|
|
134
136
|
wordPacks: {},
|
|
137
|
+
outputStyle: "default",
|
|
135
138
|
};
|
|
136
139
|
|
|
137
140
|
/** Menu key carrying the loader element order as a comma-joined list of element ids. */
|
|
138
141
|
export const LOADER_ORDER_ID = "loaderOrder";
|
|
142
|
+
/** Menu key for the output style cycle row. */
|
|
143
|
+
export const OUTPUT_STYLE_ID = "outputStyle";
|
|
139
144
|
const LOADER_ELEMENT_LABELS: Record<LoaderElement, string> = {
|
|
140
145
|
spinner: "Animated spinner",
|
|
141
146
|
text: "“Working” text",
|
|
@@ -163,6 +168,19 @@ export function parseLoaderOrder(value: unknown): LoaderElement[] {
|
|
|
163
168
|
}
|
|
164
169
|
|
|
165
170
|
export function settingsPath(): string { return join(getAgentDir(), "pi-topping", "settings.json"); }
|
|
171
|
+
|
|
172
|
+
/** pi-topping-insights' saved style, honored until pi-topping's own settings record one. */
|
|
173
|
+
function legacyOutputStyle(): OutputStyleId {
|
|
174
|
+
try {
|
|
175
|
+
const parsed: unknown = JSON.parse(readFileSync(join(getAgentDir(), "pi-topping-insights", "settings.json"), "utf8"));
|
|
176
|
+
return (isPlainObject(parsed) ? fromLegacyOutputStyle(parsed.style) : undefined) ?? DEFAULT_SETTINGS.outputStyle;
|
|
177
|
+
} catch { return DEFAULT_SETTINGS.outputStyle; }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function defaultSettings(): DecoratorSettings {
|
|
181
|
+
return { ...structuredClone(DEFAULT_SETTINGS), outputStyle: legacyOutputStyle() };
|
|
182
|
+
}
|
|
183
|
+
|
|
166
184
|
function mergeGroup<T extends Record<string, boolean | string | number>>(defaults: T, parsed: unknown): T {
|
|
167
185
|
const merged = { ...defaults };
|
|
168
186
|
if (!isPlainObject(parsed)) return merged;
|
|
@@ -313,6 +331,10 @@ export function buildMenuSections(settings: DecoratorSettings, bundledPacks: rea
|
|
|
313
331
|
...buildSection("Word Packs", settings).items.map((item, index) => ({ ...item, spacerBefore: index === 0, disabledUnlessAnyOf: packIds })),
|
|
314
332
|
],
|
|
315
333
|
},
|
|
334
|
+
{
|
|
335
|
+
title: "Output Style",
|
|
336
|
+
items: [{ id: OUTPUT_STYLE_ID, label: "Style", value: settings.outputStyle, cycleValues: OUTPUT_STYLE_IDS, cycleValueLabels: Object.fromEntries(OUTPUT_STYLE_IDS.map(id => [id, OUTPUT_STYLES[id].label])) }],
|
|
337
|
+
},
|
|
316
338
|
buildSection("Options", settings),
|
|
317
339
|
];
|
|
318
340
|
}
|
|
@@ -337,6 +359,8 @@ export function applyMenuResult(settings: DecoratorSettings, values: Record<stri
|
|
|
337
359
|
if (isWordPackId(packId)) next.wordPacks[packId] = value;
|
|
338
360
|
}
|
|
339
361
|
if (typeof values[LOADER_ORDER_ID] === "string") next.loaderOrder = parseLoaderOrder(values[LOADER_ORDER_ID]);
|
|
362
|
+
const outputStyle = values[OUTPUT_STYLE_ID];
|
|
363
|
+
if (isOutputStyleId(outputStyle)) next.outputStyle = outputStyle;
|
|
340
364
|
return next;
|
|
341
365
|
}
|
|
342
366
|
|
|
@@ -350,12 +374,13 @@ export function applyMenuResult(settings: DecoratorSettings, values: Record<stri
|
|
|
350
374
|
* border color is reset. Below 4, the response-model footer inherits the response-model
|
|
351
375
|
* setting. A missing, non-integer, or out-of-range meter upper bound resolves
|
|
352
376
|
* to the default of 80 tps. A legacy `features.simCityWorkingText: true` enables the
|
|
353
|
-
* `simcity` pack when no explicit preference is stored.
|
|
377
|
+
* `simcity` pack when no explicit preference is stored. A missing or invalid `outputStyle`
|
|
378
|
+
* falls back to `pi-topping-insights/settings.json`'s `style`, then to `default`, without a schema-version gate.
|
|
354
379
|
*/
|
|
355
380
|
export function loadSettings(): DecoratorSettings {
|
|
356
381
|
try {
|
|
357
382
|
const parsed = JSON.parse(readFileSync(settingsPath(), "utf8"));
|
|
358
|
-
if (!isPlainObject(parsed)) return
|
|
383
|
+
if (!isPlainObject(parsed)) return defaultSettings();
|
|
359
384
|
const wordPacks: Record<string, boolean> = { ...DEFAULT_SETTINGS.wordPacks };
|
|
360
385
|
if (isPlainObject(parsed.wordPacks)) {
|
|
361
386
|
for (const [id, enabled] of Object.entries(parsed.wordPacks)) {
|
|
@@ -365,7 +390,9 @@ export function loadSettings(): DecoratorSettings {
|
|
|
365
390
|
if (isPlainObject(parsed.features) && parsed.features.simCityWorkingText === true && !("simcity" in wordPacks)) {
|
|
366
391
|
wordPacks.simcity = true;
|
|
367
392
|
}
|
|
368
|
-
const
|
|
393
|
+
const storedOutputStyle = parsed.outputStyle;
|
|
394
|
+
const outputStyle = isOutputStyleId(storedOutputStyle) ? storedOutputStyle : legacyOutputStyle();
|
|
395
|
+
const settings = { decorations: mergeGroup(DEFAULT_SETTINGS.decorations, parsed.decorations), features: mergeGroup(DEFAULT_SETTINGS.features, parsed.features), loaderOrder: parseLoaderOrder(parsed.loaderOrder), wordPacks, outputStyle };
|
|
369
396
|
if (isPlainObject(parsed.decorations)) {
|
|
370
397
|
for (const [key, value] of Object.entries(parsed.decorations)) {
|
|
371
398
|
if (!Object.hasOwn(DEFAULT_SETTINGS.decorations, key) || typeof value !== "string") continue;
|
|
@@ -394,7 +421,7 @@ export function loadSettings(): DecoratorSettings {
|
|
|
394
421
|
}
|
|
395
422
|
}
|
|
396
423
|
return settings;
|
|
397
|
-
} catch { return
|
|
424
|
+
} catch { return defaultSettings(); }
|
|
398
425
|
}
|
|
399
426
|
|
|
400
427
|
export function atomicWriteFile(path: string, contents: string): void {
|