pi-minimalist 0.1.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/LICENSE +21 -0
- package/README.md +24 -0
- package/index.ts +132 -0
- package/package.json +21 -0
- package/src/bridge.ts +124 -0
- package/src/components.ts +173 -0
- package/src/config-file.ts +277 -0
- package/src/config-ui.ts +208 -0
- package/src/config.ts +246 -0
- package/src/core-patch.ts +510 -0
- package/src/row.ts +193 -0
- package/src/run-grouping.ts +378 -0
- package/src/thinking.ts +100 -0
- package/src/tool-renderer.ts +260 -0
- package/src/tools.ts +195 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading and writing the `minimalist` key in Pi's settings.json.
|
|
3
|
+
*
|
|
4
|
+
* WHY settings.json AND NOT OUR OWN FILE
|
|
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).
|
|
12
|
+
*
|
|
13
|
+
* SCOPE
|
|
14
|
+
* defaults < global agent settings.json (PI_CODING_AGENT_DIR if set).
|
|
15
|
+
* Project-local `minimalist` blocks are deliberately ignored.
|
|
16
|
+
*
|
|
17
|
+
* FAILURE POLICY
|
|
18
|
+
* This file is hand-edited by a human, so a syntax error must never take the UI
|
|
19
|
+
* down: every read failure falls back to defaults, and a malformed value is
|
|
20
|
+
* ignored field by field so one bad line cannot disable the rest.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { homedir } from "node:os";
|
|
25
|
+
import { dirname, join } from "node:path";
|
|
26
|
+
import {
|
|
27
|
+
BASIC_KEYS,
|
|
28
|
+
DEFAULT_BASIC,
|
|
29
|
+
DEFAULTS,
|
|
30
|
+
DEFAULT_TOKENS,
|
|
31
|
+
GLYPH_PRESETS,
|
|
32
|
+
SETTINGS_KEY,
|
|
33
|
+
type BasicSettings,
|
|
34
|
+
type Glyphs,
|
|
35
|
+
type Settings,
|
|
36
|
+
type Tokens,
|
|
37
|
+
} from "./config.ts";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Pi's config directory.
|
|
41
|
+
*
|
|
42
|
+
* Two lines copied from Pi's `getAgentDir()` on purpose: importing it pulls in
|
|
43
|
+
* the package's whole module graph (~17MB), which would make `./run-tests.sh
|
|
44
|
+
* --unit` depend on a full Pi install. `test/integration.test.ts` asserts this
|
|
45
|
+
* copy agrees with Pi's real implementation, so it cannot drift unnoticed.
|
|
46
|
+
*/
|
|
47
|
+
export function agentDir(env = process.env): string {
|
|
48
|
+
return env.PI_CODING_AGENT_DIR || join(homedir(), ".pi", "agent");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function globalSettingsPath(env = process.env): string {
|
|
52
|
+
return join(agentDir(env), "settings.json");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Legacy state file written by the removed `/quiet` command. */
|
|
56
|
+
export function legacyQuietPath(env = process.env): string {
|
|
57
|
+
return join(agentDir(env), "pi-minimalist.json");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type Json = Record<string, unknown>;
|
|
61
|
+
|
|
62
|
+
function isRecord(value: unknown): value is Json {
|
|
63
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// JSON comments
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Remove `//` and block comments that are outside string literals.
|
|
72
|
+
*
|
|
73
|
+
* Pi bundles `strip-json-comments` but does not re-export it to extensions, so
|
|
74
|
+
* this is a small character scanner. The only real subtlety is that `//` inside a
|
|
75
|
+
* JSON string (a URL, a Windows path) must NOT start a comment.
|
|
76
|
+
*/
|
|
77
|
+
export function stripJsonComments(text: string): string {
|
|
78
|
+
let out = "";
|
|
79
|
+
let inString = false;
|
|
80
|
+
let escaped = false;
|
|
81
|
+
let comment: "line" | "block" | undefined;
|
|
82
|
+
|
|
83
|
+
for (let i = 0; i < text.length; i++) {
|
|
84
|
+
const char = text[i];
|
|
85
|
+
const next = text[i + 1];
|
|
86
|
+
|
|
87
|
+
if (comment === "line") {
|
|
88
|
+
if (char === "\n") {
|
|
89
|
+
comment = undefined;
|
|
90
|
+
out += char;
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (comment === "block") {
|
|
95
|
+
if (char === "*" && next === "/") {
|
|
96
|
+
comment = undefined;
|
|
97
|
+
i++;
|
|
98
|
+
}
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (inString) {
|
|
102
|
+
out += char;
|
|
103
|
+
if (escaped) escaped = false;
|
|
104
|
+
else if (char === "\\") escaped = true;
|
|
105
|
+
else if (char === '"') inString = false;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (char === '"') {
|
|
109
|
+
inString = true;
|
|
110
|
+
out += char;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (char === "/" && next === "/") {
|
|
114
|
+
comment = "line";
|
|
115
|
+
i++;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (char === "/" && next === "*") {
|
|
119
|
+
comment = "block";
|
|
120
|
+
i++;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
out += char;
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** True when a file carries comments, so writing it back would destroy them. */
|
|
129
|
+
export function hasComments(text: string): boolean {
|
|
130
|
+
return stripJsonComments(text).length !== text.length;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Reading
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
/** Parse a settings file. Any failure yields `{}` — never throws. */
|
|
138
|
+
function readSettingsFile(path: string): Json {
|
|
139
|
+
try {
|
|
140
|
+
if (!existsSync(path)) return {};
|
|
141
|
+
// Pi supports comments in settings.json, so tolerate them when READING.
|
|
142
|
+
const parsed = JSON.parse(stripJsonComments(readFileSync(path, "utf8")));
|
|
143
|
+
return isRecord(parsed) ? parsed : {};
|
|
144
|
+
} catch {
|
|
145
|
+
return {};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Take only well-typed fields from raw JSON.
|
|
151
|
+
*
|
|
152
|
+
* Field-by-field so one bad value cannot poison the rest, which matters because
|
|
153
|
+
* this is hand-edited input.
|
|
154
|
+
*/
|
|
155
|
+
function coerce(raw: unknown, into: Settings): Settings {
|
|
156
|
+
if (!isRecord(raw)) return into;
|
|
157
|
+
const out = { ...into };
|
|
158
|
+
|
|
159
|
+
for (const key of BASIC_KEYS) {
|
|
160
|
+
if (typeof DEFAULT_BASIC[key] === "boolean" && typeof raw[key] === "boolean") {
|
|
161
|
+
// BASIC_KEYS also contains the enum below; the runtime default narrows the
|
|
162
|
+
// assignable values more honestly than a second maintained key list.
|
|
163
|
+
(out as Record<string, unknown>)[key] = raw[key];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (raw.activitySummary === "elapsed" || raw.activitySummary === "tools") {
|
|
167
|
+
out.activitySummary = raw.activitySummary;
|
|
168
|
+
}
|
|
169
|
+
if (Array.isArray(raw.excludeTools)) {
|
|
170
|
+
out.excludeTools = raw.excludeTools.filter((name): name is string => typeof name === "string");
|
|
171
|
+
}
|
|
172
|
+
if (raw.glyphStyle === "unicode" || raw.glyphStyle === "ascii") {
|
|
173
|
+
out.glyphStyle = raw.glyphStyle;
|
|
174
|
+
}
|
|
175
|
+
if (isRecord(raw.glyphs)) {
|
|
176
|
+
out.glyphs = pickStrings(raw.glyphs, Object.keys(GLYPH_PRESETS.unicode) as (keyof Glyphs)[]);
|
|
177
|
+
}
|
|
178
|
+
if (isRecord(raw.tokens)) {
|
|
179
|
+
// A token NAME cannot be validated here: the set of valid names is Pi's theme
|
|
180
|
+
// palette, which this file deliberately does not import. An unknown name
|
|
181
|
+
// throws inside theme.fg/bg at paint time, and core then falls back to its
|
|
182
|
+
// verbose card — visibly wrong, rather than silently mispainted. The cast is
|
|
183
|
+
// therefore the honest boundary of what this function can check.
|
|
184
|
+
out.tokens = pickStrings(raw.tokens, Object.keys(DEFAULT_TOKENS) as (keyof Tokens)[]) as Partial<Tokens>;
|
|
185
|
+
}
|
|
186
|
+
if (typeof raw.maxDetailChars === "number" && raw.maxDetailChars > 0) {
|
|
187
|
+
out.maxDetailChars = Math.floor(raw.maxDetailChars);
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Copy only the string-valued keys we recognize. */
|
|
193
|
+
function pickStrings<K extends string>(raw: Json, keys: K[]): Partial<Record<K, string>> {
|
|
194
|
+
const out: Partial<Record<K, string>> = {};
|
|
195
|
+
for (const key of keys) {
|
|
196
|
+
if (typeof raw[key] === "string") out[key] = raw[key] as string;
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Read only the global agent settings, never project-local preferences. */
|
|
202
|
+
export function loadSettings(env = process.env): Settings {
|
|
203
|
+
const global = readSettingsFile(globalSettingsPath(env));
|
|
204
|
+
return coerce(global[SETTINGS_KEY], DEFAULTS);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* One-time migration of the old `/quiet` preference.
|
|
209
|
+
*
|
|
210
|
+
* Returns a value only when settings.json has no `groupToolRuns` yet, so an
|
|
211
|
+
* explicit new setting always wins and a stale legacy file cannot resurrect a
|
|
212
|
+
* preference the user has since changed.
|
|
213
|
+
*/
|
|
214
|
+
export function migratedQuiet(env = process.env): boolean | undefined {
|
|
215
|
+
const existing = readSettingsFile(globalSettingsPath(env))[SETTINGS_KEY];
|
|
216
|
+
if (isRecord(existing) && typeof existing.groupToolRuns === "boolean") return undefined;
|
|
217
|
+
try {
|
|
218
|
+
const legacy = JSON.parse(readFileSync(legacyQuietPath(env), "utf8"));
|
|
219
|
+
return isRecord(legacy) && legacy.quiet === true ? true : undefined;
|
|
220
|
+
} catch {
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// Writing
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
export type SaveResult =
|
|
230
|
+
| { ok: true }
|
|
231
|
+
| { ok: false; reason: "comments" | "unparsable" | "write"; detail?: string };
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Persist only the supplied editor-managed settings into the global agent file.
|
|
235
|
+
* A normal edit passes one key; Restore defaults intentionally passes all.
|
|
236
|
+
* Hand-written `glyphs`/`tokens`/`excludeTools` values survive untouched.
|
|
237
|
+
*
|
|
238
|
+
* REFUSES to write a file containing comments. `JSON.stringify` would silently
|
|
239
|
+
* delete them, and quietly destroying an annotated config is far worse than
|
|
240
|
+
* asking the user to edit one line by hand.
|
|
241
|
+
*/
|
|
242
|
+
export function saveBasicSettings(settings: Partial<BasicSettings>, env = process.env): SaveResult {
|
|
243
|
+
const path = globalSettingsPath(env);
|
|
244
|
+
|
|
245
|
+
let raw = "";
|
|
246
|
+
try {
|
|
247
|
+
if (existsSync(path)) raw = readFileSync(path, "utf8");
|
|
248
|
+
} catch (error) {
|
|
249
|
+
return { ok: false, reason: "write", detail: String(error) };
|
|
250
|
+
}
|
|
251
|
+
if (raw && hasComments(raw)) return { ok: false, reason: "comments", detail: path };
|
|
252
|
+
|
|
253
|
+
let root: unknown;
|
|
254
|
+
try {
|
|
255
|
+
root = raw.trim() ? JSON.parse(raw) : {};
|
|
256
|
+
} catch (error) {
|
|
257
|
+
return { ok: false, reason: "unparsable", detail: String(error) };
|
|
258
|
+
}
|
|
259
|
+
if (!isRecord(root)) return { ok: false, reason: "unparsable", detail: path };
|
|
260
|
+
|
|
261
|
+
if (root[SETTINGS_KEY] !== undefined && !isRecord(root[SETTINGS_KEY])) {
|
|
262
|
+
return { ok: false, reason: "unparsable", detail: path };
|
|
263
|
+
}
|
|
264
|
+
const existing = isRecord(root[SETTINGS_KEY]) ? root[SETTINGS_KEY] : {};
|
|
265
|
+
const merged: Json = { ...existing };
|
|
266
|
+
for (const key of BASIC_KEYS) {
|
|
267
|
+
if (settings[key] !== undefined) merged[key] = settings[key];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
272
|
+
writeFileSync(path, `${JSON.stringify({ ...root, [SETTINGS_KEY]: merged }, null, 2)}\n`);
|
|
273
|
+
return { ok: true };
|
|
274
|
+
} catch (error) {
|
|
275
|
+
return { ok: false, reason: "write", detail: String(error) };
|
|
276
|
+
}
|
|
277
|
+
}
|
package/src/config-ui.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/** `/minimalist` (also `/minimalist config`) — Pi's SettingsList with live, conditional rows. */
|
|
2
|
+
|
|
3
|
+
import { truncateToWidth, type AutocompleteItem, type Component, type SettingItem, type SettingsList, type SettingsListTheme } from "@earendil-works/pi-tui";
|
|
4
|
+
import {
|
|
5
|
+
BASIC_KEYS,
|
|
6
|
+
DEFAULT_BASIC,
|
|
7
|
+
type BasicKey,
|
|
8
|
+
type BasicSettings,
|
|
9
|
+
type Config,
|
|
10
|
+
type Settings,
|
|
11
|
+
} from "./config.ts";
|
|
12
|
+
|
|
13
|
+
type Field = {
|
|
14
|
+
key: BasicKey;
|
|
15
|
+
label: string;
|
|
16
|
+
description: string;
|
|
17
|
+
/** Hidden until prose folding is on. */
|
|
18
|
+
activityOption?: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Label and hover description for every basic setting, in display order. */
|
|
22
|
+
export const FIELDS: Field[] = [
|
|
23
|
+
{
|
|
24
|
+
key: "compactToolRows",
|
|
25
|
+
label: "Compact tool rows",
|
|
26
|
+
description: "Show each tool call as one line instead of Pi's full card. Off restores Pi's native rendering.",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
key: "groupToolRuns",
|
|
30
|
+
label: "Combine consecutive tool calls",
|
|
31
|
+
description: "Replace consecutive finished tool calls with a single count, e.g. 'read ×2, edit ×1'.",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
key: "foldIntermediateActivity",
|
|
35
|
+
label: "Collapse earlier activity",
|
|
36
|
+
description: "Keep the latest assistant reply visible; replace earlier text, thinking, and tool calls with one summary.",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
key: "foldActivityOnFinalAnswer",
|
|
40
|
+
label: " Collapse when final answer starts",
|
|
41
|
+
description: "Keep work visible until OpenAI starts its final answer, then collapse the earlier activity.",
|
|
42
|
+
activityOption: true,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
key: "activitySummary",
|
|
46
|
+
label: " Activity summary",
|
|
47
|
+
description: "Show either elapsed work time or the tools used in place of folded activity.",
|
|
48
|
+
activityOption: true,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
key: "gutter",
|
|
52
|
+
label: "Left border",
|
|
53
|
+
description: "Show a coloured bar to the left of each row, making tool calls easier to spot.",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
key: "timer",
|
|
57
|
+
label: "Elapsed timer",
|
|
58
|
+
description: "Show how long a running tool has taken, updated once per second.",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
key: "thinkingAsToolCall",
|
|
62
|
+
label: "Compact thinking rows",
|
|
63
|
+
description: "Show collapsed thinking as a compact tool-style row. Off restores Pi's native thinking rendering.",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
key: "keepActiveToolsExpanded",
|
|
67
|
+
label: "Keep running tools out of groups",
|
|
68
|
+
description: "Never fold a tool that is still running, so you can watch its progress.",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
key: "keepActiveThinkingExpanded",
|
|
72
|
+
label: "Show thinking while generating",
|
|
73
|
+
description: "Show the full thinking block as it arrives, instead of a one-line preview.",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
key: "glyphStyle",
|
|
77
|
+
label: "Symbols",
|
|
78
|
+
description: "Use ordinary Unicode symbols, or switch to ASCII if some symbols are missing or misaligned. A Nerd Font is not required.",
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
type InputComponent = Component & { handleInput(data: string): void };
|
|
83
|
+
type SettingsListConstructor = new (...args: ConstructorParameters<typeof SettingsList>) => SettingsList;
|
|
84
|
+
type BasicValue = BasicSettings[BasicKey];
|
|
85
|
+
|
|
86
|
+
/** Conditional rows plus their display values. */
|
|
87
|
+
export function items(settings: Settings): SettingItem[] {
|
|
88
|
+
return FIELDS.filter((field) => !field.activityOption || settings.foldIntermediateActivity).map(
|
|
89
|
+
({ key, label, description }) => {
|
|
90
|
+
const value = settings[key];
|
|
91
|
+
return {
|
|
92
|
+
id: key,
|
|
93
|
+
label,
|
|
94
|
+
description,
|
|
95
|
+
currentValue: displayValue(value),
|
|
96
|
+
values: key === "activitySummary" ? ["elapsed time", "tools used"] : key === "glyphStyle" ? ["Unicode", "ASCII"] : ["on", "off"],
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type ConfigScreenDeps = {
|
|
103
|
+
SettingsList: SettingsListConstructor;
|
|
104
|
+
theme: SettingsListTheme;
|
|
105
|
+
config: Config;
|
|
106
|
+
onChange: (key: BasicKey, value: BasicValue) => void;
|
|
107
|
+
onReset: () => void;
|
|
108
|
+
onClose: () => void;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** Build a live screen; toggling prose folding immediately adds/removes its two child rows. */
|
|
112
|
+
export function createConfigScreen(deps: ConfigScreenDeps): InputComponent {
|
|
113
|
+
const { SettingsList, theme, config, onChange, onReset, onClose } = deps;
|
|
114
|
+
let list: SettingsList;
|
|
115
|
+
|
|
116
|
+
const build = () => {
|
|
117
|
+
const rows = [
|
|
118
|
+
...items(config.all()),
|
|
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
|
+
];
|
|
127
|
+
return new SettingsList(
|
|
128
|
+
rows,
|
|
129
|
+
rows.length,
|
|
130
|
+
theme,
|
|
131
|
+
(id, displayValue) => {
|
|
132
|
+
if (id === "restoreDefaults") {
|
|
133
|
+
for (const key of BASIC_KEYS) config.set(key, DEFAULT_BASIC[key]);
|
|
134
|
+
onReset();
|
|
135
|
+
list = build();
|
|
136
|
+
list.selectItem(id);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (!isBasicKey(id)) return;
|
|
140
|
+
if (id === "activitySummary") {
|
|
141
|
+
const value = displayValue === "tools used" ? "tools" : "elapsed";
|
|
142
|
+
config.set(id, value);
|
|
143
|
+
onChange(id, value);
|
|
144
|
+
} else if (id === "glyphStyle") {
|
|
145
|
+
const value = displayValue === "ASCII" ? "ascii" : "unicode";
|
|
146
|
+
config.set(id, value);
|
|
147
|
+
onChange(id, value);
|
|
148
|
+
} else {
|
|
149
|
+
const value = displayValue === "on";
|
|
150
|
+
config.set(id, value);
|
|
151
|
+
onChange(id, value);
|
|
152
|
+
}
|
|
153
|
+
if (id === "foldIntermediateActivity") {
|
|
154
|
+
list = build();
|
|
155
|
+
list.selectItem(id);
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
onClose,
|
|
159
|
+
);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
list = build();
|
|
163
|
+
return {
|
|
164
|
+
render: (width) => [
|
|
165
|
+
...list.render(width),
|
|
166
|
+
truncateToWidth(theme.hint(" Changes apply live · Ctrl+O reveals tool output"), width),
|
|
167
|
+
],
|
|
168
|
+
handleInput: (data) => list.handleInput(data),
|
|
169
|
+
handleMouse: (event) => list.handleMouse?.(event),
|
|
170
|
+
invalidate: () => list.invalidate?.(),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function isBasicKey(id: string): id is BasicKey {
|
|
175
|
+
return (BASIC_KEYS as string[]).includes(id);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function displayValue(value: BasicValue): string {
|
|
179
|
+
if (value === "elapsed") return "elapsed time";
|
|
180
|
+
if (value === "tools") return "tools used";
|
|
181
|
+
if (value === "unicode") return "Unicode";
|
|
182
|
+
if (value === "ascii") return "ASCII";
|
|
183
|
+
return value ? "on" : "off";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Argument completions for `/minimalist ...`. */
|
|
187
|
+
export function argumentCompletions(prefix: string): AutocompleteItem[] | null {
|
|
188
|
+
const items: AutocompleteItem[] = [
|
|
189
|
+
{ value: "config", label: "config", description: "Open the settings editor" },
|
|
190
|
+
{ value: "status", label: "status", description: "Show current settings" },
|
|
191
|
+
];
|
|
192
|
+
const filtered = items.filter((item) => item.value.startsWith(prefix));
|
|
193
|
+
return filtered.length > 0 ? filtered : null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** One-line-per-visible-setting summary for `/minimalist status`. */
|
|
197
|
+
export function summary(settings: Settings): string {
|
|
198
|
+
const visible = FIELDS.filter((field) => !field.activityOption || settings.foldIntermediateActivity);
|
|
199
|
+
const width = Math.max(...visible.map((field) => field.label.length));
|
|
200
|
+
return [
|
|
201
|
+
...visible.map(({ key, label }) => {
|
|
202
|
+
return ` ${label.padEnd(width)} ${displayValue(settings[key])}`;
|
|
203
|
+
}),
|
|
204
|
+
"",
|
|
205
|
+
" /minimalist change these",
|
|
206
|
+
" settings.json custom glyphs, colours, excluded tools",
|
|
207
|
+
].join("\n");
|
|
208
|
+
}
|