pi-minimalist 0.1.1 → 0.2.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/README.md +24 -3
- package/index.ts +12 -27
- package/package.json +1 -1
- package/src/components.ts +15 -3
- package/src/config-file.ts +23 -2
- package/src/config-ui.ts +42 -28
- package/src/config.ts +50 -6
- package/src/run-grouping.ts +31 -2
- package/src/thinking.ts +1 -1
- package/src/tool-renderer.ts +4 -1
package/README.md
CHANGED
|
@@ -14,6 +14,10 @@ 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.
|
|
18
|
+
|
|
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
|
+
|
|
17
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.
|
|
18
22
|
|
|
19
23
|
## Install
|
|
@@ -33,7 +37,25 @@ Run **`/minimalist`** to change settings live. Start with compact tool rows, or
|
|
|
33
37
|
- **Compact thinking rows**, or keep thinking visible as it arrives.
|
|
34
38
|
- **Keep running tools visible** while grouping other calls, with an elapsed timer to show how long they take.
|
|
35
39
|
|
|
36
|
-
|
|
40
|
+
A fresh install starts on the **`full`** preset (see below): compact tool rows, grouped calls and compact thinking. Activity folding is opt-in. Choose `lite` if you prefer to see each call separately.
|
|
41
|
+
|
|
42
|
+
### Presets
|
|
43
|
+
|
|
44
|
+
The **Preset** row at the top of `/minimalist` sets four options at once: **Compact tool rows**, **Combine consecutive tool calls**, **Collapse earlier activity** and **Compact thinking rows**.
|
|
45
|
+
|
|
46
|
+
| Preset | What you get |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `off` | Pi's native tool cards. |
|
|
49
|
+
| `lite` | One-line tool rows, each call on its own line. |
|
|
50
|
+
| `full` | `lite` plus combined consecutive calls and compact thinking rows. |
|
|
51
|
+
| `max` | `full` plus collapsed earlier activity (`Worked for …`). |
|
|
52
|
+
| `custom` | Your own values for those four options. |
|
|
53
|
+
|
|
54
|
+
<img src="docs/presets.png" width="600" alt="Clicking a folded line opens it into rows; clicking one row expands only that row">
|
|
55
|
+
|
|
56
|
+
A preset never overwrites your settings. While `off`, `lite`, `full` or `max` is selected, only those four rows are greyed out and show the preset's values; pressing Enter on one does nothing. Every other option (Left border, Elapsed timer, Symbols and so on) stays yours and stays editable. Your own values are kept and come back when you choose `custom`. The first time you choose `custom`, it starts from `lite`.
|
|
57
|
+
|
|
58
|
+
**Upgrading:** if your settings already contain a `minimalist` block, you stay on `custom` and nothing changes. Only a user with no `minimalist` settings at all starts on `full`, which makes thinking rows compact. To pick another look, change **Preset**.
|
|
37
59
|
|
|
38
60
|
### Everyday controls
|
|
39
61
|
|
|
@@ -43,13 +65,12 @@ Compact tool rows and consecutive-call grouping are **on by default**. Activity
|
|
|
43
65
|
| `/minimalist status` | Show current settings |
|
|
44
66
|
| Ctrl+O | Expand or collapse tool output |
|
|
45
67
|
| Ctrl+T | Expand or collapse thinking |
|
|
46
|
-
| Restore defaults | Reset display settings in the editor |
|
|
47
68
|
|
|
48
69
|
Settings apply to existing conversation history and are saved globally. If a symbol looks wrong in your terminal, choose **Symbols → ASCII**. No Nerd Font required.
|
|
49
70
|
|
|
50
71
|
## Go back to Pi's usual view
|
|
51
72
|
|
|
52
|
-
Turn off **Compact tool rows** to restore native tool cards.
|
|
73
|
+
Turn off **Compact tool rows** to restore native tool cards. Or choose the `off` preset.
|
|
53
74
|
|
|
54
75
|
To disable the extension completely, run `pi config`, disable pi-minimalist, and restart Pi. To uninstall:
|
|
55
76
|
|
package/index.ts
CHANGED
|
@@ -22,8 +22,8 @@ import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
|
22
22
|
// keybindings and colours, as Pi's own `/settings`.
|
|
23
23
|
import { SettingsList } from "@earendil-works/pi-tui";
|
|
24
24
|
import { installBridges, sharedState } from "./src/bridge.ts";
|
|
25
|
-
import {
|
|
26
|
-
import { loadSettings, migratedQuiet, saveBasicSettings } from "./src/config-file.ts";
|
|
25
|
+
import { type BasicKey, type BasicSettings, type Config } from "./src/config.ts";
|
|
26
|
+
import { customSeed, loadSettings, migratedQuiet, 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";
|
|
@@ -82,7 +82,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
82
82
|
theme: getSettingsListTheme(),
|
|
83
83
|
config,
|
|
84
84
|
onChange: (key, value) => persist(ctx, config, key, value),
|
|
85
|
-
onReset: () => persistDefaults(ctx, config),
|
|
86
85
|
onClose: () => done(),
|
|
87
86
|
}),
|
|
88
87
|
);
|
|
@@ -92,37 +91,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
92
91
|
|
|
93
92
|
type NotifyContext = { ui: { notify(message: string, type?: "info" | "warning" | "error"): void } };
|
|
94
93
|
|
|
95
|
-
/**
|
|
96
|
-
* Write only the changed key to the global agent settings, and be honest when that is impossible.
|
|
97
|
-
*
|
|
98
|
-
* The live value has already changed, so a silent failure would leave the UI and
|
|
99
|
-
* the file disagreeing. Keeping it as a session override means the per-turn
|
|
100
|
-
* re-read cannot revert what the message says was applied.
|
|
101
|
-
*/
|
|
102
|
-
function persistDefaults(ctx: NotifyContext, config: Config): void {
|
|
103
|
-
const result = saveBasicSettings(DEFAULT_BASIC);
|
|
104
|
-
for (const key of BASIC_KEYS) {
|
|
105
|
-
if (result.ok) config.clearSessionOverride(key);
|
|
106
|
-
else config.setSessionOverride(key, config.get(key));
|
|
107
|
-
}
|
|
108
|
-
if (!result.ok) {
|
|
109
|
-
ctx.ui.notify(`Defaults applied for this session only; settings.json could not be written (${result.reason}).`, "warning");
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
94
|
function persist(
|
|
114
95
|
ctx: NotifyContext,
|
|
115
96
|
config: Config,
|
|
116
97
|
key: BasicKey,
|
|
117
98
|
value: BasicSettings[BasicKey],
|
|
118
99
|
): void {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
100
|
+
// The first switch to `custom` starts from the `lite` look (see customSeed).
|
|
101
|
+
const seed = key === "preset" && value === "custom" ? customSeed() : undefined;
|
|
102
|
+
const changes: Partial<Record<BasicKey, unknown>> = { ...seed, [key]: value };
|
|
103
|
+
const result = saveBasicSettings(changes as Partial<BasicSettings>);
|
|
104
|
+
for (const [k, v] of Object.entries(changes) as [BasicKey, never][]) {
|
|
105
|
+
config.set(k, v);
|
|
106
|
+
// On success the file agrees, so it becomes the source of truth again.
|
|
107
|
+
if (result.ok) config.clearSessionOverride(k);
|
|
108
|
+
else config.setSessionOverride(k, v);
|
|
124
109
|
}
|
|
125
|
-
|
|
110
|
+
if (result.ok) return;
|
|
126
111
|
ctx.ui.notify(
|
|
127
112
|
result.reason === "comments"
|
|
128
113
|
? `Applied for this session only. settings.json has comments, which JSON.stringify would delete — set "minimalist": { "${key}": ${JSON.stringify(value)} } by hand to persist.`
|
package/package.json
CHANGED
package/src/components.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
10
|
-
import type { Component } from "@earendil-works/pi-tui";
|
|
10
|
+
import type { Component, TuiMouseEvent, TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
11
11
|
import { gutterWidth, type Row } from "./row.ts";
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -50,6 +50,7 @@ export type ResolveRow = (width: number) => Row | null;
|
|
|
50
50
|
*/
|
|
51
51
|
export class CompactLine implements Component {
|
|
52
52
|
private resolve: ResolveRow = () => null;
|
|
53
|
+
private click?: () => boolean;
|
|
53
54
|
private ticker: unknown;
|
|
54
55
|
private timers: Timers;
|
|
55
56
|
|
|
@@ -57,9 +58,20 @@ export class CompactLine implements Component {
|
|
|
57
58
|
this.timers = timers;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
/**
|
|
61
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Install the row resolver. Called on every updateDisplay with fresh state.
|
|
63
|
+
* `click` returning true stops Pi's own left-click (expand) handler.
|
|
64
|
+
*/
|
|
65
|
+
setRow(resolve: ResolveRow, click?: () => boolean): void {
|
|
62
66
|
this.resolve = resolve;
|
|
67
|
+
this.click = click;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Pi's MouseRegion asks its child first, so a handled click never reaches
|
|
71
|
+
// the region's own expand/collapse toggle.
|
|
72
|
+
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
73
|
+
if (event.type !== "click" || event.button !== "left") return undefined;
|
|
74
|
+
return this.click?.() ? { handled: true } : undefined;
|
|
63
75
|
}
|
|
64
76
|
|
|
65
77
|
/**
|
package/src/config-file.ts
CHANGED
|
@@ -29,6 +29,11 @@ import {
|
|
|
29
29
|
DEFAULTS,
|
|
30
30
|
DEFAULT_TOKENS,
|
|
31
31
|
GLYPH_PRESETS,
|
|
32
|
+
FRESH_PRESET,
|
|
33
|
+
LOOK_KEYS,
|
|
34
|
+
PRESETS,
|
|
35
|
+
isFresh,
|
|
36
|
+
isPreset,
|
|
32
37
|
SETTINGS_KEY,
|
|
33
38
|
type BasicSettings,
|
|
34
39
|
type Glyphs,
|
|
@@ -153,8 +158,10 @@ function readSettingsFile(path: string): Json {
|
|
|
153
158
|
* this is hand-edited input.
|
|
154
159
|
*/
|
|
155
160
|
function coerce(raw: unknown, into: Settings): Settings {
|
|
156
|
-
if (!isRecord(raw)) return into;
|
|
157
161
|
const out = { ...into };
|
|
162
|
+
// A user with no basic key has never configured anything: they get the default preset.
|
|
163
|
+
if (isFresh(raw)) out.preset = FRESH_PRESET;
|
|
164
|
+
if (!isRecord(raw)) return out;
|
|
158
165
|
|
|
159
166
|
for (const key of BASIC_KEYS) {
|
|
160
167
|
if (typeof DEFAULT_BASIC[key] === "boolean" && typeof raw[key] === "boolean") {
|
|
@@ -169,6 +176,7 @@ function coerce(raw: unknown, into: Settings): Settings {
|
|
|
169
176
|
if (Array.isArray(raw.excludeTools)) {
|
|
170
177
|
out.excludeTools = raw.excludeTools.filter((name): name is string => typeof name === "string");
|
|
171
178
|
}
|
|
179
|
+
if (isPreset(raw.preset)) out.preset = raw.preset;
|
|
172
180
|
if (raw.glyphStyle === "unicode" || raw.glyphStyle === "ascii") {
|
|
173
181
|
out.glyphStyle = raw.glyphStyle;
|
|
174
182
|
}
|
|
@@ -204,6 +212,17 @@ export function loadSettings(env = process.env): Settings {
|
|
|
204
212
|
return coerce(global[SETTINGS_KEY], DEFAULTS);
|
|
205
213
|
}
|
|
206
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Look keys to write when the user switches to `custom` for the first time: the
|
|
217
|
+
* `lite` look, so "custom" starts from something known. Undefined when the file
|
|
218
|
+
* already holds any of the user's own look keys (they are kept untouched).
|
|
219
|
+
*/
|
|
220
|
+
export function customSeed(env = process.env): Record<string, boolean> | undefined {
|
|
221
|
+
const block = readSettingsFile(globalSettingsPath(env))[SETTINGS_KEY];
|
|
222
|
+
const own = isRecord(block) && LOOK_KEYS.some((key) => key in block);
|
|
223
|
+
return own ? undefined : PRESETS.lite;
|
|
224
|
+
}
|
|
225
|
+
|
|
207
226
|
/**
|
|
208
227
|
* One-time migration of the old `/quiet` preference.
|
|
209
228
|
*
|
|
@@ -232,7 +251,7 @@ export type SaveResult =
|
|
|
232
251
|
|
|
233
252
|
/**
|
|
234
253
|
* Persist only the supplied editor-managed settings into the global agent file.
|
|
235
|
-
* A normal edit passes one key
|
|
254
|
+
* A normal edit passes one key.
|
|
236
255
|
* Hand-written `glyphs`/`tokens`/`excludeTools` values survive untouched.
|
|
237
256
|
*
|
|
238
257
|
* REFUSES to write a file containing comments. `JSON.stringify` would silently
|
|
@@ -263,6 +282,8 @@ export function saveBasicSettings(settings: Partial<BasicSettings>, env = proces
|
|
|
263
282
|
}
|
|
264
283
|
const existing = isRecord(root[SETTINGS_KEY]) ? root[SETTINGS_KEY] : {};
|
|
265
284
|
const merged: Json = { ...existing };
|
|
285
|
+
// The first write must pin the fresh-install preset, or the file would stop looking fresh and become `custom`.
|
|
286
|
+
if (isFresh(existing)) merged.preset = FRESH_PRESET;
|
|
266
287
|
for (const key of BASIC_KEYS) {
|
|
267
288
|
if (settings[key] !== undefined) merged[key] = settings[key];
|
|
268
289
|
}
|
package/src/config-ui.ts
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
import { truncateToWidth, type AutocompleteItem, type Component, type SettingItem, type SettingsList, type SettingsListTheme } from "@earendil-works/pi-tui";
|
|
4
4
|
import {
|
|
5
5
|
BASIC_KEYS,
|
|
6
|
-
|
|
6
|
+
LOOK_KEYS,
|
|
7
|
+
PRESET_NAMES,
|
|
8
|
+
isPreset,
|
|
7
9
|
type BasicKey,
|
|
8
10
|
type BasicSettings,
|
|
9
11
|
type Config,
|
|
@@ -20,6 +22,11 @@ type Field = {
|
|
|
20
22
|
|
|
21
23
|
/** Label and hover description for every basic setting, in display order. */
|
|
22
24
|
export const FIELDS: Field[] = [
|
|
25
|
+
{
|
|
26
|
+
key: "preset",
|
|
27
|
+
label: "Preset",
|
|
28
|
+
description: "off = native Pi, lite = one-line rows, full = + groups and thinking rows, max = + collapsed earlier activity. custom uses your own values. A preset only sets the four look rows; your other settings are kept.",
|
|
29
|
+
},
|
|
23
30
|
{
|
|
24
31
|
key: "compactToolRows",
|
|
25
32
|
label: "Compact tool rows",
|
|
@@ -83,18 +90,27 @@ type InputComponent = Component & { handleInput(data: string): void };
|
|
|
83
90
|
type SettingsListConstructor = new (...args: ConstructorParameters<typeof SettingsList>) => SettingsList;
|
|
84
91
|
type BasicValue = BasicSettings[BasicKey];
|
|
85
92
|
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
const CHOICES: Partial<Record<BasicKey, string[]>> = {
|
|
94
|
+
preset: [...PRESET_NAMES],
|
|
95
|
+
activitySummary: ["elapsed time", "tools used"],
|
|
96
|
+
glyphStyle: ["Unicode", "ASCII"],
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const isLook = (key: string) => (LOOK_KEYS as readonly string[]).includes(key);
|
|
100
|
+
|
|
101
|
+
/** Conditional rows plus their display values. `dim` greys the look rows a preset owns. */
|
|
102
|
+
export function items(settings: Settings, dim?: (text: string) => string): SettingItem[] {
|
|
88
103
|
return FIELDS.filter((field) => !field.activityOption || settings.foldIntermediateActivity).map(
|
|
89
104
|
({ key, label, description }) => {
|
|
90
|
-
const value = settings[key];
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
105
|
+
const value = displayValue(settings[key]);
|
|
106
|
+
// SettingsList has no disabled rows, so a locked row shows its dimmed value
|
|
107
|
+
// as its ONLY value: Enter "cycles" to the same text and nothing changes.
|
|
108
|
+
if (dim && isLook(key)) {
|
|
109
|
+
const shown = dim(value);
|
|
110
|
+
const hint = `Set by the '${settings.preset}' preset. Switch Preset to 'custom' to edit (your custom values are kept).`;
|
|
111
|
+
return { id: key, label, description: hint, currentValue: shown, values: [shown] };
|
|
112
|
+
}
|
|
113
|
+
return { id: key, label, description, currentValue: value, values: CHOICES[key] ?? ["on", "off"] };
|
|
98
114
|
},
|
|
99
115
|
);
|
|
100
116
|
}
|
|
@@ -104,39 +120,35 @@ export type ConfigScreenDeps = {
|
|
|
104
120
|
theme: SettingsListTheme;
|
|
105
121
|
config: Config;
|
|
106
122
|
onChange: (key: BasicKey, value: BasicValue) => void;
|
|
107
|
-
onReset: () => void;
|
|
108
123
|
onClose: () => void;
|
|
109
124
|
};
|
|
110
125
|
|
|
111
126
|
/** Build a live screen; toggling prose folding immediately adds/removes its two child rows. */
|
|
112
127
|
export function createConfigScreen(deps: ConfigScreenDeps): InputComponent {
|
|
113
|
-
const { SettingsList, theme, config, onChange,
|
|
128
|
+
const { SettingsList, theme, config, onChange, onClose } = deps;
|
|
114
129
|
let list: SettingsList;
|
|
130
|
+
let locked = false;
|
|
115
131
|
|
|
116
132
|
const build = () => {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
id: "restoreDefaults",
|
|
121
|
-
label: "Restore defaults",
|
|
122
|
-
description: "Reset the settings in this editor, including Symbols. Custom glyphs, colours, and excluded tools are kept.",
|
|
123
|
-
currentValue: "Enter to restore",
|
|
124
|
-
values: ["Restore"],
|
|
125
|
-
},
|
|
126
|
-
];
|
|
133
|
+
const preset = config.all().preset;
|
|
134
|
+
locked = preset !== "custom";
|
|
135
|
+
const rows = items(config.all(), locked ? theme.hint : undefined);
|
|
127
136
|
return new SettingsList(
|
|
128
137
|
rows,
|
|
129
138
|
rows.length,
|
|
130
139
|
theme,
|
|
131
140
|
(id, displayValue) => {
|
|
132
|
-
if (id
|
|
133
|
-
|
|
134
|
-
|
|
141
|
+
if (!isBasicKey(id)) return;
|
|
142
|
+
if (id === "preset") {
|
|
143
|
+
const value = isPreset(displayValue) ? displayValue : "custom";
|
|
144
|
+
config.set(id, value);
|
|
145
|
+
onChange(id, value);
|
|
135
146
|
list = build();
|
|
136
147
|
list.selectItem(id);
|
|
137
148
|
return;
|
|
138
149
|
}
|
|
139
|
-
|
|
150
|
+
// Locked rows only echo their own text; do nothing (their description explains).
|
|
151
|
+
if (locked && isLook(id)) return;
|
|
140
152
|
if (id === "activitySummary") {
|
|
141
153
|
const value = displayValue === "tools used" ? "tools" : "elapsed";
|
|
142
154
|
config.set(id, value);
|
|
@@ -176,6 +188,7 @@ function isBasicKey(id: string): id is BasicKey {
|
|
|
176
188
|
}
|
|
177
189
|
|
|
178
190
|
function displayValue(value: BasicValue): string {
|
|
191
|
+
if (isPreset(value)) return value;
|
|
179
192
|
if (value === "elapsed") return "elapsed time";
|
|
180
193
|
if (value === "tools") return "tools used";
|
|
181
194
|
if (value === "unicode") return "Unicode";
|
|
@@ -199,7 +212,8 @@ export function summary(settings: Settings): string {
|
|
|
199
212
|
const width = Math.max(...visible.map((field) => field.label.length));
|
|
200
213
|
return [
|
|
201
214
|
...visible.map(({ key, label }) => {
|
|
202
|
-
|
|
215
|
+
const dimmed = settings.preset !== "custom" && isLook(key) ? " (from preset)" : "";
|
|
216
|
+
return ` ${label.padEnd(width)} ${displayValue(settings[key])}${dimmed}`;
|
|
203
217
|
}),
|
|
204
218
|
"",
|
|
205
219
|
" /minimalist change these",
|
package/src/config.ts
CHANGED
|
@@ -85,7 +85,28 @@ export const DEFAULT_TOKENS: Tokens = {
|
|
|
85
85
|
/** Settings `/minimalist` can edit: visible effect, no vocabulary needed. */
|
|
86
86
|
export type ActivitySummary = "elapsed" | "tools";
|
|
87
87
|
|
|
88
|
+
export const PRESET_NAMES = ["off", "lite", "full", "max", "custom"] as const;
|
|
89
|
+
export type Preset = (typeof PRESET_NAMES)[number];
|
|
90
|
+
|
|
91
|
+
export function isPreset(value: unknown): value is Preset {
|
|
92
|
+
return (PRESET_NAMES as readonly unknown[]).includes(value);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The only settings a preset names. Every other setting stays the user's own. */
|
|
96
|
+
export const LOOK_KEYS = ["compactToolRows", "groupToolRuns", "foldIntermediateActivity", "thinkingAsToolCall"] as const;
|
|
97
|
+
export type LookKey = (typeof LOOK_KEYS)[number];
|
|
98
|
+
|
|
99
|
+
/** Preset of a user who has never configured anything (no basic key in the `minimalist` block). */
|
|
100
|
+
export const FRESH_PRESET: Preset = "full";
|
|
101
|
+
|
|
102
|
+
/** True when the `minimalist` block holds no basic key, so the user never configured anything. */
|
|
103
|
+
export function isFresh(block: unknown): boolean {
|
|
104
|
+
return typeof block !== "object" || block === null || !BASIC_KEYS.some((key) => key in block);
|
|
105
|
+
}
|
|
106
|
+
|
|
88
107
|
export type BasicSettings = {
|
|
108
|
+
/** A named bundle of the four look settings below. `custom` uses the user's own values for all of them. */
|
|
109
|
+
preset: Preset;
|
|
89
110
|
/** Master switch. Off restores Pi's native tool cards entirely. */
|
|
90
111
|
compactToolRows: boolean;
|
|
91
112
|
/** Fold adjacent finished rows into one summary line. */
|
|
@@ -127,6 +148,7 @@ export type AdvancedSettings = {
|
|
|
127
148
|
export type Settings = BasicSettings & AdvancedSettings;
|
|
128
149
|
|
|
129
150
|
export const DEFAULT_BASIC: BasicSettings = {
|
|
151
|
+
preset: "custom",
|
|
130
152
|
compactToolRows: true,
|
|
131
153
|
groupToolRuns: true,
|
|
132
154
|
foldIntermediateActivity: false,
|
|
@@ -140,6 +162,17 @@ export const DEFAULT_BASIC: BasicSettings = {
|
|
|
140
162
|
glyphStyle: "unicode",
|
|
141
163
|
};
|
|
142
164
|
|
|
165
|
+
/**
|
|
166
|
+
* The four look settings each non-custom preset forces. A preset is a LAYER
|
|
167
|
+
* applied on read (see `Config`); it never writes the user's own keys.
|
|
168
|
+
*/
|
|
169
|
+
export const PRESETS: Record<Exclude<Preset, "custom">, Record<LookKey, boolean>> = {
|
|
170
|
+
off: { compactToolRows: false, groupToolRuns: false, foldIntermediateActivity: false, thinkingAsToolCall: false },
|
|
171
|
+
lite: { compactToolRows: true, groupToolRuns: false, foldIntermediateActivity: false, thinkingAsToolCall: false },
|
|
172
|
+
full: { compactToolRows: true, groupToolRuns: true, foldIntermediateActivity: false, thinkingAsToolCall: true },
|
|
173
|
+
max: { compactToolRows: true, groupToolRuns: true, foldIntermediateActivity: true, thinkingAsToolCall: true },
|
|
174
|
+
};
|
|
175
|
+
|
|
143
176
|
export const DEFAULT_ADVANCED: AdvancedSettings = {
|
|
144
177
|
// `subagent` ships a renderer showing run id, state and a ctrl+o hint that one
|
|
145
178
|
// line cannot carry. Excluded by NAME rather than by the old "does this tool
|
|
@@ -167,6 +200,11 @@ export type BasicKey = keyof BasicSettings;
|
|
|
167
200
|
// Live configuration
|
|
168
201
|
// ---------------------------------------------------------------------------
|
|
169
202
|
|
|
203
|
+
/** defaults < the user's own keys < the preset's four look keys (none for `custom`). */
|
|
204
|
+
function layer(user: Settings): Settings {
|
|
205
|
+
return user.preset === "custom" ? user : { ...user, ...PRESETS[user.preset] };
|
|
206
|
+
}
|
|
207
|
+
|
|
170
208
|
/**
|
|
171
209
|
* The live settings object.
|
|
172
210
|
*
|
|
@@ -175,7 +213,12 @@ export type BasicKey = keyof BasicSettings;
|
|
|
175
213
|
* and no restart.
|
|
176
214
|
*/
|
|
177
215
|
export class Config {
|
|
216
|
+
/** The user's own values (what settings.json says), before any preset. */
|
|
178
217
|
private settings: Settings;
|
|
218
|
+
/** What the rest of the extension sees: `settings` with the preset layered on. */
|
|
219
|
+
private get view(): Settings {
|
|
220
|
+
return layer(this.settings);
|
|
221
|
+
}
|
|
179
222
|
|
|
180
223
|
/**
|
|
181
224
|
* Values changed in this session that are NOT in settings.json.
|
|
@@ -192,9 +235,10 @@ export class Config {
|
|
|
192
235
|
}
|
|
193
236
|
|
|
194
237
|
get<K extends keyof Settings>(key: K): Settings[K] {
|
|
195
|
-
return this.
|
|
238
|
+
return this.view[key];
|
|
196
239
|
}
|
|
197
240
|
|
|
241
|
+
/** Writes the user's own value; a non-custom preset keeps shadowing it until `preset` is custom. */
|
|
198
242
|
set<K extends keyof Settings>(key: K, value: Settings[K]): void {
|
|
199
243
|
this.settings = { ...this.settings, [key]: value };
|
|
200
244
|
}
|
|
@@ -211,7 +255,7 @@ export class Config {
|
|
|
211
255
|
}
|
|
212
256
|
|
|
213
257
|
all(): Settings {
|
|
214
|
-
return this.
|
|
258
|
+
return this.view;
|
|
215
259
|
}
|
|
216
260
|
|
|
217
261
|
/** Adopt freshly read settings, re-applying anything held only in memory. */
|
|
@@ -221,12 +265,12 @@ export class Config {
|
|
|
221
265
|
|
|
222
266
|
/** Preset chosen by `glyphStyle`, then per-glyph overrides. */
|
|
223
267
|
glyphs(): Glyphs {
|
|
224
|
-
return { ...GLYPH_PRESETS[this.
|
|
268
|
+
return { ...GLYPH_PRESETS[this.view.glyphStyle], ...this.view.glyphs };
|
|
225
269
|
}
|
|
226
270
|
|
|
227
271
|
/** Default token map plus overrides. */
|
|
228
272
|
tokens(): Tokens {
|
|
229
|
-
return { ...DEFAULT_TOKENS, ...this.
|
|
273
|
+
return { ...DEFAULT_TOKENS, ...this.view.tokens };
|
|
230
274
|
}
|
|
231
275
|
|
|
232
276
|
/**
|
|
@@ -236,11 +280,11 @@ export class Config {
|
|
|
236
280
|
* alongside it wherever claiming happens.
|
|
237
281
|
*/
|
|
238
282
|
isExcluded(toolName: string): boolean {
|
|
239
|
-
return this.
|
|
283
|
+
return this.view.excludeTools.includes(toolName);
|
|
240
284
|
}
|
|
241
285
|
|
|
242
286
|
/** True when this tool should be drawn as a compact row. */
|
|
243
287
|
compacts(toolName: string): boolean {
|
|
244
|
-
return this.
|
|
288
|
+
return this.view.compactToolRows && !this.isExcluded(toolName);
|
|
245
289
|
}
|
|
246
290
|
}
|
package/src/run-grouping.ts
CHANGED
|
@@ -55,6 +55,8 @@ export class RunGrouping {
|
|
|
55
55
|
private cycle = 0;
|
|
56
56
|
private cycles = new Map<number, Cycle>();
|
|
57
57
|
private agentActive = false;
|
|
58
|
+
/** Rows the user opened out of a summary by clicking it. Never fold again. */
|
|
59
|
+
private opened = new Set<string>();
|
|
58
60
|
private version = 0;
|
|
59
61
|
private cache?: ViewCache;
|
|
60
62
|
|
|
@@ -226,6 +228,33 @@ export class RunGrouping {
|
|
|
226
228
|
return painter.summary(view);
|
|
227
229
|
}
|
|
228
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Open the summary this row draws back into separate rows, after a click.
|
|
233
|
+
*
|
|
234
|
+
* Needed because the summary is drawn by ONE member (the tail) and the others
|
|
235
|
+
* draw zero lines: Pi's own click handler would expand only the tail.
|
|
236
|
+
* Returns false when the row draws no summary, so Pi's click still applies.
|
|
237
|
+
*/
|
|
238
|
+
open(id: string): boolean {
|
|
239
|
+
const entry = this.byId.get(id);
|
|
240
|
+
if (!entry) return false;
|
|
241
|
+
const { activity, grouped } = this.views();
|
|
242
|
+
const members = [entry];
|
|
243
|
+
if (activity.get(id) === "summary") {
|
|
244
|
+
members.push(...this.entries.filter((e) => e.cycle === entry.cycle && activity.get(e.id) === "hide"));
|
|
245
|
+
} else if (typeof grouped.get(id) === "object") {
|
|
246
|
+
// A run's hidden members are the contiguous "hide" rows before its tail.
|
|
247
|
+
for (let i = this.entries.indexOf(entry) - 1; i >= 0 && grouped.get(this.entries[i].id) === "hide"; i--) {
|
|
248
|
+
members.push(this.entries[i]);
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
for (const member of members) this.opened.add(member.id);
|
|
254
|
+
this.invalidateViews();
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
|
|
229
258
|
view(id: string): RowView {
|
|
230
259
|
return this.views().grouped.get(id) ?? "show";
|
|
231
260
|
}
|
|
@@ -320,7 +349,7 @@ export class RunGrouping {
|
|
|
320
349
|
}
|
|
321
350
|
|
|
322
351
|
private activityFoldable(entry: Entry): boolean {
|
|
323
|
-
if (entry.expanded) return false;
|
|
352
|
+
if (entry.expanded || this.opened.has(entry.id)) return false;
|
|
324
353
|
if (entry.outcome === "pending" && entry.kind !== "prose" && this.config.get("keepActiveToolsExpanded")) {
|
|
325
354
|
return false;
|
|
326
355
|
}
|
|
@@ -328,7 +357,7 @@ export class RunGrouping {
|
|
|
328
357
|
}
|
|
329
358
|
|
|
330
359
|
private foldable(entry: Entry | undefined): boolean {
|
|
331
|
-
if (!entry?.foldable || entry.expanded) return false;
|
|
360
|
+
if (!entry?.foldable || entry.expanded || this.opened.has(entry.id)) return false;
|
|
332
361
|
if (entry.kind === "thinking" && !this.config.get("thinkingAsToolCall")) return false;
|
|
333
362
|
if (entry.outcome === "pending" && this.config.get("keepActiveToolsExpanded")) return false;
|
|
334
363
|
return true;
|
package/src/thinking.ts
CHANGED
|
@@ -59,7 +59,7 @@ export function createThinkingPreview(deps: ThinkingDeps): ThinkingPreview {
|
|
|
59
59
|
|
|
60
60
|
if (owner && runIndex !== undefined) {
|
|
61
61
|
const id = grouping.thinkingId(owner, runIndex);
|
|
62
|
-
line.setRow(() => grouping.rowFor(id, new Painter(theme, config), paint));
|
|
62
|
+
line.setRow(() => grouping.rowFor(id, new Painter(theme, config), paint), () => grouping.open(id));
|
|
63
63
|
} else {
|
|
64
64
|
line.setRow(paint);
|
|
65
65
|
}
|
package/src/tool-renderer.ts
CHANGED
|
@@ -194,7 +194,10 @@ export function createToolRenderer(deps: RendererDeps): ToolRenderer {
|
|
|
194
194
|
return component;
|
|
195
195
|
}
|
|
196
196
|
grouping.observe(id, summaryName(name, args), status.outcome, context.expanded === true);
|
|
197
|
-
component.setRow(
|
|
197
|
+
component.setRow(
|
|
198
|
+
(width) => grouping.rowFor(id, new Painter(theme, config), () => paint(width)),
|
|
199
|
+
() => grouping.open(id),
|
|
200
|
+
);
|
|
198
201
|
return component;
|
|
199
202
|
},
|
|
200
203
|
|