pi-native-output-styles 0.4.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/CHANGELOG.md +24 -0
- package/LICENSE +22 -0
- package/README.md +94 -0
- package/extensions/output-styles.ts +401 -0
- package/extensions/styles/concise.md +5 -0
- package/extensions/styles/diagrams-first.md +5 -0
- package/extensions/styles/eli5.md +13 -0
- package/extensions/styles/explanatory.md +5 -0
- package/extensions/styles/reviewer.md +5 -0
- package/extensions/styles/ste.md +50 -0
- package/extensions/styles/teacher.md +5 -0
- package/package.json +33 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
Repackaged as `pi-native-output-styles` — a Pi-only output-style switcher built on Pi's native `.pi/` directories.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Native `.pi/` locations. Styles resolve from `~/.pi/agent/output-styles/` and `<repo>/.pi/output-styles/`; `--save` / `--project` write `~/.pi/agent/output-styles.json` and `<repo>/.pi/output-styles.json`.
|
|
10
|
+
- `PI_CODING_AGENT_DIR` is honoured as the user config root (default `~/.pi/agent`).
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- `/style` now injects a single `# Personality` block at the top of the system prompt, replacing the previous slot-swapping behaviour. It is idempotent and touches nothing else in the prompt.
|
|
15
|
+
- Bundled styles are `concise`, `explanatory`, `teacher`, `reviewer`, `diagrams-first`, `ste`, `eli5`.
|
|
16
|
+
|
|
17
|
+
### Removed
|
|
18
|
+
|
|
19
|
+
- All non-Pi harness support: the `.omp/` style and state locations, the slot-replacement logic for `§`-delimited prompts, and the three `omp-*` bundled presets.
|
|
20
|
+
- The CLI demo assets, which showed another harness.
|
|
21
|
+
|
|
22
|
+
### Notes
|
|
23
|
+
|
|
24
|
+
Forked from [`LoneExile/pi-output-styles`](https://github.com/LoneExile/pi-output-styles). Inherited changelog entries for versions before the fork are not reproduced here; see upstream for that history.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LoneExile
|
|
4
|
+
Copyright (c) 2026 code-koan contributors
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# pi-native-output-styles
|
|
2
|
+
|
|
3
|
+
[](https://github.com/code-koan/pi-native-output-styles/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/pi-native-output-styles)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
|
|
7
|
+
Named, swappable system-prompt styles for [Pi](https://pi.dev) — with a live `/style` switcher. Unlike Claude Code's output styles (which need `/clear` to switch), styles here apply and switch **live, mid-session**.
|
|
8
|
+
|
|
9
|
+
Styles and saved defaults live in Pi's own directories: `~/.pi/agent/output-styles/` and `<repo>/.pi/output-styles/`.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pi install npm:pi-native-output-styles
|
|
15
|
+
|
|
16
|
+
# or straight from the repo
|
|
17
|
+
pi install git:github.com/code-koan/pi-native-output-styles
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Then start a **new** session. Extensions do not hot-reload.
|
|
21
|
+
|
|
22
|
+
## Use
|
|
23
|
+
|
|
24
|
+
- `/style` — show the active style and list the available ones.
|
|
25
|
+
- `/style <name>` — activate a style for this session.
|
|
26
|
+
- `/style <name> --save` — also save it as your personal (user) default.
|
|
27
|
+
- `/style <name> --project` — save it as the project default (committed with the repo).
|
|
28
|
+
- `/style off` — clear the active style for this session, overriding any saved default.
|
|
29
|
+
- `/style off --save` / `/style off --project` — also clear the saved default. `none` is an alias for `off`.
|
|
30
|
+
- While composing `/style`, a hint line below the input shows the available flags.
|
|
31
|
+
|
|
32
|
+
The style is applied every turn, and the status line shows `style: eli5`.
|
|
33
|
+
|
|
34
|
+
## How a style is applied
|
|
35
|
+
|
|
36
|
+
Pi's assembled system prompt is flat prose plus XML blocks — there is no personality slot to replace. The active style is injected as a `# Personality` block at the **top** of the prompt, above the tool list and the cwd line, the way Claude Code output styles sit.
|
|
37
|
+
|
|
38
|
+
It is idempotent: a prompt that already carries the marker is left untouched, so switching styles mid-session never stacks a second block. Nothing else in the prompt is modified.
|
|
39
|
+
|
|
40
|
+
## Bundled styles
|
|
41
|
+
|
|
42
|
+
`concise` · `explanatory` · `teacher` · `reviewer` · `diagrams-first` · `ste` · `eli5`
|
|
43
|
+
|
|
44
|
+
`ste` writes in [ASD-STE100](https://asd-ste100.org) Simplified Technical English, with the v2.0 action-first reply shape for person-addressed replies, tasks, issues, pull request descriptions, and commit messages. Adapted from [Ege Chelebi's ste-writing skill](https://github.com/woosal1337/blog/blob/9240b25eac013467554fd8217f319743aa0282b8/videos/ep01-the-cure-for-ai-slop/asd-ste100/SKILL.md).
|
|
45
|
+
|
|
46
|
+
`eli5` is [Lydia Hallie's ELI5 style](https://x.com/lydiahallie/status/2080378470111256907).
|
|
47
|
+
|
|
48
|
+
## Custom styles
|
|
49
|
+
|
|
50
|
+
Drop a Markdown file in either location. The filename is the style name unless frontmatter overrides it.
|
|
51
|
+
|
|
52
|
+
- Project: `<repo>/.pi/output-styles/<name>.md`
|
|
53
|
+
- Personal: `~/.pi/agent/output-styles/<name>.md`
|
|
54
|
+
|
|
55
|
+
```markdown
|
|
56
|
+
---
|
|
57
|
+
name: teacher
|
|
58
|
+
description: Teach as you go
|
|
59
|
+
---
|
|
60
|
+
Act as a patient teacher. Explain the concept before applying it.
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The body becomes the `# Personality` block.
|
|
64
|
+
|
|
65
|
+
Precedence:
|
|
66
|
+
|
|
67
|
+
- **Definitions** (low → high): bundled < user < project.
|
|
68
|
+
- **Which style is active**: session `/style` > user default > project default.
|
|
69
|
+
|
|
70
|
+
## Config
|
|
71
|
+
|
|
72
|
+
| | Path |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| Project styles | `<repo>/.pi/output-styles/` |
|
|
75
|
+
| User styles | `~/.pi/agent/output-styles/` |
|
|
76
|
+
| Project default (`--project`) | `<repo>/.pi/output-styles.json` |
|
|
77
|
+
| User default (`--save`) | `~/.pi/agent/output-styles.json` |
|
|
78
|
+
|
|
79
|
+
Environment:
|
|
80
|
+
|
|
81
|
+
- `PI_CODING_AGENT_DIR` — Pi's config-dir override. Sets the user root (default `~/.pi/agent`).
|
|
82
|
+
- `PI_OUTPUT_STYLES_HOME` — higher-precedence override of the user root, for tests and non-standard layouts.
|
|
83
|
+
|
|
84
|
+
## Develop
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
bun install
|
|
88
|
+
bun test
|
|
89
|
+
bun x tsc --noEmit
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Credits
|
|
93
|
+
|
|
94
|
+
Started as a fork of [LoneExile/pi-output-styles](https://github.com/LoneExile/pi-output-styles), reworked to use Pi's native `.pi/` directories. MIT licensed; original work © 2026 LoneExile.
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
// pi-native-output-styles — named, swappable system-prompt styles for Pi.
|
|
2
|
+
// The active style is injected as a `# Personality` block at the top of the
|
|
3
|
+
// system prompt each turn. Pure helpers are exported for unit testing.
|
|
4
|
+
|
|
5
|
+
import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
export interface Style {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
body: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type NotifyType = "info" | "warning" | "error";
|
|
17
|
+
|
|
18
|
+
interface ExtensionUI {
|
|
19
|
+
setStatus(key: string, text: string | undefined): void;
|
|
20
|
+
setWidget(key: string, lines: string[] | undefined, options?: { placement: "aboveEditor" | "belowEditor" }): void;
|
|
21
|
+
getEditorText(): string;
|
|
22
|
+
notify(message: string, type?: NotifyType): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ExtensionContext {
|
|
26
|
+
cwd: string;
|
|
27
|
+
hasUI: boolean;
|
|
28
|
+
ui: ExtensionUI;
|
|
29
|
+
setInterval?(callback: () => void, ms?: number): unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface BeforeAgentStartEvent {
|
|
33
|
+
prompt: string;
|
|
34
|
+
systemPrompt: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface BeforeAgentStartResult {
|
|
38
|
+
systemPrompt?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface AutocompleteItem {
|
|
42
|
+
value: string;
|
|
43
|
+
label: string;
|
|
44
|
+
description?: string;
|
|
45
|
+
hint?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type EventHandler<E, R = void> = (event: E, ctx: ExtensionContext) => R | void | Promise<R | void>;
|
|
49
|
+
|
|
50
|
+
interface ExtensionAPI {
|
|
51
|
+
on(event: "session_start", handler: EventHandler<unknown>): void;
|
|
52
|
+
on(event: "session_shutdown", handler: EventHandler<unknown>): void;
|
|
53
|
+
on(event: "before_agent_start", handler: EventHandler<BeforeAgentStartEvent, BeforeAgentStartResult>): void;
|
|
54
|
+
registerCommand(
|
|
55
|
+
name: string,
|
|
56
|
+
def: {
|
|
57
|
+
description: string;
|
|
58
|
+
getArgumentCompletions?: (argumentPrefix: string) => AutocompleteItem[] | null;
|
|
59
|
+
handler: (args: string, ctx: ExtensionContext) => void | Promise<void>;
|
|
60
|
+
},
|
|
61
|
+
): void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function parseStyle(text: string, fallbackName: string): Style {
|
|
65
|
+
let name = fallbackName;
|
|
66
|
+
let description = "";
|
|
67
|
+
let body = text;
|
|
68
|
+
const fm = text.match(/^---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)([\s\S]*)$/);
|
|
69
|
+
if (fm) {
|
|
70
|
+
body = fm[2];
|
|
71
|
+
for (const line of fm[1].split(/\r?\n/)) {
|
|
72
|
+
const m = line.match(/^([A-Za-z][\w-]*)\s*:\s*(.*)$/);
|
|
73
|
+
if (!m) continue;
|
|
74
|
+
const key = m[1].toLowerCase();
|
|
75
|
+
const value = m[2].trim().replace(/^(["'])([\s\S]*)\1$/, "$2");
|
|
76
|
+
if (key === "name" && value.length > 0) name = value;
|
|
77
|
+
else if (key === "description") description = value;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { name, description, body: body.trim() };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function discoverStyles(dirsLowToHigh: string[]): Map<string, Style> {
|
|
84
|
+
const styles = new Map<string, Style>();
|
|
85
|
+
for (const dir of dirsLowToHigh) {
|
|
86
|
+
let entries: string[];
|
|
87
|
+
try {
|
|
88
|
+
entries = readdirSync(dir).sort();
|
|
89
|
+
} catch {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
if (!entry.endsWith(".md")) continue;
|
|
94
|
+
let text: string;
|
|
95
|
+
try {
|
|
96
|
+
text = readFileSync(join(dir, entry), "utf8");
|
|
97
|
+
} catch {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const style = parseStyle(text, entry.slice(0, -3));
|
|
101
|
+
if (style.body.length === 0) continue;
|
|
102
|
+
styles.set(style.name, style);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return styles;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Pi config roots: `PI_CODING_AGENT_DIR` (default `~/.pi/agent`) for user scope
|
|
109
|
+
// and `<repo>/.pi` for project scope. `PI_OUTPUT_STYLES_HOME` overrides the
|
|
110
|
+
// user root and takes precedence over `PI_CODING_AGENT_DIR`.
|
|
111
|
+
export function configHome(): string {
|
|
112
|
+
return process.env.PI_OUTPUT_STYLES_HOME || process.env.PI_CODING_AGENT_DIR || join(homedir(), ".pi", "agent");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function userStylesDir(): string {
|
|
116
|
+
return join(configHome(), "output-styles");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function projectStylesDir(cwd: string): string {
|
|
120
|
+
return join(cwd, ".pi", "output-styles");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function bundledStylesDir(): string {
|
|
124
|
+
return join(dirname(fileURLToPath(import.meta.url)), "styles");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface StyleState {
|
|
128
|
+
active?: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function readState(file: string): StyleState {
|
|
132
|
+
try {
|
|
133
|
+
const parsed: unknown = JSON.parse(readFileSync(file, "utf8"));
|
|
134
|
+
if (parsed && typeof parsed === "object" && "active" in parsed && typeof parsed.active === "string") {
|
|
135
|
+
return { active: parsed.active };
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// missing or malformed → empty
|
|
139
|
+
}
|
|
140
|
+
return {};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function writeState(file: string, state: StyleState): void {
|
|
144
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
145
|
+
writeFileSync(file, JSON.stringify(state, null, 2) + "\n");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function userStateFile(): string {
|
|
149
|
+
return join(configHome(), "output-styles.json");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function projectStateFile(cwd: string): string {
|
|
153
|
+
return join(cwd, ".pi", "output-styles.json");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Session selection beats the user default, which beats the project default.
|
|
157
|
+
export function resolveActiveName(
|
|
158
|
+
sessionActive: string | null,
|
|
159
|
+
userState: StyleState,
|
|
160
|
+
projectState: StyleState,
|
|
161
|
+
): string | null {
|
|
162
|
+
return sessionActive ?? userState.active ?? projectState.active ?? null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const MARKER_PREFIX = "<!-- output-styles:";
|
|
166
|
+
|
|
167
|
+
export function styleMarker(style: Style): string {
|
|
168
|
+
return `${MARKER_PREFIX}${style.name} -->\n${style.body}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Pi's assembled system prompt is flat prose plus XML blocks — it has no
|
|
172
|
+
// personality slot. So the style becomes one, prepended to the top the way
|
|
173
|
+
// Claude Code output styles are, rather than appended after the tool list and
|
|
174
|
+
// cwd line. Idempotent: a prompt that already carries a marker is returned as-is.
|
|
175
|
+
export function applyStyle(systemPrompt: string, style: Style): string {
|
|
176
|
+
if (systemPrompt.includes(MARKER_PREFIX)) return systemPrompt;
|
|
177
|
+
const marked = styleMarker(style);
|
|
178
|
+
return systemPrompt.length === 0 ? marked : `# Personality\n${marked}\n\n${systemPrompt}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export type PersistScope = "none" | "user" | "project";
|
|
182
|
+
|
|
183
|
+
export interface StyleCommandArgs {
|
|
184
|
+
name: string | null;
|
|
185
|
+
persist: PersistScope;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Flags recognized by the /style command. parseStyleCommandArgs maps each to a
|
|
189
|
+
// persist scope; the command handler warns on any --flag NOT in this set.
|
|
190
|
+
// Keep this in sync with the flag handling in parseStyleCommandArgs.
|
|
191
|
+
const KNOWN_FLAGS = ["--save", "--global", "--project"];
|
|
192
|
+
|
|
193
|
+
// Reserved argument words that clear the active style instead of selecting one.
|
|
194
|
+
const OFF_WORDS: Record<string, true> = { off: true, none: true };
|
|
195
|
+
|
|
196
|
+
export function parseStyleCommandArgs(args: string): StyleCommandArgs {
|
|
197
|
+
const tokens = args.trim().split(/\s+/).filter(t => t.length > 0);
|
|
198
|
+
let name: string | null = null;
|
|
199
|
+
let save = false;
|
|
200
|
+
let project = false;
|
|
201
|
+
for (const t of tokens) {
|
|
202
|
+
if (t === "--save" || t === "--global") save = true;
|
|
203
|
+
else if (t === "--project") project = true;
|
|
204
|
+
else if (!t.startsWith("--") && name === null) name = t;
|
|
205
|
+
}
|
|
206
|
+
const persist: PersistScope = project ? "project" : save ? "user" : "none";
|
|
207
|
+
return { name, persist };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const STATUS_KEY = "output-styles";
|
|
211
|
+
const HINT_KEY = "output-styles-hint";
|
|
212
|
+
// Persistent ghost hint shown below the editor while a `/style` command is
|
|
213
|
+
// being composed. Pi only renders inline usage ghost text for builtin
|
|
214
|
+
// commands, so this widget carries the same message for extension commands.
|
|
215
|
+
const STYLE_HINT_LINES = [
|
|
216
|
+
"/style <name|off> [--save] [--project]",
|
|
217
|
+
"persist: --save (user default, --global alias) · --project (this project)",
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
// Pure matcher for the widget: show the hint while the input starts with a
|
|
221
|
+
// `/style` command word (line start, with optional leading whitespace).
|
|
222
|
+
export function styleHintFor(text: string): string[] | null {
|
|
223
|
+
return /^\s*\/style(?:\s|$)/.test(text) ? STYLE_HINT_LINES : null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Poller state: one started flag guards re-registration across in-process
|
|
227
|
+
// session restarts; lastHintInput dedupes widget updates against the text the
|
|
228
|
+
// hint was computed for.
|
|
229
|
+
let started = false;
|
|
230
|
+
let lastHintInput: string | null = null;
|
|
231
|
+
|
|
232
|
+
// Debounced poller: only updates the widget once the input text is stable
|
|
233
|
+
// across a tick and differs from the last-checked text. Exported for tests.
|
|
234
|
+
export function startHintPoller(ctx: ExtensionContext): void {
|
|
235
|
+
if (typeof ctx.setInterval !== "function") return;
|
|
236
|
+
let stableInput: string | null = null;
|
|
237
|
+
ctx.setInterval(() => {
|
|
238
|
+
const text = ctx.ui.getEditorText();
|
|
239
|
+
if (text !== stableInput) {
|
|
240
|
+
stableInput = text;
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const lines = styleHintFor(text);
|
|
244
|
+
if (lines !== null && lastHintInput !== text) {
|
|
245
|
+
ctx.ui.setWidget(HINT_KEY, lines, { placement: "belowEditor" });
|
|
246
|
+
lastHintInput = text;
|
|
247
|
+
} else if (lines === null && lastHintInput !== null) {
|
|
248
|
+
ctx.ui.setWidget(HINT_KEY, undefined);
|
|
249
|
+
lastHintInput = null;
|
|
250
|
+
}
|
|
251
|
+
}, 600);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Session-active style selection is process-global (module-level) state.
|
|
255
|
+
// This assumes one module instance per session/cwd, which holds under
|
|
256
|
+
// today's per-session extension loading. If Pi ever shares one module
|
|
257
|
+
// instance across multiple concurrent sessions, switch this to a
|
|
258
|
+
// cwd-keyed Map instead of a single variable.
|
|
259
|
+
type SessionSelection = { type: "inherit" } | { type: "off" } | { type: "style"; name: string };
|
|
260
|
+
let session: SessionSelection = { type: "inherit" };
|
|
261
|
+
|
|
262
|
+
function styleDirs(cwd: string): string[] {
|
|
263
|
+
// low → high precedence: bundled < user < project
|
|
264
|
+
return [bundledStylesDir(), userStylesDir(), projectStylesDir(cwd)];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Argument completions for `/style <name>`: matches style names by prefix.
|
|
268
|
+
// getArgumentCompletions carries no ctx, so discovery uses process.cwd() as the
|
|
269
|
+
// project scope (best-effort; the command handler still uses ctx.cwd).
|
|
270
|
+
// Flags advertised as dim ghost text on every completion item, so users see
|
|
271
|
+
// that a style can be persisted beyond the session with --save (user default)
|
|
272
|
+
// or --project (per-project default). --global is accepted as a --save alias.
|
|
273
|
+
const FLAG_HINT = "[--save] [--project]";
|
|
274
|
+
|
|
275
|
+
export function styleCompletions(argumentPrefix: string, cwd: string): AutocompleteItem[] | null {
|
|
276
|
+
if (argumentPrefix.includes(" ")) return null;
|
|
277
|
+
const prefix = argumentPrefix.trim().toLowerCase();
|
|
278
|
+
const styleItems: AutocompleteItem[] = [...discoverStyles(styleDirs(cwd)).values()]
|
|
279
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
280
|
+
.map(s => ({ value: s.name, label: s.name, description: s.description || undefined, hint: FLAG_HINT }));
|
|
281
|
+
const offItem: AutocompleteItem = {
|
|
282
|
+
value: "off",
|
|
283
|
+
label: "off",
|
|
284
|
+
description: "Turn off styling for this session",
|
|
285
|
+
hint: FLAG_HINT,
|
|
286
|
+
};
|
|
287
|
+
const items = [...styleItems, offItem].filter(i => i.value.toLowerCase().startsWith(prefix));
|
|
288
|
+
return items.length > 0 ? items : null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function resolveActiveStyle(cwd: string, styles?: Map<string, Style>): Style | null {
|
|
292
|
+
if (session.type === "off") return null;
|
|
293
|
+
const sessionActive = session.type === "style" ? session.name : null;
|
|
294
|
+
const name = resolveActiveName(sessionActive, readState(userStateFile()), readState(projectStateFile(cwd)));
|
|
295
|
+
if (!name) return null;
|
|
296
|
+
const map = styles ?? discoverStyles(styleDirs(cwd));
|
|
297
|
+
return map.get(name) ?? null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function refreshStatus(ctx: ExtensionContext, style: Style | null): void {
|
|
301
|
+
if (!ctx.hasUI || typeof ctx.ui.setStatus !== "function") return;
|
|
302
|
+
ctx.ui.setStatus(STATUS_KEY, style ? `style: ${style.name}` : undefined);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export default function outputStyles(pi: ExtensionAPI): void {
|
|
306
|
+
pi.on("session_start", (_event, ctx) => {
|
|
307
|
+
refreshStatus(ctx, resolveActiveStyle(ctx.cwd));
|
|
308
|
+
if (started || !ctx.hasUI) return;
|
|
309
|
+
started = true;
|
|
310
|
+
startHintPoller(ctx);
|
|
311
|
+
pi.on("session_shutdown", () => {
|
|
312
|
+
started = false;
|
|
313
|
+
lastHintInput = null;
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
318
|
+
try {
|
|
319
|
+
const style = resolveActiveStyle(ctx.cwd);
|
|
320
|
+
if (!style) {
|
|
321
|
+
refreshStatus(ctx, null);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
// Apply first; only reflect the style in the status line once the prompt
|
|
325
|
+
// was actually augmented, so a swallowed throw never advertises a style
|
|
326
|
+
// the turn did not apply.
|
|
327
|
+
const systemPrompt = applyStyle(event.systemPrompt ?? "", style);
|
|
328
|
+
refreshStatus(ctx, style);
|
|
329
|
+
return { systemPrompt };
|
|
330
|
+
} catch {
|
|
331
|
+
return; // never fail a turn over a styling concern
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
pi.registerCommand("style", {
|
|
336
|
+
description:
|
|
337
|
+
"Select an output style (injected as the # Personality block of the system prompt), or clear it. Usage: /style [name|off] [--save] [--project]",
|
|
338
|
+
getArgumentCompletions: argumentPrefix => styleCompletions(argumentPrefix, process.cwd()),
|
|
339
|
+
handler: (args, ctx) => {
|
|
340
|
+
const { name, persist } = parseStyleCommandArgs(args);
|
|
341
|
+
const styles = discoverStyles(styleDirs(ctx.cwd));
|
|
342
|
+
const available = [...styles.keys()].sort().join(", ") || "(none)";
|
|
343
|
+
|
|
344
|
+
const unknownFlags = args
|
|
345
|
+
.trim()
|
|
346
|
+
.split(/\s+/)
|
|
347
|
+
.filter(t => t.startsWith("--") && !KNOWN_FLAGS.includes(t));
|
|
348
|
+
if (unknownFlags.length > 0) {
|
|
349
|
+
ctx.ui.notify(`Ignored unknown flag(s): ${unknownFlags.join(", ")}`, "warning");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (!name) {
|
|
353
|
+
const current = resolveActiveStyle(ctx.cwd, styles);
|
|
354
|
+
const listing = [...styles.values()]
|
|
355
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
356
|
+
.map(s => (s.description ? `${s.name} — ${s.description}` : s.name))
|
|
357
|
+
.join("\n");
|
|
358
|
+
ctx.ui.notify(`Active style: ${current?.name ?? "(none)"}\nAvailable:\n${listing || "(none)"}`, "info");
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (OFF_WORDS[name.toLowerCase()]) {
|
|
362
|
+
session = { type: "off" };
|
|
363
|
+
let offScope = "this session";
|
|
364
|
+
try {
|
|
365
|
+
if (persist === "user") {
|
|
366
|
+
writeState(userStateFile(), {});
|
|
367
|
+
offScope = "cleared · user default";
|
|
368
|
+
} else if (persist === "project") {
|
|
369
|
+
writeState(projectStateFile(ctx.cwd), {});
|
|
370
|
+
offScope = "cleared · project default";
|
|
371
|
+
}
|
|
372
|
+
} catch (err) {
|
|
373
|
+
ctx.ui.notify(`Cleared for this session, but updating the saved default failed: ${String(err)}`, "warning");
|
|
374
|
+
}
|
|
375
|
+
refreshStatus(ctx, null);
|
|
376
|
+
ctx.ui.notify(`Output style off (${offScope}).`, "info");
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (!styles.has(name)) {
|
|
380
|
+
ctx.ui.notify(`Unknown style "${name}". Available: ${available}`, "error");
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
session = { type: "style", name };
|
|
385
|
+
let scope = "this session";
|
|
386
|
+
try {
|
|
387
|
+
if (persist === "user") {
|
|
388
|
+
writeState(userStateFile(), { active: name });
|
|
389
|
+
scope = "saved · user default";
|
|
390
|
+
} else if (persist === "project") {
|
|
391
|
+
writeState(projectStateFile(ctx.cwd), { active: name });
|
|
392
|
+
scope = "saved · project default";
|
|
393
|
+
}
|
|
394
|
+
} catch (err) {
|
|
395
|
+
ctx.ui.notify(`Applied for this session, but saving failed: ${String(err)}`, "warning");
|
|
396
|
+
}
|
|
397
|
+
refreshStatus(ctx, styles.get(name) ?? null);
|
|
398
|
+
ctx.ui.notify(`Output style → "${name}" (${scope}).`, "info");
|
|
399
|
+
},
|
|
400
|
+
});
|
|
401
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: concise
|
|
3
|
+
description: Minimal words; answer first, no preamble
|
|
4
|
+
---
|
|
5
|
+
Answer in the fewest words that are still correct. Lead with the conclusion, then only the essential supporting detail. No preamble, no restating the question, no summary. Prefer fragments and lists over paragraphs. Omit pleasantries.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: diagrams-first
|
|
3
|
+
description: Lead structural/flow explanations with a Mermaid diagram
|
|
4
|
+
---
|
|
5
|
+
When explaining code structure, architecture, control flow, or a request path, begin with a Mermaid diagram (```mermaid fenced block) showing the structure, then explain in prose. Use `flowchart TD` for control/data flow and `sequenceDiagram` for request/response paths. Keep diagrams under ~15 nodes; split a larger system into multiple focused diagrams.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: eli5
|
|
3
|
+
description: keep it simple pls
|
|
4
|
+
---
|
|
5
|
+
It's been a long day and my brain is fried, talk to me like I'm 5.
|
|
6
|
+
|
|
7
|
+
Small words, short sentences, short paragraphs. If you have to use a big word, explain it right after. Only return what's actually necessary.
|
|
8
|
+
|
|
9
|
+
Just tell me what you did, did it work, what do I do now.
|
|
10
|
+
|
|
11
|
+
If I have to decide something: 2 options max, the context I need to pick fast, and which one you'd go with.
|
|
12
|
+
|
|
13
|
+
Keep paths and commands exact. I have no brain cells left for the rest.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: explanatory
|
|
3
|
+
description: Complete the task, then add a short "why" insight
|
|
4
|
+
---
|
|
5
|
+
Complete the task normally. After each substantive change or decision, add a brief "Insight:" note (one or two sentences) explaining why this approach, what tradeoff it makes, or what codebase pattern it follows. Keep insights short and specific — they supplement the work, they do not replace it.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reviewer
|
|
3
|
+
description: Critical code-review lens; risks, edge cases, and quality
|
|
4
|
+
---
|
|
5
|
+
Adopt a critical code-review lens. For any code you read or write, actively surface: correctness bugs, unhandled edge cases, security and injection risks, performance concerns, and readability/maintainability issues. Call out risky assumptions explicitly. When you propose a change, note what could break and how to verify it. Prefer specific, actionable observations over general praise.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ste
|
|
3
|
+
description: Write clear prose in ASD-STE100 Simplified Technical English with action-first replies
|
|
4
|
+
---
|
|
5
|
+
Write clear prose in ASD-STE100 Simplified Technical English. Layer 1 governs chat replies, tasks, issues, pull request descriptions, commit messages, documentation, release notes, tool descriptions, error messages, and runbooks. Layer 2 applies only to chat replies addressed to a person, tasks, issues, pull request descriptions, and commit messages. Layer 1 does not govern code, identifiers, command syntax, log output, or fenced code blocks. Neither layer governs blogs, essays, video scripts, or marketing copy.
|
|
6
|
+
|
|
7
|
+
# Layer 1 — Words
|
|
8
|
+
|
|
9
|
+
Use one name for one thing. Pick one term and reuse it. Prefer short common words: start, use, help, make sure, before, after, about, get, and show. Avoid marketing adjectives. Use American spelling.
|
|
10
|
+
|
|
11
|
+
Use active voice when the actor is known. Use a verb for an action. Use simple tenses. Do not stack auxiliary verbs. Do not use an -ing main verb when a simple tense works. Avoid phrasal verbs such as "spin up" and "roll out". Replace nominalizations with verbs.
|
|
12
|
+
|
|
13
|
+
Write one instruction per sentence. Keep instructions to 20 words or fewer. Keep descriptive sentences to 25 words or fewer. Put a comma after a condition before its command. Use articles when they apply. Do not use contractions. Do not use semicolons. Write two sentences instead.
|
|
14
|
+
|
|
15
|
+
Keep one topic in each paragraph. Limit a paragraph to six sentences. Write procedures as numbered vertical lists. Put one imperative action in each item. Put a condition before the command it protects.
|
|
16
|
+
|
|
17
|
+
Use WARNING for risk of injury, CAUTION for risk of damage, and NOTE for information only. Put a warning directly before the step it protects. Start with the command or condition, then state the risk.
|
|
18
|
+
|
|
19
|
+
Use two word modes:
|
|
20
|
+
|
|
21
|
+
- STRICT applies every rule to procedures, runbooks, safety text, and error messages. Use "but" instead of "however", "because" instead of "since" for causes, "can" instead of "may", "must" instead of "should" or "shall", "use" or "with" instead of "using", "obey" instead of "follow", and "push" instead of "press" for physical controls.
|
|
22
|
+
- STE-FLAVORED applies the sentence, paragraph, tense, active-voice, and no-phrasal-verb rules to general prose. Keep natural vocabulary.
|
|
23
|
+
|
|
24
|
+
# Layer 2 — Reply shape
|
|
25
|
+
Apply this layer only to chat replies addressed to a person, tasks, issues, pull request descriptions, and commit messages.
|
|
26
|
+
|
|
27
|
+
Lead with the next action. Put a command, path, or useful snippet on the first line. Do not start with context, a plan, a recap, or an announcement of what you will do.
|
|
28
|
+
|
|
29
|
+
Number a multi-step task. Keep each step to one bounded action. Cap an action list at five items. Split a longer list into "do now" and "later", or "must" and "nice to have".
|
|
30
|
+
|
|
31
|
+
End with one action that takes less than two minutes. Name the next action if work remains. Do not add a generic offer to help.
|
|
32
|
+
|
|
33
|
+
For multi-turn work, restate the current state in every turn. Give time estimates in minutes, hours, or days. Show what now works with a concrete command, path, or result.
|
|
34
|
+
|
|
35
|
+
State errors matter-of-factly. Give the failing path, observed result, cause, and fix. Suppress tangents until the first issue is complete. Ask a separate question before starting a second issue.
|
|
36
|
+
|
|
37
|
+
Break these Layer 2 rules when the reader asks for an explanation or walkthrough, when a destructive action needs confirmation, when three debug turns failed, or when the request is genuinely ambiguous. Keep the command-first order when it still fits. Skip the Layer 2 first-line and last-line checks when an exception applies.
|
|
38
|
+
|
|
39
|
+
# Conflict rules
|
|
40
|
+
|
|
41
|
+
Keep articles in sentences. Allow terse labels in lists. Apply the five-item cap to action lists, not reference tables or rule lists. Put the command first, then connect related explanation. Keep qualifiers that bound a claim. Remove empty hedges.
|
|
42
|
+
|
|
43
|
+
# Self-check
|
|
44
|
+
|
|
45
|
+
Before sending Layer 1 text, check the sentence and paragraph limits. Replace semicolons and contractions. Change known-actor passive voice to active voice. Replace nominalizations, stacked auxiliaries, -ing main verbs, and phrasal verbs.
|
|
46
|
+
|
|
47
|
+
For Layer 2 text, check that the first line gives the next action and that the last line gives one concrete next action or ends the text. Skip this check when an exception applies.
|
|
48
|
+
|
|
49
|
+
This style fixes the form of unclear writing. It cannot make an unsupported claim true.
|
|
50
|
+
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: teacher
|
|
3
|
+
description: Teach as you go; explain concepts before applying them
|
|
4
|
+
---
|
|
5
|
+
Act as a patient teacher. Before making a non-trivial change, explain the underlying concept in one or two sentences, then make the change. Prefer showing the reasoning over just the result. When you use an unfamiliar API or pattern, say what it does and why it fits. Assume the reader wants to learn, not just receive an answer.
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-native-output-styles",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Named, swappable system-prompt styles for Pi — live /style switcher with native .pi/ directory support.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi",
|
|
8
|
+
"extension",
|
|
9
|
+
"output-styles",
|
|
10
|
+
"system-prompt",
|
|
11
|
+
"persona",
|
|
12
|
+
"personality"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "code-koan",
|
|
16
|
+
"contributors": ["LoneExile"],
|
|
17
|
+
"type": "module",
|
|
18
|
+
"homepage": "https://github.com/code-koan/pi-native-output-styles#readme",
|
|
19
|
+
"repository": { "type": "git", "url": "git+https://github.com/code-koan/pi-native-output-styles.git" },
|
|
20
|
+
"bugs": { "url": "https://github.com/code-koan/pi-native-output-styles/issues" },
|
|
21
|
+
"pi": {
|
|
22
|
+
"extensions": ["./extensions"]
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"test": "bun test"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^22.10.0",
|
|
30
|
+
"typescript": "^5.6.0"
|
|
31
|
+
},
|
|
32
|
+
"files": ["extensions", "README.md", "CHANGELOG.md", "LICENSE"]
|
|
33
|
+
}
|