pi-native-output-styles 0.5.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 +15 -0
- package/README.md +27 -2
- package/extensions/output-styles.ts +169 -22
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
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
|
+
|
|
3
18
|
## 0.5.0
|
|
4
19
|
|
|
5
20
|
One command, plus a bundled style and an agent that maintains styles.
|
package/README.md
CHANGED
|
@@ -41,7 +41,17 @@ Then start a **new** session. Extensions do not hot-reload.
|
|
|
41
41
|
/output-style 创建一个适合代码 Review 的 output style
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
|
|
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.
|
|
45
55
|
|
|
46
56
|
While composing the command, a hint line under the editor shows both forms.
|
|
47
57
|
|
|
@@ -97,12 +107,27 @@ Precedence:
|
|
|
97
107
|
|
|
98
108
|
## Config
|
|
99
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
|
+
|
|
100
125
|
| | Path |
|
|
101
126
|
| --- | --- |
|
|
102
127
|
| Project styles | `<repo>/.pi/output-styles/` |
|
|
103
128
|
| User styles | `~/.pi/agent/output-styles/` |
|
|
104
129
|
| Project default (`--project`) | `<repo>/.pi/output-styles.json` |
|
|
105
|
-
| User default (`--save`) | `~/.pi/agent/output-styles.json` |
|
|
130
|
+
| User default + config (`--save`, `config`) | `~/.pi/agent/output-styles.json` |
|
|
106
131
|
|
|
107
132
|
Environment:
|
|
108
133
|
|
|
@@ -35,6 +35,7 @@ interface ExtensionUI {
|
|
|
35
35
|
setWidget(key: string, lines: string[] | undefined, options?: { placement: "aboveEditor" | "belowEditor" }): void;
|
|
36
36
|
getEditorText(): string;
|
|
37
37
|
notify(message: string, type?: NotifyType): void;
|
|
38
|
+
select?(title: string, options: string[]): Promise<string | undefined>;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
interface ExtensionContext {
|
|
@@ -143,13 +144,25 @@ export function bundledStylesDir(): string {
|
|
|
143
144
|
|
|
144
145
|
export interface StyleState {
|
|
145
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);
|
|
146
156
|
}
|
|
147
157
|
|
|
148
158
|
export function readState(file: string): StyleState {
|
|
149
159
|
try {
|
|
150
160
|
const parsed: unknown = JSON.parse(readFileSync(file, "utf8"));
|
|
151
|
-
if (parsed && typeof parsed === "object"
|
|
152
|
-
|
|
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;
|
|
153
166
|
}
|
|
154
167
|
} catch {
|
|
155
168
|
// missing or malformed → empty
|
|
@@ -162,6 +175,13 @@ export function writeState(file: string, state: StyleState): void {
|
|
|
162
175
|
writeFileSync(file, JSON.stringify(state, null, 2) + "\n");
|
|
163
176
|
}
|
|
164
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
|
+
|
|
165
185
|
export function userStateFile(): string {
|
|
166
186
|
return join(configHome(), "output-styles.json");
|
|
167
187
|
}
|
|
@@ -179,6 +199,12 @@ export function resolveActiveName(
|
|
|
179
199
|
return sessionActive ?? userState.active ?? projectState.active ?? null;
|
|
180
200
|
}
|
|
181
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
|
+
|
|
182
208
|
const MARKER_PREFIX = "<!-- output-styles:";
|
|
183
209
|
|
|
184
210
|
export function styleMarker(style: Style): string {
|
|
@@ -224,16 +250,20 @@ export function parseStyleCommandArgs(args: string): StyleCommandArgs {
|
|
|
224
250
|
return { name, persist };
|
|
225
251
|
}
|
|
226
252
|
|
|
227
|
-
// One command serves
|
|
228
|
-
// words alone. Rule: the request is
|
|
229
|
-
//
|
|
230
|
-
// else is a task for the agent. `/output-style
|
|
231
|
-
// rewrite concise` asks the agent.
|
|
232
|
-
export type StyleCommandRoute =
|
|
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 };
|
|
233
262
|
|
|
234
263
|
export function routeStyleCommand(args: string, styleNames: Iterable<string>): StyleCommandRoute {
|
|
235
264
|
const request = args.trim();
|
|
236
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() };
|
|
237
267
|
const words = request.split(/\s+/).filter(t => t.length > 0 && !t.startsWith("--"));
|
|
238
268
|
if (words.length === 0) return { kind: "manage" }; // flags only
|
|
239
269
|
if (words.length > 1) return { kind: "task", request };
|
|
@@ -251,13 +281,14 @@ export function resolveStyleName(name: string, styleNames: Iterable<string>): st
|
|
|
251
281
|
}
|
|
252
282
|
|
|
253
283
|
const STATUS_KEY = "output-styles";
|
|
284
|
+
const INDICATOR_KEY = "output-styles-indicator";
|
|
254
285
|
const HINT_KEY = "output-styles-hint";
|
|
255
286
|
// Persistent ghost hint shown below the editor while `/output-style` is being
|
|
256
287
|
// composed. Pi only renders inline usage ghost text for builtin commands, so
|
|
257
288
|
// this widget carries the same message for extension commands.
|
|
258
289
|
const STYLE_HINT_LINES = [
|
|
259
290
|
"/output-style <name|off> [--save] [--project]",
|
|
260
|
-
"/output-style
|
|
291
|
+
"/output-style config — default style, indicator",
|
|
261
292
|
];
|
|
262
293
|
|
|
263
294
|
// Pure matcher for the widget: show the hint while the input starts with the
|
|
@@ -360,9 +391,19 @@ export function resolveActiveStyle(cwd: string, styles?: Map<string, Style>): St
|
|
|
360
391
|
return map.get(name) ?? null;
|
|
361
392
|
}
|
|
362
393
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
+
}
|
|
366
407
|
}
|
|
367
408
|
|
|
368
409
|
// The Leader brief is plain Markdown next to the extension, so its wording can
|
|
@@ -396,9 +437,104 @@ export function buildStyleTask(brief: string, cwd: string, active: Style | null,
|
|
|
396
437
|
].join("\n");
|
|
397
438
|
}
|
|
398
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");
|
|
533
|
+
}
|
|
534
|
+
|
|
399
535
|
export default function outputStyles(pi: ExtensionAPI): void {
|
|
400
536
|
pi.on("session_start", (_event, ctx) => {
|
|
401
|
-
|
|
537
|
+
renderIndicator(ctx, resolveActiveStyle(ctx.cwd));
|
|
402
538
|
if (started || !ctx.hasUI) return;
|
|
403
539
|
started = true;
|
|
404
540
|
startHintPoller(ctx);
|
|
@@ -412,14 +548,14 @@ export default function outputStyles(pi: ExtensionAPI): void {
|
|
|
412
548
|
try {
|
|
413
549
|
const style = resolveActiveStyle(ctx.cwd);
|
|
414
550
|
if (!style) {
|
|
415
|
-
|
|
551
|
+
renderIndicator(ctx, null);
|
|
416
552
|
return;
|
|
417
553
|
}
|
|
418
554
|
// Apply first; only reflect the style in the status line once the prompt
|
|
419
555
|
// was actually augmented, so a swallowed throw never advertises a style
|
|
420
556
|
// the turn did not apply.
|
|
421
557
|
const systemPrompt = applyStyle(event.systemPrompt ?? "", style);
|
|
422
|
-
|
|
558
|
+
renderIndicator(ctx, style);
|
|
423
559
|
return { systemPrompt };
|
|
424
560
|
} catch {
|
|
425
561
|
return; // never fail a turn over a styling concern
|
|
@@ -430,10 +566,21 @@ export default function outputStyles(pi: ExtensionAPI): void {
|
|
|
430
566
|
description:
|
|
431
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]",
|
|
432
568
|
getArgumentCompletions: argumentPrefix => styleCompletions(argumentPrefix, process.cwd()),
|
|
433
|
-
handler: (args, ctx) => {
|
|
569
|
+
handler: async (args, ctx) => {
|
|
434
570
|
const styles = discoverStyles(styleDirs(ctx.cwd));
|
|
435
571
|
const route = routeStyleCommand(args, styles.keys());
|
|
436
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
|
+
|
|
437
584
|
if (route.kind === "task") {
|
|
438
585
|
let task: string;
|
|
439
586
|
try {
|
|
@@ -478,16 +625,16 @@ export default function outputStyles(pi: ExtensionAPI): void {
|
|
|
478
625
|
let offScope = "this session";
|
|
479
626
|
try {
|
|
480
627
|
if (persist === "user") {
|
|
481
|
-
|
|
628
|
+
updateState(userStateFile(), { active: undefined });
|
|
482
629
|
offScope = "cleared · user default";
|
|
483
630
|
} else if (persist === "project") {
|
|
484
|
-
|
|
631
|
+
updateState(projectStateFile(ctx.cwd), { active: undefined });
|
|
485
632
|
offScope = "cleared · project default";
|
|
486
633
|
}
|
|
487
634
|
} catch (err) {
|
|
488
635
|
ctx.ui.notify(`Cleared for this session, but updating the saved default failed: ${String(err)}`, "warning");
|
|
489
636
|
}
|
|
490
|
-
|
|
637
|
+
renderIndicator(ctx, null);
|
|
491
638
|
ctx.ui.notify(`Output style off (${offScope}).`, "info");
|
|
492
639
|
return;
|
|
493
640
|
}
|
|
@@ -500,16 +647,16 @@ export default function outputStyles(pi: ExtensionAPI): void {
|
|
|
500
647
|
let scope = "this session";
|
|
501
648
|
try {
|
|
502
649
|
if (persist === "user") {
|
|
503
|
-
|
|
650
|
+
updateState(userStateFile(), { active: name });
|
|
504
651
|
scope = "saved · user default";
|
|
505
652
|
} else if (persist === "project") {
|
|
506
|
-
|
|
653
|
+
updateState(projectStateFile(ctx.cwd), { active: name });
|
|
507
654
|
scope = "saved · project default";
|
|
508
655
|
}
|
|
509
656
|
} catch (err) {
|
|
510
657
|
ctx.ui.notify(`Applied for this session, but saving failed: ${String(err)}`, "warning");
|
|
511
658
|
}
|
|
512
|
-
|
|
659
|
+
renderIndicator(ctx, styles.get(name) ?? null);
|
|
513
660
|
ctx.ui.notify(`Output style → "${name}" (${scope}).`, "info");
|
|
514
661
|
},
|
|
515
662
|
});
|