pi-native-output-styles 0.4.0 → 0.6.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 +29 -0
- package/LICENSE +2 -0
- package/README.md +68 -15
- package/extensions/output-styles.ts +292 -30
- package/extensions/prompts/output-style-leader.md +71 -0
- package/extensions/styles/caveman.md +11 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `/output-style config` for the two settings worth persisting. It runs from the user config file, so both apply to every session and project:
|
|
8
|
+
- `config default <style|off>` — the style new sessions start with.
|
|
9
|
+
- `config indicator <status|widget|off>` — where the active style is shown; `status` is the default.
|
|
10
|
+
- Run `config` with no arguments for a dialog per setting. A bare key reads the value back, an unknown key is an error, and a run without a dialog UI prints the config instead.
|
|
11
|
+
- A `widget` indicator renders the style above the editor. Every refresh writes both surfaces, so switching modes can never leave a stale badge.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- `config` joins the management words in the command router. `configure this style` still routes to the agent.
|
|
16
|
+
- Clearing a saved default now merges state instead of overwriting the file, so it no longer drops the indicator setting.
|
|
17
|
+
|
|
18
|
+
## 0.5.0
|
|
19
|
+
|
|
20
|
+
One command, plus a bundled style and an agent that maintains styles.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- `/output-style <request>` hands the task to an agent that reviews, rewrites, or creates styles. The brief is short Markdown in `extensions/prompts/output-style-leader.md`, and the agent decides for itself whether to fan work out to other agents.
|
|
25
|
+
- Bundled `caveman` style, from [carlosduplar/caveman-output-style-claude-code](https://github.com/carlosduplar/caveman-output-style-claude-code) (MIT, © 2026 Carlos Mello).
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **`/style` is gone; `/output-style` is the only command.** A single word that names a style, `off`, or `none` is style management; anything else is a request for the agent. `/output-style` with no arguments lists what exists.
|
|
30
|
+
- Style names are matched case-insensitively and resolved to their declared name.
|
|
31
|
+
|
|
3
32
|
## 0.4.0
|
|
4
33
|
|
|
5
34
|
Repackaged as `pi-native-output-styles` — a Pi-only output-style switcher built on Pi's native `.pi/` directories.
|
package/LICENSE
CHANGED
|
@@ -2,6 +2,8 @@ MIT License
|
|
|
2
2
|
|
|
3
3
|
Copyright (c) 2026 LoneExile
|
|
4
4
|
Copyright (c) 2026 code-koan contributors
|
|
5
|
+
The bundled `caveman` output style is Copyright (c) 2026 Carlos Mello
|
|
6
|
+
(https://github.com/carlosduplar/caveman-output-style-claude-code), MIT.
|
|
5
7
|
|
|
6
8
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
9
|
of this software and associated documentation files (the "Software"), to deal
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/pi-native-output-styles)
|
|
5
5
|
[](./LICENSE)
|
|
6
6
|
|
|
7
|
-
Named, swappable system-prompt styles for [Pi](https://pi.dev)
|
|
7
|
+
Named, swappable system-prompt styles for [Pi](https://pi.dev), plus an agent that can review, rewrite, and create them for you. Unlike Claude Code's output styles (which need `/clear` to switch), styles here apply and switch **live, mid-session**.
|
|
8
8
|
|
|
9
9
|
Styles and saved defaults live in Pi's own directories: `~/.pi/agent/output-styles/` and `<repo>/.pi/output-styles/`.
|
|
10
10
|
|
|
@@ -19,17 +19,41 @@ pi install git:github.com/code-koan/pi-native-output-styles
|
|
|
19
19
|
|
|
20
20
|
Then start a **new** session. Extensions do not hot-reload.
|
|
21
21
|
|
|
22
|
-
##
|
|
22
|
+
## One command
|
|
23
23
|
|
|
24
|
-
-
|
|
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.
|
|
24
|
+
`/output-style` does both jobs. What you type decides which one you get.
|
|
31
25
|
|
|
32
|
-
|
|
26
|
+
**Switch styles** — the first word is a style name, `off`, or nothing:
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
/output-style list styles and show the active one
|
|
30
|
+
/output-style caveman use caveman for this session
|
|
31
|
+
/output-style concise --save and make it your default
|
|
32
|
+
/output-style concise --project save it as this project's default
|
|
33
|
+
/output-style off clear it; --save / --project also clears the default
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Ask the agent to work on a style** — anything else:
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
/output-style review the reviewer style
|
|
40
|
+
/output-style 重写这个 output style,让它更简洁、更适合编程
|
|
41
|
+
/output-style 创建一个适合代码 Review 的 output style
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Configure it** — `/output-style config`:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
/output-style config interactive: pick the default and the indicator
|
|
48
|
+
/output-style config default caveman set the cross-session default
|
|
49
|
+
/output-style config default off clear it
|
|
50
|
+
/output-style config indicator widget where the active style shows
|
|
51
|
+
/output-style config indicator read one setting back
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The rule is one line: *`config` and a single word that names a style, `off`, or `none` are management; anything else is a request for the agent.* So `/output-style concise` activates, and `/output-style rewrite concise` asks. To review a style whose name you would otherwise activate, say more than its name.
|
|
55
|
+
|
|
56
|
+
While composing the command, a hint line under the editor shows both forms.
|
|
33
57
|
|
|
34
58
|
## How a style is applied
|
|
35
59
|
|
|
@@ -37,9 +61,23 @@ Pi's assembled system prompt is flat prose plus XML blocks — there is no perso
|
|
|
37
61
|
|
|
38
62
|
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
63
|
|
|
64
|
+
## The style agent
|
|
65
|
+
|
|
66
|
+
`/output-style <request>` hands the task to the agent with a short brief and the live picture: which style is active, both writable directories, and every style that exists with its tier and file path.
|
|
67
|
+
|
|
68
|
+
The agent owns the task end to end. It decides for itself whether the work needs other agents — a review is the usual case, where prompt quality, responsibility boundaries, and conflicts/redundancy are worth splitting across children. It uses whatever delegation tool the session has, and does the analysis itself when there is none. The final write and the summary stay with it.
|
|
69
|
+
|
|
70
|
+
Review findings are specific: conflicting instructions, duplicate or unenforceable rules, vague wording, over-constraining the model, content that belongs to a different concern, AI-slop voice, and rules that are hard to follow while actually working. Rewrites keep what works, delete what does not, and add only what is missing.
|
|
71
|
+
|
|
72
|
+
A review reports; it does not edit. The file is written only when the request asks for a change. In observed runs the agent delegated on its own when a task had several independent angles to check, and worked solo on a small single-file review.
|
|
73
|
+
|
|
74
|
+
The brief lives in [`extensions/prompts/output-style-leader.md`](extensions/prompts/output-style-leader.md) and is plain Markdown — edit it without touching code.
|
|
75
|
+
|
|
40
76
|
## Bundled styles
|
|
41
77
|
|
|
42
|
-
`concise` · `explanatory` · `teacher` · `reviewer` · `diagrams-first` · `ste` · `eli5`
|
|
78
|
+
`caveman` · `concise` · `explanatory` · `teacher` · `reviewer` · `diagrams-first` · `ste` · `eli5`
|
|
79
|
+
|
|
80
|
+
`caveman` is [Carlos Mello's Caveman output style](https://github.com/carlosduplar/caveman-output-style-claude-code) — terse replies, no filler, same technical signal. The same repo also ships a more aggressive `caveman-ultra`; it is not bundled, but you can drop it into your own `output-styles/` directory unchanged.
|
|
43
81
|
|
|
44
82
|
`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
83
|
|
|
@@ -60,21 +98,36 @@ description: Teach as you go
|
|
|
60
98
|
Act as a patient teacher. Explain the concept before applying it.
|
|
61
99
|
```
|
|
62
100
|
|
|
63
|
-
|
|
101
|
+
`name` is lowercase kebab-case, `description` is one line and shows up in `/output-style`.
|
|
64
102
|
|
|
65
103
|
Precedence:
|
|
66
104
|
|
|
67
105
|
- **Definitions** (low → high): bundled < user < project.
|
|
68
|
-
- **Which style is active**: session `/style` > user default > project default.
|
|
106
|
+
- **Which style is active**: session `/output-style` > user default > project default.
|
|
69
107
|
|
|
70
108
|
## Config
|
|
71
109
|
|
|
110
|
+
`/output-style config` holds the two things that are worth setting once. Both live in `~/.pi/agent/output-styles.json`, so they apply to every session and every project without re-stating anything.
|
|
111
|
+
|
|
112
|
+
| Key | Values | What it does |
|
|
113
|
+
| --- | --- | --- |
|
|
114
|
+
| `default` | a style name, or `off` | The style every new session starts with. Alias: `style`. |
|
|
115
|
+
| `indicator` | `status`, `widget`, `off` | Where the active style shows. Default `status`. |
|
|
116
|
+
|
|
117
|
+
A bare key reads the value back. An unknown key is an error, not a silent no-op. Running `config` with no arguments opens a dialog per setting; in a non-dialog run (print or JSON mode) it prints the current config instead.
|
|
118
|
+
|
|
119
|
+
The indicator is a display preference, so like the default style the personal setting wins over a project one, and a project setting applies when you have none.
|
|
120
|
+
|
|
121
|
+
To set a default without opening dialogs, `/output-style <name> --save` does the same thing.
|
|
122
|
+
|
|
123
|
+
## File locations
|
|
124
|
+
|
|
72
125
|
| | Path |
|
|
73
126
|
| --- | --- |
|
|
74
127
|
| Project styles | `<repo>/.pi/output-styles/` |
|
|
75
128
|
| User styles | `~/.pi/agent/output-styles/` |
|
|
76
129
|
| Project default (`--project`) | `<repo>/.pi/output-styles.json` |
|
|
77
|
-
| User default (`--save`) | `~/.pi/agent/output-styles.json` |
|
|
130
|
+
| User default + config (`--save`, `config`) | `~/.pi/agent/output-styles.json` |
|
|
78
131
|
|
|
79
132
|
Environment:
|
|
80
133
|
|
|
@@ -91,4 +144,4 @@ bun x tsc --noEmit
|
|
|
91
144
|
|
|
92
145
|
## Credits
|
|
93
146
|
|
|
94
|
-
Started as a fork of [LoneExile/pi-output-styles](https://github.com/LoneExile/pi-output-styles)
|
|
147
|
+
Started as a fork of [LoneExile/pi-output-styles](https://github.com/LoneExile/pi-output-styles). Bundles [caveman](https://github.com/carlosduplar/caveman-output-style-claude-code) by Carlos Mello. MIT licensed; original work © 2026 LoneExile, caveman style © 2026 Carlos Mello.
|
|
@@ -11,6 +11,21 @@ export interface Style {
|
|
|
11
11
|
name: string;
|
|
12
12
|
description: string;
|
|
13
13
|
body: string;
|
|
14
|
+
/** Absolute file path; set by discoverStyles. */
|
|
15
|
+
path?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Where a style definition was found. Only `project` and `user` are writable. */
|
|
19
|
+
export type StyleTier = "bundled" | "user" | "project";
|
|
20
|
+
|
|
21
|
+
export interface StyleSource {
|
|
22
|
+
dir: string;
|
|
23
|
+
tier: StyleTier;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface StyleEntry extends Style {
|
|
27
|
+
tier: StyleTier;
|
|
28
|
+
path: string;
|
|
14
29
|
}
|
|
15
30
|
|
|
16
31
|
type NotifyType = "info" | "warning" | "error";
|
|
@@ -20,11 +35,13 @@ interface ExtensionUI {
|
|
|
20
35
|
setWidget(key: string, lines: string[] | undefined, options?: { placement: "aboveEditor" | "belowEditor" }): void;
|
|
21
36
|
getEditorText(): string;
|
|
22
37
|
notify(message: string, type?: NotifyType): void;
|
|
38
|
+
select?(title: string, options: string[]): Promise<string | undefined>;
|
|
23
39
|
}
|
|
24
40
|
|
|
25
41
|
interface ExtensionContext {
|
|
26
42
|
cwd: string;
|
|
27
43
|
hasUI: boolean;
|
|
44
|
+
isIdle?(): boolean;
|
|
28
45
|
ui: ExtensionUI;
|
|
29
46
|
setInterval?(callback: () => void, ms?: number): unknown;
|
|
30
47
|
}
|
|
@@ -59,6 +76,7 @@ interface ExtensionAPI {
|
|
|
59
76
|
handler: (args: string, ctx: ExtensionContext) => void | Promise<void>;
|
|
60
77
|
},
|
|
61
78
|
): void;
|
|
79
|
+
sendUserMessage(content: string, options?: { deliverAs?: "steer" | "followUp" }): void;
|
|
62
80
|
}
|
|
63
81
|
|
|
64
82
|
export function parseStyle(text: string, fallbackName: string): Style {
|
|
@@ -99,7 +117,7 @@ export function discoverStyles(dirsLowToHigh: string[]): Map<string, Style> {
|
|
|
99
117
|
}
|
|
100
118
|
const style = parseStyle(text, entry.slice(0, -3));
|
|
101
119
|
if (style.body.length === 0) continue;
|
|
102
|
-
styles.set(style.name, style);
|
|
120
|
+
styles.set(style.name, { ...style, path: join(dir, entry) });
|
|
103
121
|
}
|
|
104
122
|
}
|
|
105
123
|
return styles;
|
|
@@ -126,13 +144,25 @@ export function bundledStylesDir(): string {
|
|
|
126
144
|
|
|
127
145
|
export interface StyleState {
|
|
128
146
|
active?: string;
|
|
147
|
+
indicator?: IndicatorMode;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Where the active style is shown. `status` is the default. */
|
|
151
|
+
export const INDICATOR_MODES = ["status", "widget", "off"] as const;
|
|
152
|
+
export type IndicatorMode = (typeof INDICATOR_MODES)[number];
|
|
153
|
+
|
|
154
|
+
export function isIndicatorMode(value: unknown): value is IndicatorMode {
|
|
155
|
+
return typeof value === "string" && (INDICATOR_MODES as readonly string[]).includes(value);
|
|
129
156
|
}
|
|
130
157
|
|
|
131
158
|
export function readState(file: string): StyleState {
|
|
132
159
|
try {
|
|
133
160
|
const parsed: unknown = JSON.parse(readFileSync(file, "utf8"));
|
|
134
|
-
if (parsed && typeof parsed === "object"
|
|
135
|
-
|
|
161
|
+
if (parsed && typeof parsed === "object") {
|
|
162
|
+
const state: StyleState = {};
|
|
163
|
+
if ("active" in parsed && typeof parsed.active === "string") state.active = parsed.active;
|
|
164
|
+
if ("indicator" in parsed && isIndicatorMode(parsed.indicator)) state.indicator = parsed.indicator;
|
|
165
|
+
return state;
|
|
136
166
|
}
|
|
137
167
|
} catch {
|
|
138
168
|
// missing or malformed → empty
|
|
@@ -145,6 +175,13 @@ export function writeState(file: string, state: StyleState): void {
|
|
|
145
175
|
writeFileSync(file, JSON.stringify(state, null, 2) + "\n");
|
|
146
176
|
}
|
|
147
177
|
|
|
178
|
+
// Merge, never replace: clearing the default must not drop the indicator
|
|
179
|
+
// setting, and setting the indicator must not drop the default. A key passed
|
|
180
|
+
// as undefined drops out of the JSON, which is how a key is cleared.
|
|
181
|
+
export function updateState(file: string, patch: StyleState): void {
|
|
182
|
+
writeState(file, { ...readState(file), ...patch });
|
|
183
|
+
}
|
|
184
|
+
|
|
148
185
|
export function userStateFile(): string {
|
|
149
186
|
return join(configHome(), "output-styles.json");
|
|
150
187
|
}
|
|
@@ -162,6 +199,12 @@ export function resolveActiveName(
|
|
|
162
199
|
return sessionActive ?? userState.active ?? projectState.active ?? null;
|
|
163
200
|
}
|
|
164
201
|
|
|
202
|
+
// The indicator is presentation, so it resolves from the same places as the
|
|
203
|
+
// default style but falls back to the status bar.
|
|
204
|
+
export function resolveIndicator(cwd: string): IndicatorMode {
|
|
205
|
+
return readState(userStateFile()).indicator ?? readState(projectStateFile(cwd)).indicator ?? "status";
|
|
206
|
+
}
|
|
207
|
+
|
|
165
208
|
const MARKER_PREFIX = "<!-- output-styles:";
|
|
166
209
|
|
|
167
210
|
export function styleMarker(style: Style): string {
|
|
@@ -207,20 +250,51 @@ export function parseStyleCommandArgs(args: string): StyleCommandArgs {
|
|
|
207
250
|
return { name, persist };
|
|
208
251
|
}
|
|
209
252
|
|
|
253
|
+
// One command serves every job, so the split has to be guessable from the
|
|
254
|
+
// words alone. Rule, in order: the request is `config` when it starts with the
|
|
255
|
+
// config word; empty, `off`, `none`, or a single word naming an existing style
|
|
256
|
+
// is style management; anything else is a task for the agent. `/output-style
|
|
257
|
+
// concise` activates; `/output-style rewrite concise` asks the agent.
|
|
258
|
+
export type StyleCommandRoute =
|
|
259
|
+
| { kind: "config"; args: string }
|
|
260
|
+
| { kind: "manage" }
|
|
261
|
+
| { kind: "task"; request: string };
|
|
262
|
+
|
|
263
|
+
export function routeStyleCommand(args: string, styleNames: Iterable<string>): StyleCommandRoute {
|
|
264
|
+
const request = args.trim();
|
|
265
|
+
if (request.length === 0) return { kind: "manage" };
|
|
266
|
+
if (/^config(?=\s|$)/i.test(request)) return { kind: "config", args: request.replace(/^config\s*/i, "").trim() };
|
|
267
|
+
const words = request.split(/\s+/).filter(t => t.length > 0 && !t.startsWith("--"));
|
|
268
|
+
if (words.length === 0) return { kind: "manage" }; // flags only
|
|
269
|
+
if (words.length > 1) return { kind: "task", request };
|
|
270
|
+
const word = words[0].toLowerCase();
|
|
271
|
+
if (OFF_WORDS[word]) return { kind: "manage" };
|
|
272
|
+
const known = new Set([...styleNames].map(n => n.toLowerCase()));
|
|
273
|
+
return known.has(word) ? { kind: "manage" } : { kind: "task", request };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Routing matches names case-insensitively, but the style map is keyed by the
|
|
277
|
+
// exact declared name, so the selected name has to be canonicalised before use.
|
|
278
|
+
export function resolveStyleName(name: string, styleNames: Iterable<string>): string | null {
|
|
279
|
+
const all = [...styleNames];
|
|
280
|
+
return all.find(n => n === name) ?? all.find(n => n.toLowerCase() === name.toLowerCase()) ?? null;
|
|
281
|
+
}
|
|
282
|
+
|
|
210
283
|
const STATUS_KEY = "output-styles";
|
|
284
|
+
const INDICATOR_KEY = "output-styles-indicator";
|
|
211
285
|
const HINT_KEY = "output-styles-hint";
|
|
212
|
-
// Persistent ghost hint shown below the editor while
|
|
213
|
-
//
|
|
214
|
-
//
|
|
286
|
+
// Persistent ghost hint shown below the editor while `/output-style` is being
|
|
287
|
+
// composed. Pi only renders inline usage ghost text for builtin commands, so
|
|
288
|
+
// this widget carries the same message for extension commands.
|
|
215
289
|
const STYLE_HINT_LINES = [
|
|
216
|
-
"/style <name|off> [--save] [--project]",
|
|
217
|
-
"
|
|
290
|
+
"/output-style <name|off> [--save] [--project]",
|
|
291
|
+
"/output-style config — default style, indicator",
|
|
218
292
|
];
|
|
219
293
|
|
|
220
|
-
// Pure matcher for the widget: show the hint while the input starts with
|
|
221
|
-
// `/style` command word (line start,
|
|
294
|
+
// Pure matcher for the widget: show the hint while the input starts with the
|
|
295
|
+
// `/output-style` command word (line start, optional leading whitespace).
|
|
222
296
|
export function styleHintFor(text: string): string[] | null {
|
|
223
|
-
return /^\s*\/style(?:\s|$)/.test(text) ? STYLE_HINT_LINES : null;
|
|
297
|
+
return /^\s*\/output-style(?:\s|$)/.test(text) ? STYLE_HINT_LINES : null;
|
|
224
298
|
}
|
|
225
299
|
|
|
226
300
|
// Poller state: one started flag guards re-registration across in-process
|
|
@@ -259,9 +333,29 @@ export function startHintPoller(ctx: ExtensionContext): void {
|
|
|
259
333
|
type SessionSelection = { type: "inherit" } | { type: "off" } | { type: "style"; name: string };
|
|
260
334
|
let session: SessionSelection = { type: "inherit" };
|
|
261
335
|
|
|
262
|
-
function
|
|
336
|
+
export function styleSources(cwd: string): StyleSource[] {
|
|
263
337
|
// low → high precedence: bundled < user < project
|
|
264
|
-
return [
|
|
338
|
+
return [
|
|
339
|
+
{ dir: bundledStylesDir(), tier: "bundled" },
|
|
340
|
+
{ dir: userStylesDir(), tier: "user" },
|
|
341
|
+
{ dir: projectStylesDir(cwd), tier: "project" },
|
|
342
|
+
];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// One row per style name: the winning definition plus the file that actually
|
|
346
|
+
// produced it, so a rewrite knows whether it may edit in place.
|
|
347
|
+
export function styleCatalog(cwd: string): StyleEntry[] {
|
|
348
|
+
const byName = new Map<string, StyleEntry>();
|
|
349
|
+
for (const { dir, tier } of styleSources(cwd)) {
|
|
350
|
+
for (const style of discoverStyles([dir]).values()) {
|
|
351
|
+
if (style.path) byName.set(style.name, { ...style, tier, path: style.path });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function styleDirs(cwd: string): string[] {
|
|
358
|
+
return styleSources(cwd).map(s => s.dir);
|
|
265
359
|
}
|
|
266
360
|
|
|
267
361
|
// Argument completions for `/style <name>`: matches style names by prefix.
|
|
@@ -297,14 +391,150 @@ export function resolveActiveStyle(cwd: string, styles?: Map<string, Style>): St
|
|
|
297
391
|
return map.get(name) ?? null;
|
|
298
392
|
}
|
|
299
393
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
394
|
+
// One renderer for every call site, so switching the mode can never leave a
|
|
395
|
+
// stale badge behind: both surfaces are written on every refresh, and only the
|
|
396
|
+
// configured one gets text.
|
|
397
|
+
function renderIndicator(ctx: ExtensionContext, style: Style | null): void {
|
|
398
|
+
if (!ctx.hasUI) return;
|
|
399
|
+
const mode = resolveIndicator(ctx.cwd);
|
|
400
|
+
const label = style ? `style: ${style.name}` : undefined;
|
|
401
|
+
if (typeof ctx.ui.setStatus === "function") {
|
|
402
|
+
ctx.ui.setStatus(STATUS_KEY, mode === "status" ? label : undefined);
|
|
403
|
+
}
|
|
404
|
+
if (typeof ctx.ui.setWidget === "function") {
|
|
405
|
+
ctx.ui.setWidget(INDICATOR_KEY, mode === "widget" && label ? [label] : undefined, { placement: "aboveEditor" });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// The Leader brief is plain Markdown next to the extension, so its wording can
|
|
410
|
+
// be edited without touching code.
|
|
411
|
+
export function leaderBrief(): string {
|
|
412
|
+
return readFileSync(join(dirname(fileURLToPath(import.meta.url)), "prompts", "output-style-leader.md"), "utf8").trim();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// The brief stays fixed and short. What changes per run is the live picture:
|
|
416
|
+
// which style is active, where styles may be written, and what already exists.
|
|
417
|
+
// The user's words are passed through untouched — this is a router, not a form.
|
|
418
|
+
export function buildStyleTask(brief: string, cwd: string, active: Style | null, request: string): string {
|
|
419
|
+
const catalog = styleCatalog(cwd);
|
|
420
|
+
return [
|
|
421
|
+
brief,
|
|
422
|
+
"",
|
|
423
|
+
"## Context",
|
|
424
|
+
"",
|
|
425
|
+
`Active style: ${active?.name ?? "(none)"}`,
|
|
426
|
+
`Project style dir: ${projectStylesDir(cwd)}`,
|
|
427
|
+
`User style dir: ${userStylesDir()}`,
|
|
428
|
+
"",
|
|
429
|
+
"Available styles:",
|
|
430
|
+
...(catalog.length
|
|
431
|
+
? catalog.map(s => `- ${s.name} [${s.tier}] ${s.path}${s.description ? ` — ${s.description}` : ""}`)
|
|
432
|
+
: ["(none)"]),
|
|
433
|
+
"",
|
|
434
|
+
"## Request",
|
|
435
|
+
"",
|
|
436
|
+
request,
|
|
437
|
+
].join("\n");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export type ConfigKey = "default" | "indicator";
|
|
441
|
+
|
|
442
|
+
export interface ConfigArgs {
|
|
443
|
+
/** null means "no key given" — open the interactive flow. */
|
|
444
|
+
key: ConfigKey | null;
|
|
445
|
+
value: string;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const CONFIG_KEY_ALIASES: Record<string, ConfigKey> = {
|
|
449
|
+
default: "default",
|
|
450
|
+
style: "default",
|
|
451
|
+
indicator: "indicator",
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
// `null` means an unrecognised key; `{key: null}` means no key at all.
|
|
455
|
+
export function parseConfigArgs(args: string): ConfigArgs | null {
|
|
456
|
+
const tokens = args.trim().split(/\s+/).filter(t => t.length > 0);
|
|
457
|
+
if (tokens.length === 0) return { key: null, value: "" };
|
|
458
|
+
const key = CONFIG_KEY_ALIASES[tokens[0].toLowerCase()];
|
|
459
|
+
if (!key) return null;
|
|
460
|
+
return { key, value: tokens.slice(1).join(" ") };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function describeConfig(cwd: string): string {
|
|
464
|
+
return `Default style (new sessions): ${readState(userStateFile()).active ?? "(none)"}\nStyle indicator: ${resolveIndicator(cwd)}`;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function applyConfigValue(key: ConfigKey, value: string, ctx: ExtensionContext, styles: Map<string, Style>): void {
|
|
468
|
+
const raw = value.trim();
|
|
469
|
+
|
|
470
|
+
if (key === "default") {
|
|
471
|
+
if (raw.length === 0) {
|
|
472
|
+
ctx.ui.notify(`Default style (new sessions): ${readState(userStateFile()).active ?? "(none)"}`, "info");
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (OFF_WORDS[raw.toLowerCase()]) {
|
|
476
|
+
updateState(userStateFile(), { active: undefined });
|
|
477
|
+
ctx.ui.notify("Default style cleared. New sessions start with no style.", "info");
|
|
478
|
+
renderIndicator(ctx, resolveActiveStyle(ctx.cwd, styles));
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const name = resolveStyleName(raw, styles.keys());
|
|
482
|
+
if (!name) {
|
|
483
|
+
ctx.ui.notify(`Unknown style "${raw}". Available: ${[...styles.keys()].sort().join(", ") || "(none)"}`, "error");
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
updateState(userStateFile(), { active: name });
|
|
487
|
+
ctx.ui.notify(`Default style → "${name}" for every new session and project.`, "info");
|
|
488
|
+
renderIndicator(ctx, resolveActiveStyle(ctx.cwd, styles));
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (raw.length === 0) {
|
|
493
|
+
ctx.ui.notify(`Style indicator: ${resolveIndicator(ctx.cwd)}`, "info");
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (!isIndicatorMode(raw)) {
|
|
497
|
+
ctx.ui.notify(`Indicator must be one of: ${INDICATOR_MODES.join(", ")}`, "error");
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
updateState(userStateFile(), { indicator: raw });
|
|
501
|
+
ctx.ui.notify(`Style indicator → ${raw}`, "info");
|
|
502
|
+
renderIndicator(ctx, resolveActiveStyle(ctx.cwd, styles));
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Interactive path: two dialogs, then a summary. Falls back to printing when
|
|
506
|
+
// the run has no dialog-capable UI (print/json modes).
|
|
507
|
+
async function openConfigDialogs(ctx: ExtensionContext, styles: Map<string, Style>): Promise<void> {
|
|
508
|
+
if (typeof ctx.ui.select !== "function") {
|
|
509
|
+
ctx.ui.notify(describeConfig(ctx.cwd), "info");
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const current = readState(userStateFile()).active ?? "(none)";
|
|
514
|
+
const chosen = await ctx.ui.select(`Default output style for new sessions (now: ${current})`, [
|
|
515
|
+
"(keep current)",
|
|
516
|
+
"(none)",
|
|
517
|
+
...[...styles.keys()].sort(),
|
|
518
|
+
]);
|
|
519
|
+
if (chosen !== undefined && chosen !== "(keep current)") {
|
|
520
|
+
updateState(userStateFile(), { active: chosen === "(none)" ? undefined : chosen });
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const mode = await ctx.ui.select(`Style indicator (now: ${resolveIndicator(ctx.cwd)})`, [
|
|
524
|
+
"(keep current)",
|
|
525
|
+
...INDICATOR_MODES,
|
|
526
|
+
]);
|
|
527
|
+
if (mode !== undefined && mode !== "(keep current)" && isIndicatorMode(mode)) {
|
|
528
|
+
updateState(userStateFile(), { indicator: mode });
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
renderIndicator(ctx, resolveActiveStyle(ctx.cwd, styles));
|
|
532
|
+
ctx.ui.notify(`Saved.\n${describeConfig(ctx.cwd)}`, "info");
|
|
303
533
|
}
|
|
304
534
|
|
|
305
535
|
export default function outputStyles(pi: ExtensionAPI): void {
|
|
306
536
|
pi.on("session_start", (_event, ctx) => {
|
|
307
|
-
|
|
537
|
+
renderIndicator(ctx, resolveActiveStyle(ctx.cwd));
|
|
308
538
|
if (started || !ctx.hasUI) return;
|
|
309
539
|
started = true;
|
|
310
540
|
startHintPoller(ctx);
|
|
@@ -318,27 +548,55 @@ export default function outputStyles(pi: ExtensionAPI): void {
|
|
|
318
548
|
try {
|
|
319
549
|
const style = resolveActiveStyle(ctx.cwd);
|
|
320
550
|
if (!style) {
|
|
321
|
-
|
|
551
|
+
renderIndicator(ctx, null);
|
|
322
552
|
return;
|
|
323
553
|
}
|
|
324
554
|
// Apply first; only reflect the style in the status line once the prompt
|
|
325
555
|
// was actually augmented, so a swallowed throw never advertises a style
|
|
326
556
|
// the turn did not apply.
|
|
327
557
|
const systemPrompt = applyStyle(event.systemPrompt ?? "", style);
|
|
328
|
-
|
|
558
|
+
renderIndicator(ctx, style);
|
|
329
559
|
return { systemPrompt };
|
|
330
560
|
} catch {
|
|
331
561
|
return; // never fail a turn over a styling concern
|
|
332
562
|
}
|
|
333
563
|
});
|
|
334
564
|
|
|
335
|
-
pi.registerCommand("style", {
|
|
565
|
+
pi.registerCommand("output-style", {
|
|
336
566
|
description:
|
|
337
|
-
"Select an output style
|
|
567
|
+
"Select an output style, or ask the agent to review, rewrite, or create one. Usage: /output-style <name|off|what you want> [--save] [--project]",
|
|
338
568
|
getArgumentCompletions: argumentPrefix => styleCompletions(argumentPrefix, process.cwd()),
|
|
339
|
-
handler: (args, ctx) => {
|
|
340
|
-
const { name, persist } = parseStyleCommandArgs(args);
|
|
569
|
+
handler: async (args, ctx) => {
|
|
341
570
|
const styles = discoverStyles(styleDirs(ctx.cwd));
|
|
571
|
+
const route = routeStyleCommand(args, styles.keys());
|
|
572
|
+
|
|
573
|
+
if (route.kind === "config") {
|
|
574
|
+
const parsed = parseConfigArgs(route.args);
|
|
575
|
+
if (parsed === null) {
|
|
576
|
+
ctx.ui.notify("Config keys: default <style|off>, indicator <status|widget|off>", "error");
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (parsed.key === null) await openConfigDialogs(ctx, styles);
|
|
580
|
+
else applyConfigValue(parsed.key, parsed.value, ctx, styles);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (route.kind === "task") {
|
|
585
|
+
let task: string;
|
|
586
|
+
try {
|
|
587
|
+
task = buildStyleTask(leaderBrief(), ctx.cwd, resolveActiveStyle(ctx.cwd, styles), route.request);
|
|
588
|
+
} catch (err) {
|
|
589
|
+
ctx.ui.notify(`Could not build the output-style task: ${String(err)}`, "error");
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
// Mid-stream the delivery mode is required; otherwise the message goes
|
|
593
|
+
// out immediately and triggers the turn.
|
|
594
|
+
pi.sendUserMessage(task, ctx.isIdle?.() === false ? { deliverAs: "followUp" } : undefined);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const { name: requested, persist } = parseStyleCommandArgs(args);
|
|
599
|
+
const name = requested === null ? null : (resolveStyleName(requested, styles.keys()) ?? requested);
|
|
342
600
|
const available = [...styles.keys()].sort().join(", ") || "(none)";
|
|
343
601
|
|
|
344
602
|
const unknownFlags = args
|
|
@@ -355,7 +613,11 @@ export default function outputStyles(pi: ExtensionAPI): void {
|
|
|
355
613
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
356
614
|
.map(s => (s.description ? `${s.name} — ${s.description}` : s.name))
|
|
357
615
|
.join("\n");
|
|
358
|
-
ctx.ui.notify(
|
|
616
|
+
ctx.ui.notify(
|
|
617
|
+
`Active style: ${current?.name ?? "(none)"}\nAvailable:\n${listing || "(none)"}\n\n` +
|
|
618
|
+
"/output-style <name|off> [--save] [--project] to switch, or /output-style <request> to have the agent review, rewrite, or create a style.",
|
|
619
|
+
"info",
|
|
620
|
+
);
|
|
359
621
|
return;
|
|
360
622
|
}
|
|
361
623
|
if (OFF_WORDS[name.toLowerCase()]) {
|
|
@@ -363,16 +625,16 @@ export default function outputStyles(pi: ExtensionAPI): void {
|
|
|
363
625
|
let offScope = "this session";
|
|
364
626
|
try {
|
|
365
627
|
if (persist === "user") {
|
|
366
|
-
|
|
628
|
+
updateState(userStateFile(), { active: undefined });
|
|
367
629
|
offScope = "cleared · user default";
|
|
368
630
|
} else if (persist === "project") {
|
|
369
|
-
|
|
631
|
+
updateState(projectStateFile(ctx.cwd), { active: undefined });
|
|
370
632
|
offScope = "cleared · project default";
|
|
371
633
|
}
|
|
372
634
|
} catch (err) {
|
|
373
635
|
ctx.ui.notify(`Cleared for this session, but updating the saved default failed: ${String(err)}`, "warning");
|
|
374
636
|
}
|
|
375
|
-
|
|
637
|
+
renderIndicator(ctx, null);
|
|
376
638
|
ctx.ui.notify(`Output style off (${offScope}).`, "info");
|
|
377
639
|
return;
|
|
378
640
|
}
|
|
@@ -385,16 +647,16 @@ export default function outputStyles(pi: ExtensionAPI): void {
|
|
|
385
647
|
let scope = "this session";
|
|
386
648
|
try {
|
|
387
649
|
if (persist === "user") {
|
|
388
|
-
|
|
650
|
+
updateState(userStateFile(), { active: name });
|
|
389
651
|
scope = "saved · user default";
|
|
390
652
|
} else if (persist === "project") {
|
|
391
|
-
|
|
653
|
+
updateState(projectStateFile(ctx.cwd), { active: name });
|
|
392
654
|
scope = "saved · project default";
|
|
393
655
|
}
|
|
394
656
|
} catch (err) {
|
|
395
657
|
ctx.ui.notify(`Applied for this session, but saving failed: ${String(err)}`, "warning");
|
|
396
658
|
}
|
|
397
|
-
|
|
659
|
+
renderIndicator(ctx, styles.get(name) ?? null);
|
|
398
660
|
ctx.ui.notify(`Output style → "${name}" (${scope}).`, "info");
|
|
399
661
|
},
|
|
400
662
|
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Output style task
|
|
2
|
+
|
|
3
|
+
You own this end to end: read the request, decide what work it needs, do it,
|
|
4
|
+
then verify what you produced. Finish with a short report of what changed and
|
|
5
|
+
where.
|
|
6
|
+
|
|
7
|
+
## What an output style is
|
|
8
|
+
|
|
9
|
+
A style defines **how** you behave: voice, structure, level of detail,
|
|
10
|
+
interaction style. It is not a home for project architecture, coding rules,
|
|
11
|
+
domain knowledge, tooling conventions, or repo facts. Anything like that is a
|
|
12
|
+
responsibility-boundary bug: drop it, or say where it belongs instead.
|
|
13
|
+
|
|
14
|
+
## Delegation
|
|
15
|
+
|
|
16
|
+
Decide for yourself whether the task needs other agents. Delegate only when
|
|
17
|
+
independent angles genuinely improve the result. A review is the usual case:
|
|
18
|
+
prompt quality, responsibility boundaries, and conflicts/redundancy are worth
|
|
19
|
+
splitting. Use the `subagent` tool when it is available, and run the children
|
|
20
|
+
in one workflow call. If it is not available, do the analysis yourself.
|
|
21
|
+
Children report findings back to you and never write files. You do the final
|
|
22
|
+
write and the final summary — never hand the file off.
|
|
23
|
+
|
|
24
|
+
## Reviewing
|
|
25
|
+
|
|
26
|
+
Review means report, not edit. Do not change the file unless the request also
|
|
27
|
+
asks for the change; offer the fix instead.
|
|
28
|
+
|
|
29
|
+
Name each finding specifically: what is wrong, what it costs, and the fix.
|
|
30
|
+
Look for:
|
|
31
|
+
|
|
32
|
+
- instructions that conflict with each other
|
|
33
|
+
- duplicate, dead, or unenforceable rules
|
|
34
|
+
- vague wording that cannot be acted on
|
|
35
|
+
- over-constraining the model
|
|
36
|
+
- content that belongs to another concern
|
|
37
|
+
- AI-slop voice: inflated claims, filler, ceremony
|
|
38
|
+
- rules that are hard to follow while actually working
|
|
39
|
+
|
|
40
|
+
No praise padding, and do not restate the style back at the user.
|
|
41
|
+
|
|
42
|
+
## Rewriting
|
|
43
|
+
|
|
44
|
+
Keep what works, delete what does not, add only what is missing. Fewer sharp
|
|
45
|
+
rules beat more rules. Do not add text to look thorough, and do not quietly
|
|
46
|
+
widen the scope.
|
|
47
|
+
|
|
48
|
+
## Creating
|
|
49
|
+
|
|
50
|
+
Write the smallest style that achieves the goal. `name` is lowercase
|
|
51
|
+
kebab-case; `description` is one line and shows up in `/style`.
|
|
52
|
+
|
|
53
|
+
## File format
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
name: <kebab-case>
|
|
57
|
+
description: <one line>
|
|
58
|
+
---
|
|
59
|
+
<style body>
|
|
60
|
+
|
|
61
|
+
## Where to write
|
|
62
|
+
|
|
63
|
+
Default to the project directory. Write to the user directory only when asked
|
|
64
|
+
for personal or global. Never edit a style under a package install path — those
|
|
65
|
+
are read-only bundled styles; copy one out instead.
|
|
66
|
+
|
|
67
|
+
## Before you finish
|
|
68
|
+
|
|
69
|
+
- Re-read what you wrote: frontmatter plus body, nothing else.
|
|
70
|
+
- Confirm the file parses and the `name` matches what `/style <name>` expects.
|
|
71
|
+
- State the path you wrote and what changed.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: caveman
|
|
3
|
+
description: Ultra-compact replies. Less fluff. Same technical signal.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Smart caveman style: terse responses, keep technical substance, drop ALL fluff.
|
|
7
|
+
Lead with answer. Drop articles, filler, pleasantries, hedging, preamble, recap.
|
|
8
|
+
Fragments OK. Pattern: `[thing] [action] [reason]. [next step].`
|
|
9
|
+
Keep technical terms, code, commands, paths, JSON, errors unchanged.
|
|
10
|
+
Bullets/tables only when scan > prose.
|
|
11
|
+
Normal language for safety, irreversible actions, or confusion risk.
|