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 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
  [![npm version](https://img.shields.io/npm/v/pi-native-output-styles.svg)](https://www.npmjs.com/package/pi-native-output-styles)
5
5
  [![license](https://img.shields.io/npm/l/pi-native-output-styles.svg)](./LICENSE)
6
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**.
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
- ## Use
22
+ ## One command
23
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.
24
+ `/output-style` does both jobs. What you type decides which one you get.
31
25
 
32
- The style is applied every turn, and the status line shows `style: eli5`.
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
- The body becomes the `# Personality` block.
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), reworked to use Pi's native `.pi/` directories. MIT licensed; original work © 2026 LoneExile.
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" && "active" in parsed && typeof parsed.active === "string") {
135
- return { active: parsed.active };
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 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.
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
- "persist: --save (user default, --global alias) · --project (this project)",
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 a
221
- // `/style` command word (line start, with optional leading whitespace).
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 styleDirs(cwd: string): string[] {
336
+ export function styleSources(cwd: string): StyleSource[] {
263
337
  // low → high precedence: bundled < user < project
264
- return [bundledStylesDir(), userStylesDir(), projectStylesDir(cwd)];
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
- 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);
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
- refreshStatus(ctx, resolveActiveStyle(ctx.cwd));
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
- refreshStatus(ctx, null);
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
- refreshStatus(ctx, style);
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 (injected as the # Personality block of the system prompt), or clear it. Usage: /style [name|off] [--save] [--project]",
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(`Active style: ${current?.name ?? "(none)"}\nAvailable:\n${listing || "(none)"}`, "info");
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
- writeState(userStateFile(), {});
628
+ updateState(userStateFile(), { active: undefined });
367
629
  offScope = "cleared · user default";
368
630
  } else if (persist === "project") {
369
- writeState(projectStateFile(ctx.cwd), {});
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
- refreshStatus(ctx, null);
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
- writeState(userStateFile(), { active: name });
650
+ updateState(userStateFile(), { active: name });
389
651
  scope = "saved · user default";
390
652
  } else if (persist === "project") {
391
- writeState(projectStateFile(ctx.cwd), { active: name });
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
- refreshStatus(ctx, styles.get(name) ?? null);
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-native-output-styles",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Named, swappable system-prompt styles for Pi — live /style switcher with native .pi/ directory support.",
5
5
  "keywords": [
6
6
  "pi-package",