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/src/config.ts ADDED
@@ -0,0 +1,246 @@
1
+ /**
2
+ * The setting schema and the live settings object.
3
+ *
4
+ * This file has NO filesystem access: reading and writing settings.json lives in
5
+ * `config-file.ts`. Keeping them apart means the schema can be unit-tested with
6
+ * no temp directories, and the I/O rules (comment handling, refusal to clobber)
7
+ * are readable on their own.
8
+ */
9
+
10
+ import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
11
+
12
+ // ThemeBg is not exported publicly, so recover it from Theme.bg's own signature
13
+ // rather than importing a deep dist path (which is not a package export).
14
+ type ThemeBg = Parameters<Theme["bg"]>[0];
15
+
16
+ /** Top-level key inside Pi's settings.json. */
17
+ export const SETTINGS_KEY = "minimalist";
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Glyphs
21
+ // ---------------------------------------------------------------------------
22
+
23
+ /**
24
+ * Status and decoration characters.
25
+ *
26
+ * Configurable for fonts or terminals that lack one of the ordinary Unicode
27
+ * characters below, or measure its width differently. A Nerd Font is not
28
+ * required; the ASCII fallback keeps each glyph within one column.
29
+ */
30
+ export type Glyphs = {
31
+ done: string;
32
+ failed: string;
33
+ running: string;
34
+ queued: string;
35
+ gutter: string;
36
+ /** Prefix inside the elapsed badge, e.g. `⏱` in `[⏱ 3s]`. May be empty. */
37
+ timer: string;
38
+ /** Multiplier in run summaries, e.g. `×` in `read ×2`. */
39
+ count: string;
40
+ };
41
+
42
+ export type GlyphStyle = "unicode" | "ascii";
43
+
44
+ export const GLYPH_PRESETS: Record<GlyphStyle, Glyphs> = {
45
+ unicode: { done: "✓", failed: "✗", running: "•", queued: "›", gutter: "▌", timer: "⏱", count: "×" },
46
+ // Every ASCII glyph is exactly one column wide, so rows stay aligned in fonts
47
+ // that render the Unicode set as double-width boxes.
48
+ ascii: { done: "+", failed: "x", running: "*", queued: ">", gutter: "|", timer: "", count: "x" },
49
+ };
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Theme tokens
53
+ // ---------------------------------------------------------------------------
54
+
55
+ /**
56
+ * Which Pi theme token paints each part of a row.
57
+ *
58
+ * Overridable, but the defaults are deliberate: the action word is a LABEL, not
59
+ * a warning, so it uses `success` green even for mutating tools. An earlier
60
+ * version used warning orange, which collided with orange meaning "highlighted
61
+ * prose".
62
+ */
63
+ export type Tokens = {
64
+ label: ThemeColor;
65
+ details: ThemeColor;
66
+ thinking: ThemeColor;
67
+ error: ThemeColor;
68
+ outputGutter: ThemeColor;
69
+ activeBg: ThemeBg;
70
+ };
71
+
72
+ export const DEFAULT_TOKENS: Tokens = {
73
+ label: "success",
74
+ details: "toolTitle",
75
+ thinking: "thinkingText",
76
+ error: "error",
77
+ outputGutter: "borderMuted",
78
+ activeBg: "toolPendingBg",
79
+ };
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // Settings
83
+ // ---------------------------------------------------------------------------
84
+
85
+ /** Settings `/minimalist` can edit: visible effect, no vocabulary needed. */
86
+ export type ActivitySummary = "elapsed" | "tools";
87
+
88
+ export type BasicSettings = {
89
+ /** Master switch. Off restores Pi's native tool cards entirely. */
90
+ compactToolRows: boolean;
91
+ /** Fold adjacent finished rows into one summary line. */
92
+ groupToolRuns: boolean;
93
+ /** Replace all but the latest assistant prose block with one summary row. */
94
+ foldIntermediateActivity: boolean;
95
+ /** Keep commentary visible until OpenAI starts its final-answer message. */
96
+ foldActivityOnFinalAnswer: boolean;
97
+ /** What replaces the activity hidden before the latest prose block. */
98
+ activitySummary: ActivitySummary;
99
+ /** Draw the left gutter bar. */
100
+ gutter: boolean;
101
+ /** Show the elapsed badge while a tool runs. */
102
+ timer: boolean;
103
+ /** Render collapsed thinking like a tool row. Off restores Pi's native thinking. */
104
+ thinkingAsToolCall: boolean;
105
+ /** Keep a RUNNING tool out of a fold so its progress stays visible. */
106
+ keepActiveToolsExpanded: boolean;
107
+ /** Keep a STREAMING thinking block expanded instead of collapsed. */
108
+ keepActiveThinkingExpanded: boolean;
109
+ /** Choose ordinary Unicode symbols or a one-column ASCII fallback. */
110
+ glyphStyle: GlyphStyle;
111
+ };
112
+
113
+ /** Settings only reachable by editing settings.json by hand. */
114
+ export type AdvancedSettings = {
115
+ /** Tool names left to their own renderer. Everything else is compacted. */
116
+ excludeTools: string[];
117
+ glyphs: Partial<Glyphs>;
118
+ tokens: Partial<Tokens>;
119
+ /**
120
+ * Sanity cap on detail text length. NOT a display limit — rows truncate at the
121
+ * real viewport width. This only stops a multi-megabyte heredoc from being
122
+ * whitespace-collapsed, colored and measured on every repaint.
123
+ */
124
+ maxDetailChars: number;
125
+ };
126
+
127
+ export type Settings = BasicSettings & AdvancedSettings;
128
+
129
+ export const DEFAULT_BASIC: BasicSettings = {
130
+ compactToolRows: true,
131
+ groupToolRuns: false,
132
+ foldIntermediateActivity: false,
133
+ foldActivityOnFinalAnswer: false,
134
+ activitySummary: "elapsed",
135
+ gutter: true,
136
+ timer: true,
137
+ thinkingAsToolCall: false,
138
+ keepActiveToolsExpanded: false,
139
+ keepActiveThinkingExpanded: false,
140
+ glyphStyle: "unicode",
141
+ };
142
+
143
+ export const DEFAULT_ADVANCED: AdvancedSettings = {
144
+ // `subagent` ships a renderer showing run id, state and a ctrl+o hint that one
145
+ // line cannot carry. Excluded by NAME rather than by the old "does this tool
146
+ // have its own renderer?" rule, which silently exempted every third-party tool
147
+ // and made the behavior impossible to discover from the UI.
148
+ excludeTools: ["subagent"],
149
+ glyphs: {},
150
+ tokens: {},
151
+ maxDetailChars: 4000,
152
+ };
153
+
154
+ export const DEFAULTS: Settings = { ...DEFAULT_BASIC, ...DEFAULT_ADVANCED };
155
+
156
+ /**
157
+ * Keys `/minimalist` may write.
158
+ *
159
+ * Derived from DEFAULT_BASIC, so adding a basic setting cannot forget to expose
160
+ * it, and the command can never clobber a hand-written advanced key.
161
+ */
162
+ export const BASIC_KEYS = Object.keys(DEFAULT_BASIC) as (keyof BasicSettings)[];
163
+
164
+ export type BasicKey = keyof BasicSettings;
165
+
166
+ // ---------------------------------------------------------------------------
167
+ // Live configuration
168
+ // ---------------------------------------------------------------------------
169
+
170
+ /**
171
+ * The live settings object.
172
+ *
173
+ * Shared and mutable on purpose: transcript rows resolve their appearance at
174
+ * RENDER time, so flipping a value re-renders existing history with no rebuild
175
+ * and no restart.
176
+ */
177
+ export class Config {
178
+ private settings: Settings;
179
+
180
+ /**
181
+ * Values changed in this session that are NOT in settings.json.
182
+ *
183
+ * Settings are re-read every turn so an external edit applies without a
184
+ * restart. Without this, a change that could not be persisted (a commented
185
+ * settings.json, a read-only file) or the legacy migration would be silently
186
+ * reverted on the next turn — while the UI still claimed it was applied.
187
+ */
188
+ private overrides = new Map<keyof Settings, Settings[keyof Settings]>();
189
+
190
+ constructor(settings: Partial<Settings> = {}) {
191
+ this.settings = { ...DEFAULTS, ...settings };
192
+ }
193
+
194
+ get<K extends keyof Settings>(key: K): Settings[K] {
195
+ return this.settings[key];
196
+ }
197
+
198
+ set<K extends keyof Settings>(key: K, value: Settings[K]): void {
199
+ this.settings = { ...this.settings, [key]: value };
200
+ }
201
+
202
+ /** Set a value that must survive a re-read of settings.json. */
203
+ setSessionOverride<K extends keyof Settings>(key: K, value: Settings[K]): void {
204
+ this.overrides.set(key, value);
205
+ this.set(key, value);
206
+ }
207
+
208
+ /** Drop an override once the file agrees with it (i.e. a later save worked). */
209
+ clearSessionOverride(key: keyof Settings): void {
210
+ this.overrides.delete(key);
211
+ }
212
+
213
+ all(): Settings {
214
+ return this.settings;
215
+ }
216
+
217
+ /** Adopt freshly read settings, re-applying anything held only in memory. */
218
+ replace(settings: Settings): void {
219
+ this.settings = { ...settings, ...Object.fromEntries(this.overrides) } as Settings;
220
+ }
221
+
222
+ /** Preset chosen by `glyphStyle`, then per-glyph overrides. */
223
+ glyphs(): Glyphs {
224
+ return { ...GLYPH_PRESETS[this.settings.glyphStyle], ...this.settings.glyphs };
225
+ }
226
+
227
+ /** Default token map plus overrides. */
228
+ tokens(): Tokens {
229
+ return { ...DEFAULT_TOKENS, ...this.settings.tokens };
230
+ }
231
+
232
+ /**
233
+ * Should this tool keep Pi's own rendering?
234
+ *
235
+ * The exclusion list is the ONLY exemption, so the master switch is checked
236
+ * alongside it wherever claiming happens.
237
+ */
238
+ isExcluded(toolName: string): boolean {
239
+ return this.settings.excludeTools.includes(toolName);
240
+ }
241
+
242
+ /** True when this tool should be drawn as a compact row. */
243
+ compacts(toolName: string): boolean {
244
+ return this.settings.compactToolRows && !this.isExcluded(toolName);
245
+ }
246
+ }