pi-minimalist 0.2.0 → 0.2.2
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 +3 -3
- package/index.ts +2 -9
- package/package.json +1 -1
- package/src/bridge.ts +76 -55
- package/src/components.ts +97 -32
- package/src/config-file.ts +7 -83
- package/src/config-ui.ts +0 -6
- package/src/config.ts +11 -33
- package/src/core-patch.ts +167 -178
- package/src/row.ts +44 -28
- package/src/run-grouping.ts +98 -52
- package/src/thinking.ts +5 -8
- package/src/tool-renderer.ts +21 -10
- package/src/tools.ts +57 -42
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
|
|
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,
|
|
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
|
-
//
|
|
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
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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
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
|
-
/** `
|
|
22
|
-
|
|
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:
|
|
35
|
-
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:
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
|
|
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.
|
|
49
|
-
* already handles.
|
|
67
|
+
* included.
|
|
50
68
|
*/
|
|
51
|
-
messageSpacer:
|
|
52
|
-
|
|
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
|
|
109
|
+
* Install the bridge.
|
|
89
110
|
*
|
|
90
|
-
* Each `/reload` overwrites
|
|
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
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
globals[slot("
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
return
|
|
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 [];
|
package/src/config-file.ts
CHANGED
|
@@ -3,12 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* WHY settings.json AND NOT OUR OWN FILE
|
|
5
5
|
* --------------------------------------
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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
|
-
|
|
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",
|