pi-minimalist 0.2.0 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,11 +14,11 @@ A quieter transcript for [Pi](https://pi.dev): turn bulky tool calls into one-li
14
14
 
15
15
  <img width="699" height="50" alt="after" src="https://github.com/user-attachments/assets/c0e8bca4-afa7-42c1-9bf0-69f267d18d9f" />
16
16
 
17
- **Click** a folded line to open it into separate rows. Click one row to see only its output.
17
+ **Click** a folded line to open it into separate rows. Click one row to see only its output. Click the `▾ Expanded · click to fold` line above an opened run to fold it back.
18
18
 
19
19
  <img src="docs/click.png" width="600" alt="Clicking a folded line opens it into rows; clicking one row expands only that row">
20
20
 
21
- Press **Ctrl+O** to expand tool output—including diffs and syntax highlighting. Nothing is removed from the session or hidden from the model; only the display changes.
21
+ Press **Ctrl+O** to expand tool output—including diffs and syntax highlighting. Expanded rows also wrap long commands instead of cutting them off. Nothing is removed from the session or hidden from the model; only the display changes.
22
22
 
23
23
  ## Install
24
24
 
@@ -72,7 +72,7 @@ Settings apply to existing conversation history and are saved globally. If a sym
72
72
 
73
73
  Turn off **Compact tool rows** to restore native tool cards. Or choose the `off` preset.
74
74
 
75
- To disable the extension completely, run `pi config`, disable pi-minimalist, and restart Pi. To uninstall:
75
+ To disable the extension completely, run `pi config`, disable pi-minimalist, and restart Pi
76
76
 
77
77
  ## Compatibility
78
78
 
package/index.ts CHANGED
@@ -23,7 +23,7 @@ import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
23
23
  import { SettingsList } from "@earendil-works/pi-tui";
24
24
  import { installBridges, sharedState } from "./src/bridge.ts";
25
25
  import { type BasicKey, type BasicSettings, type Config } from "./src/config.ts";
26
- import { customSeed, loadSettings, migratedQuiet, saveBasicSettings } from "./src/config-file.ts";
26
+ import { customSeed, loadSettings, saveBasicSettings } from "./src/config-file.ts";
27
27
  import { argumentCompletions, createConfigScreen, summary } from "./src/config-ui.ts";
28
28
  import { patchCore } from "./src/core-patch.ts";
29
29
  import { refreshMcpTools } from "./src/tools.ts";
@@ -34,15 +34,8 @@ export default function (pi: ExtensionAPI) {
34
34
  // strand every row on stale state.
35
35
  const { config, grouping } = sharedState(loadSettings());
36
36
 
37
- // One-time migration of the preference written by the removed `/quiet`
38
- // command. A session override, so the per-turn re-read below cannot revert it
39
- // before the user has saved anything.
40
- const migrated = migratedQuiet();
41
- if (migrated !== undefined) config.setSessionOverride("groupToolRuns", migrated);
42
-
43
37
  installBridges({ config, grouping });
44
- // Runtime replacement for editing Pi's compiled bundle. Idempotent, so a
45
- // /reload only refreshes the bridge slots the wrappers read.
38
+ // Idempotent, so a /reload only refreshes the Bridge the wrappers read.
46
39
  patchCore(core);
47
40
 
48
41
  const refresh = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-minimalist",
3
- "version": "0.2.0",
3
+ "version": "0.2.3",
4
4
  "description": "Compact one-line tool and thinking rows for Pi's interactive transcript",
5
5
  "keywords": ["pi-package"],
6
6
  "license": "MIT",
@@ -16,6 +16,7 @@
16
16
  "@earendil-works/pi-tui": "*"
17
17
  },
18
18
  "pi": {
19
- "extensions": ["./index.ts"]
19
+ "extensions": ["./index.ts"],
20
+ "image": "https://raw.githubusercontent.com/EviHex/pi-minimalist/main/docs/click.png"
20
21
  }
21
22
  }
package/src/bridge.ts CHANGED
@@ -2,56 +2,72 @@
2
2
  * The boundary between this extension and Pi's core components.
3
3
  *
4
4
  * Nothing on disk is patched. `core-patch.ts` wraps Pi's real component
5
- * prototypes at load time and READS the process-global slots below at CALL time,
6
- * so `/reload` swaps behavior without re-wrapping anything, and an unloaded
7
- * extension degrades to Pi's native rendering.
5
+ * prototypes at load time and READS ONE process-global object, the `Bridge`
6
+ * below, at CALL time. Every install replaces that object as a whole, so
7
+ * `/reload` swaps behavior without re-wrapping anything, and an unloaded
8
+ * extension (no object) degrades to Pi's native rendering.
8
9
  *
9
- * SINGLE SOURCE OF TRUTH for the symbol names, shared by the wrappers, the state
10
- * that must survive `/reload`, and the tests.
10
+ * SINGLE SOURCE OF TRUTH for the global key names, shared by the wrappers, the
11
+ * state that must survive `/reload`, and the tests.
11
12
  */
12
13
 
13
14
  import { Config } from "./config.ts";
14
15
  import { RunGrouping } from "./run-grouping.ts";
15
- import { Painter, type ThemeLike } from "./row.ts";
16
- import { createToolRenderer } from "./tool-renderer.ts";
17
- import { createThinkingPreview, singleHueThinkingTheme } from "./thinking.ts";
16
+ import { Painter, type Row, type ThemeLike } from "./row.ts";
17
+ import { createToolRenderer, type ToolRenderer } from "./tool-renderer.ts";
18
+ import { createThinkingPreview, singleHueThinkingTheme, type ThinkingPreview } from "./thinking.ts";
18
19
  import type { Timers } from "./components.ts";
19
20
 
20
21
  export const BRIDGE_SYMBOLS = {
21
- /** `{ renderShell, handles, renderCall, renderResult }` for every tool row. */
22
- toolRenderer: "pi.defaultToolRenderer",
23
- /** `(text, theme, pad, streaming, owner, runIndex) => Component`. */
24
- thinkingPreview: "pi.thinkingPreview",
25
- /** `(markdownTheme, theme) => markdownTheme` for expanded thinking. */
26
- thinkingMarkdownTheme: "pi.thinkingMarkdownTheme",
27
-
22
+ /** The `Bridge` object: everything the prototype wrappers call. */
23
+ bridge: "pi.minimalist.bridge",
28
24
  /** Live settings, retained across a /reload. */
29
25
  config: "pi.minimalist.config",
30
26
  /** Run-grouping state, retained across a /reload. */
31
27
  grouping: "pi.minimalist.grouping",
28
+ } as const;
29
+
30
+ /**
31
+ * What the prototype wrappers may call. Functions, not values: the wrappers read
32
+ * the object on every call, so a settings toggle takes effect without
33
+ * reinstalling anything.
34
+ */
35
+ export type Bridge = {
36
+ /** `{ renderShell, handles, renderCall, renderResult }` for every tool row. */
37
+ toolRenderer: ToolRenderer;
38
+ thinkingPreview: ThinkingPreview;
39
+ /** Expanded thinking: the Markdown theme with every token in the thinking hue. */
40
+ thinkingMarkdownTheme: (base: Record<string, unknown>, theme: ThemeLike) => Record<string, unknown>;
41
+ /** `thinkingAsToolCall`: do collapsed/expanded thinking rows belong to us? */
42
+ compactThinking: () => boolean;
43
+ /** Keep a STREAMING thinking block expanded. */
44
+ keepActiveThinkingExpanded: () => boolean;
32
45
 
33
46
  /** Chronology hooks that keep thinking and prose in transcript order. */
34
- observeThinking: "pi.minimalist.observeThinking",
35
- observeProse: "pi.minimalist.observeProse",
47
+ observeThinking: (owner: object, runIndex: number, streaming: boolean, hidden: boolean, timestamp?: number) => string;
48
+ observeProse: RunGrouping["observeProse"];
49
+ /** A visible row we do not fold (user message, excluded tool); it cuts runs. */
50
+ observeBarrier: (id: string) => void;
36
51
  /** `undefined` = native prose, `null` = hidden, Row = folded summary. */
37
- proseView: "pi.minimalist.proseView",
38
- /** `(toolCallId) => boolean`: omit core's separator before an activity summary. */
39
- activitySummaryRow: "pi.minimalist.activitySummaryRow",
40
- /** `(owner) => normal|hidden|summary`: normalize assistant host spacing. */
41
- activityMessageView: "pi.minimalist.activityMessageView",
52
+ proseView: (id: string, theme: ThemeLike) => Row | null | undefined;
53
+ /** Header of an opened run, drawn by a prose/native-thinking host. */
54
+ proseHeader: (id: string, theme: ThemeLike) => Row | undefined;
55
+ /** A click on a summary or header; true when handled. */
56
+ click: (id: string, onHeader: boolean) => boolean;
57
+ /** Omit core's separator before an activity summary. */
58
+ activitySummaryRow: (id: string) => boolean;
59
+ /** Normalize assistant host spacing. */
60
+ activityMessageView: (owner: object) => "normal" | "hidden" | "summary";
42
61
  /**
43
62
  * Spacer suppression for a fully hidden assistant message.
44
63
  *
45
64
  * There is deliberately NO tool-row equivalent: a compact row uses
46
65
  * `renderShell: "self"`, and core's self-shell branch emits its separator
47
66
  * inline and returns ZERO lines when the row renders nothing — spacer
48
- * included. An earlier `quietSpacer` bridge was compensating for a problem core
49
- * already handles.
67
+ * included.
50
68
  */
51
- messageSpacer: "pi.minimalist.messageSpacer",
52
- /** `() => boolean`: keep a STREAMING thinking block expanded. */
53
- keepActiveThinkingExpanded: "pi.minimalist.keepActiveThinkingExpanded",
54
- } as const;
69
+ messageSpacer: (owner: object) => boolean;
70
+ };
55
71
 
56
72
  type Globals = Record<symbol, unknown>;
57
73
 
@@ -59,6 +75,11 @@ function slot(key: keyof typeof BRIDGE_SYMBOLS): symbol {
59
75
  return Symbol.for(BRIDGE_SYMBOLS[key]);
60
76
  }
61
77
 
78
+ /** The installed bridge, or `undefined` when the extension is not loaded. */
79
+ export function readBridge(): Bridge | undefined {
80
+ return (globalThis as Globals)[slot("bridge")] as Bridge | undefined;
81
+ }
82
+
62
83
  /**
63
84
  * State that must survive `/reload`.
64
85
  *
@@ -85,40 +106,40 @@ export type InstallOptions = SharedState & {
85
106
  };
86
107
 
87
108
  /**
88
- * Install every bridge.
109
+ * Install the bridge.
89
110
  *
90
- * Each `/reload` overwrites these slots with fresh instances. Do NOT clear them
111
+ * Each `/reload` overwrites the slots with fresh instances. Do NOT clear them
91
112
  * from `session_shutdown`: reload ordering can let an old shutdown hook erase the
92
- * newly installed bridges. Process exit clears globalThis naturally.
113
+ * newly installed bridge. Process exit clears globalThis naturally.
93
114
  */
94
115
  export function installBridges({ config, grouping, timers }: InstallOptions): void {
95
116
  const globals = globalThis as Globals;
117
+ const painter = (theme: ThemeLike) => new Painter(theme, config);
96
118
 
97
- globals[slot("config")] = config;
98
- globals[slot("grouping")] = grouping;
99
- globals[slot("toolRenderer")] = createToolRenderer({ config, grouping, timers });
100
- globals[slot("thinkingPreview")] = createThinkingPreview({ config, grouping, timers });
101
- globals[slot("thinkingMarkdownTheme")] = (base: Record<string, unknown>, theme: never) =>
102
- singleHueThinkingTheme(base, theme, config);
119
+ const bridge: Bridge = {
120
+ toolRenderer: createToolRenderer({ config, grouping, timers }),
121
+ thinkingPreview: createThinkingPreview({ config, grouping, timers }),
122
+ thinkingMarkdownTheme: (base, theme) => singleHueThinkingTheme(base, theme, config),
123
+ compactThinking: () => config.get("thinkingAsToolCall") === true,
124
+ keepActiveThinkingExpanded: () => config.get("keepActiveThinkingExpanded"),
103
125
 
104
- // Chronology: core walks message content in transcript order, and run folding
105
- // depends on that order.
106
- globals[slot("observeThinking")] = (owner: object, runIndex: number, streaming: boolean, hidden: boolean) => {
107
- grouping.observeThinking(owner, runIndex, !streaming, !hidden);
108
- return grouping.thinkingId(owner, runIndex);
126
+ // Chronology: core walks message content in transcript order, and run
127
+ // folding depends on that order.
128
+ observeThinking: (owner, runIndex, streaming, hidden, timestamp) => {
129
+ grouping.observeThinking(owner, runIndex, !streaming, !hidden, timestamp);
130
+ return grouping.thinkingId(owner, runIndex);
131
+ },
132
+ observeProse: (owner, contentIndex, signal) => grouping.observeProse(owner, contentIndex, signal),
133
+ observeBarrier: (id) => grouping.observeBarrier(id),
134
+ proseView: (id, theme) => grouping.proseView(id, painter(theme)),
135
+ proseHeader: (id, theme) => grouping.headerFor(id, painter(theme)),
136
+ click: (id, onHeader) => grouping.click(id, onHeader),
137
+ activitySummaryRow: (id) => grouping.isActivitySummary(id),
138
+ activityMessageView: (owner) => grouping.activityMessageView(owner),
139
+ messageSpacer: (owner) => grouping.showsMessageSpacer(owner),
109
140
  };
110
- globals[slot("observeProse")] = (
111
- owner: object,
112
- contentIndex: number,
113
- signal: Parameters<RunGrouping["observeProse"]>[2],
114
- ) => grouping.observeProse(owner, contentIndex, signal);
115
- globals[slot("proseView")] = (id: string, theme: ThemeLike) =>
116
- grouping.proseView(id, new Painter(theme, config));
117
- globals[slot("activitySummaryRow")] = (id: string) => grouping.isActivitySummary(id);
118
- globals[slot("activityMessageView")] = (owner: object) => grouping.activityMessageView(owner);
119
- globals[slot("messageSpacer")] = (owner: object) => grouping.showsMessageSpacer(owner);
120
141
 
121
- // A function, not a value: the prototype wrapper reads it on every call, so a
122
- // toggle takes effect without reinstalling anything.
123
- globals[slot("keepActiveThinkingExpanded")] = () => config.get("keepActiveThinkingExpanded");
142
+ globals[slot("config")] = config;
143
+ globals[slot("grouping")] = grouping;
144
+ globals[slot("bridge")] = bridge;
124
145
  }
package/src/components.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * production components without loading an extension runtime.
7
7
  */
8
8
 
9
- import { truncateToWidth } from "@earendil-works/pi-tui";
9
+ import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
10
10
  import type { Component, TuiMouseEvent, TuiMouseEventResult } from "@earendil-works/pi-tui";
11
11
  import { gutterWidth, type Row } from "./row.ts";
12
12
 
@@ -40,17 +40,57 @@ export const realTimers: Timers = {
40
40
  */
41
41
  export type ResolveRow = (width: number) => Row | null;
42
42
 
43
+ /** Columns the details must keep on each line before wrapping is worth it. */
44
+ const MIN_WRAP_COLUMNS = 10;
45
+
46
+ /**
47
+ * An expanded row's details wrapped under themselves (a hanging indent below
48
+ * `glyph label`), or undefined when the terminal is too narrow to wrap
49
+ * usefully: the caller then truncates as usual. A body that fits wraps to a
50
+ * single line.
51
+ */
52
+ function wrapped({ head, body }: { head: string; body: string }, room: number): string[] | undefined {
53
+ const indent = visibleWidth(head) + 1;
54
+ if (room - indent < MIN_WRAP_COLUMNS) return undefined;
55
+ const pad = " ".repeat(indent);
56
+ return wrapTextWithAnsi(body, room - indent).map((line, i) => (i === 0 ? `${head} ` : pad) + line);
57
+ }
58
+
43
59
  /**
44
- * One physical terminal line with width-aware truncation and full-width color.
60
+ * Paint a resolved row at `width`; `null` renders ZERO lines.
61
+ *
62
+ * TUI pads each line to terminal width, so no manual trailing padding is
63
+ * needed. truncateToWidth understands ANSI codes and wide Unicode glyphs, so
64
+ * colored text truncates at VISIBLE columns. The gutter is a fixed-width
65
+ * prefix, so the content gets the remaining columns. Width comes from the
66
+ * row's OWN gutter text, so a disabled gutter reclaims those columns.
67
+ */
68
+ function renderRow(row: Row | null, width: number): string[] {
69
+ if (row === null) return [];
70
+ const room = Math.max(1, width - gutterWidth(row.gutter));
71
+ const paint = (line: string) => row.gutter + line;
72
+ const lines = row.wrap ? wrapped(row.wrap, room) : undefined;
73
+ if (lines) return lines.map(paint);
74
+ return [paint(truncateToWidth(row.text, room, "…"))];
75
+ }
76
+
77
+ /**
78
+ * One physical terminal line (several for an expanded row) with width-aware
79
+ * truncation and full-width color.
45
80
  *
46
81
  * Pi's Text component wraps long strings. That is correct for prose but made a
47
- * long path spill onto a second line in a supposedly single-line renderer. TUI
82
+ * long path spill onto a second line in a renderer meant for one line. TUI
48
83
  * only supplies the real terminal width during render(), so truncating with a
49
84
  * fixed character count in the caller cannot solve this reliably.
85
+ *
86
+ * The one exception is an EXPANDED row (`row.wrap`): there the user asked to see
87
+ * everything, so a long command wraps under its own details instead.
50
88
  */
51
89
  export class CompactLine implements Component {
52
90
  private resolve: ResolveRow = () => null;
53
- private click?: () => boolean;
91
+ private click?: (onHeader: boolean) => boolean;
92
+ /** Did the last render draw a header line at y=0? */
93
+ private headed = false;
54
94
  private ticker: unknown;
55
95
  private timers: Timers;
56
96
 
@@ -60,9 +100,10 @@ export class CompactLine implements Component {
60
100
 
61
101
  /**
62
102
  * Install the row resolver. Called on every updateDisplay with fresh state.
63
- * `click` returning true stops Pi's own left-click (expand) handler.
103
+ * `click` returning true stops Pi's own left-click (expand) handler; its
104
+ * argument says whether the click landed on the row's header line.
64
105
  */
65
- setRow(resolve: ResolveRow, click?: () => boolean): void {
106
+ setRow(resolve: ResolveRow, click?: (onHeader: boolean) => boolean): void {
66
107
  this.resolve = resolve;
67
108
  this.click = click;
68
109
  }
@@ -71,7 +112,8 @@ export class CompactLine implements Component {
71
112
  // the region's own expand/collapse toggle.
72
113
  handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
73
114
  if (event.type !== "click" || event.button !== "left") return undefined;
74
- return this.click?.() ? { handled: true } : undefined;
115
+ // `y` is local to THIS component (Container subtracts earlier siblings).
116
+ return this.click?.(this.headed && event.y === 0) ? { handled: true } : undefined;
75
117
  }
76
118
 
77
119
  /**
@@ -91,22 +133,10 @@ export class CompactLine implements Component {
91
133
  }
92
134
  }
93
135
 
94
- /** Test/inspection helper: is a repaint interval currently registered? */
95
- isTicking(): boolean {
96
- return this.ticker !== undefined;
97
- }
98
-
99
136
  render(width: number): string[] {
100
137
  const row = this.resolve(width);
101
- if (row === null) return [];
102
-
103
- // TUI pads each line to terminal width, so no manual trailing padding is
104
- // needed. truncateToWidth understands ANSI codes and wide Unicode glyphs,
105
- // so colored text truncates at VISIBLE columns. The gutter is a fixed-width
106
- // prefix, so the content gets the remaining columns. Width comes from the
107
- // row's OWN gutter text, so a disabled gutter reclaims those columns.
108
- const line = truncateToWidth(row.text, Math.max(1, width - gutterWidth(row.gutter)), "…");
109
- return [row.gutter + (row.highlight ? row.highlight(line) : line)];
138
+ this.headed = row?.header !== undefined;
139
+ return [...(row?.header ? [...renderRow(row.header, width), ""] : []), ...renderRow(row, width)];
110
140
  }
111
141
 
112
142
  // Component contract allows cached components to be invalidated. This class
@@ -115,28 +145,55 @@ export class CompactLine implements Component {
115
145
  }
116
146
 
117
147
  /**
118
- * Collapsed results must render ZERO lines, not one blank line.
119
- *
120
- * Pi's Text component returns [""] for empty strings (one blank row), which
121
- * doubled the height of every collapsed tool call. This is the only safe way to
122
- * say "no content at all" while still returning a Component, as the
123
- * renderResult slot contract requires.
148
+ * Assistant prose or native thinking that a fold may take over: the resolver
149
+ * returns a Row to draw it instead, `null` to hide it (zero lines), or
150
+ * `undefined` to leave `inner` untouched (with an opened run's header line
151
+ * above it, when `header` supplies one). The header is followed by one blank line. Clicks on the summary or header go to
152
+ * `click`.
124
153
  */
125
154
  export class FoldableProse implements Component {
126
155
  private inner: Component;
127
156
  private resolve: (width: number) => Row | null | undefined;
157
+ private header: () => Row | undefined;
158
+ private click: (onHeader: boolean) => boolean;
159
+ /** What the last render drew: a folded summary, or a header line above native output. */
160
+ private summary = false;
161
+ private headed = false;
128
162
 
129
- constructor(inner: Component, resolve: (width: number) => Row | null | undefined) {
163
+ /**
164
+ * `header` is the opened-run header to draw above the native output; `click`
165
+ * has the same contract as `CompactLine.setRow`'s: it gets whether the click
166
+ * landed on that header line.
167
+ */
168
+ constructor(
169
+ inner: Component,
170
+ resolve: (width: number) => Row | null | undefined,
171
+ header: () => Row | undefined,
172
+ click: (onHeader: boolean) => boolean,
173
+ ) {
130
174
  this.inner = inner;
131
175
  this.resolve = resolve;
176
+ this.header = header;
177
+ this.click = click;
132
178
  }
133
179
 
134
180
  render(width: number): string[] {
135
181
  const row = this.resolve(width);
136
- if (row === undefined) return this.inner.render(width);
137
- const line = new CompactLine();
138
- line.setRow(() => row);
139
- return line.render(width);
182
+ this.summary = row !== undefined && row !== null;
183
+ const header = row === undefined ? this.header() : undefined;
184
+ this.headed = header !== undefined;
185
+ if (row !== undefined) return renderRow(row, width);
186
+ return header ? [...renderRow(header, width), "", ...this.inner.render(width)] : this.inner.render(width);
187
+ }
188
+
189
+ // Only the summary or the header line is clickable; the native output below
190
+ // keeps whatever click behavior its own host (e.g. a thinking MouseRegion) has.
191
+ // `y` is local to THIS component, like CompactLine's.
192
+ handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
193
+ if (event.type !== "click" || event.button !== "left") return undefined;
194
+ const onHeader = this.headed && event.y === 0;
195
+ if (!this.summary && !onHeader) return undefined;
196
+ return this.click(onHeader) ? { handled: true } : undefined;
140
197
  }
141
198
 
142
199
  invalidate(): void {
@@ -144,6 +201,14 @@ export class FoldableProse implements Component {
144
201
  }
145
202
  }
146
203
 
204
+ /**
205
+ * Collapsed results must render ZERO lines, not one blank line.
206
+ *
207
+ * Pi's Text component returns [""] for empty strings (one blank row), which
208
+ * doubled the height of every collapsed tool call. This is the only safe way to
209
+ * say "no content at all" while still returning a Component, as the
210
+ * renderResult slot contract requires.
211
+ */
147
212
  export class EmptyComponent implements Component {
148
213
  render(): string[] {
149
214
  return [];
@@ -3,12 +3,9 @@
3
3
  *
4
4
  * WHY settings.json AND NOT OUR OWN FILE
5
5
  * --------------------------------------
6
- * The old `pi-minimalist.json` was a second config file the user had to know
7
- * about, and it hardcoded `~/.pi/agent`, so anyone setting
8
- * `PI_CODING_AGENT_DIR` silently wrote their preference somewhere it would never
9
- * be read back. settings.json is where a Pi user already looks, it is what
10
- * `pi-env` reads, and Pi tolerates unknown top-level keys (the published
11
- * `pi-powerline-footer` extension does exactly this with its `powerline` key).
6
+ * It is where a Pi user already looks, it is what `pi-env` reads, and Pi
7
+ * tolerates unknown top-level keys (the published `pi-powerline-footer`
8
+ * extension does exactly this with its `powerline` key).
12
9
  *
13
10
  * SCOPE
14
11
  * defaults < global agent settings.json (PI_CODING_AGENT_DIR if set).
@@ -57,11 +54,6 @@ export function globalSettingsPath(env = process.env): string {
57
54
  return join(agentDir(env), "settings.json");
58
55
  }
59
56
 
60
- /** Legacy state file written by the removed `/quiet` command. */
61
- export function legacyQuietPath(env = process.env): string {
62
- return join(agentDir(env), "pi-minimalist.json");
63
- }
64
-
65
57
  type Json = Record<string, unknown>;
66
58
 
67
59
  function isRecord(value: unknown): value is Json {
@@ -75,59 +67,12 @@ function isRecord(value: unknown): value is Json {
75
67
  /**
76
68
  * Remove `//` and block comments that are outside string literals.
77
69
  *
78
- * Pi bundles `strip-json-comments` but does not re-export it to extensions, so
79
- * this is a small character scanner. The only real subtlety is that `//` inside a
80
- * JSON string (a URL, a Windows path) must NOT start a comment.
70
+ * Pi bundles `strip-json-comments` but does not re-export it to extensions. The
71
+ * regex matches a whole string first (escapes included) and keeps it, so `//`
72
+ * inside a URL or path never starts a comment.
81
73
  */
82
74
  export function stripJsonComments(text: string): string {
83
- let out = "";
84
- let inString = false;
85
- let escaped = false;
86
- let comment: "line" | "block" | undefined;
87
-
88
- for (let i = 0; i < text.length; i++) {
89
- const char = text[i];
90
- const next = text[i + 1];
91
-
92
- if (comment === "line") {
93
- if (char === "\n") {
94
- comment = undefined;
95
- out += char;
96
- }
97
- continue;
98
- }
99
- if (comment === "block") {
100
- if (char === "*" && next === "/") {
101
- comment = undefined;
102
- i++;
103
- }
104
- continue;
105
- }
106
- if (inString) {
107
- out += char;
108
- if (escaped) escaped = false;
109
- else if (char === "\\") escaped = true;
110
- else if (char === '"') inString = false;
111
- continue;
112
- }
113
- if (char === '"') {
114
- inString = true;
115
- out += char;
116
- continue;
117
- }
118
- if (char === "/" && next === "/") {
119
- comment = "line";
120
- i++;
121
- continue;
122
- }
123
- if (char === "/" && next === "*") {
124
- comment = "block";
125
- i++;
126
- continue;
127
- }
128
- out += char;
129
- }
130
- return out;
75
+ return text.replace(/("(?:\\.|[^"\\])*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (_, string) => string ?? "");
131
76
  }
132
77
 
133
78
  /** True when a file carries comments, so writing it back would destroy them. */
@@ -191,9 +136,6 @@ function coerce(raw: unknown, into: Settings): Settings {
191
136
  // therefore the honest boundary of what this function can check.
192
137
  out.tokens = pickStrings(raw.tokens, Object.keys(DEFAULT_TOKENS) as (keyof Tokens)[]) as Partial<Tokens>;
193
138
  }
194
- if (typeof raw.maxDetailChars === "number" && raw.maxDetailChars > 0) {
195
- out.maxDetailChars = Math.floor(raw.maxDetailChars);
196
- }
197
139
  return out;
198
140
  }
199
141
 
@@ -223,24 +165,6 @@ export function customSeed(env = process.env): Record<string, boolean> | undefin
223
165
  return own ? undefined : PRESETS.lite;
224
166
  }
225
167
 
226
- /**
227
- * One-time migration of the old `/quiet` preference.
228
- *
229
- * Returns a value only when settings.json has no `groupToolRuns` yet, so an
230
- * explicit new setting always wins and a stale legacy file cannot resurrect a
231
- * preference the user has since changed.
232
- */
233
- export function migratedQuiet(env = process.env): boolean | undefined {
234
- const existing = readSettingsFile(globalSettingsPath(env))[SETTINGS_KEY];
235
- if (isRecord(existing) && typeof existing.groupToolRuns === "boolean") return undefined;
236
- try {
237
- const legacy = JSON.parse(readFileSync(legacyQuietPath(env), "utf8"));
238
- return isRecord(legacy) && legacy.quiet === true ? true : undefined;
239
- } catch {
240
- return undefined;
241
- }
242
- }
243
-
244
168
  // ---------------------------------------------------------------------------
245
169
  // Writing
246
170
  // ---------------------------------------------------------------------------
package/src/config-ui.ts CHANGED
@@ -42,12 +42,6 @@ export const FIELDS: Field[] = [
42
42
  label: "Collapse earlier activity",
43
43
  description: "Keep the latest assistant reply visible; replace earlier text, thinking, and tool calls with one summary.",
44
44
  },
45
- {
46
- key: "foldActivityOnFinalAnswer",
47
- label: " Collapse when final answer starts",
48
- description: "Keep work visible until OpenAI starts its final answer, then collapse the earlier activity.",
49
- activityOption: true,
50
- },
51
45
  {
52
46
  key: "activitySummary",
53
47
  label: " Activity summary",