@hank-warren/pi-statusline 0.4.0 → 0.4.1
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/CHANGELOG.md +8 -0
- package/README.md +3 -1
- package/index.ts +15 -2
- package/package.json +1 -1
- package/settings-menu.ts +17 -10
- package/settings.ts +61 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @hank-warren/pi-statusline
|
|
2
2
|
|
|
3
|
+
## 0.4.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 2dbfc7a: Stop settings saves from clobbering the file. Sessions load settings once at startup and used to persist their entire in-memory snapshot, so changing any row in a session that started before an edit silently reverted that edit — losing hand-written config and other sessions' changes, and collapsing the file to `{}` because serialization is sparse. Saves now write only the keys a change actually touched, merged over the current file contents, and opening `/statusline` re-reads the file first.
|
|
8
|
+
|
|
9
|
+
Also moves the **Cache celebration** row after **Subscription usage** so the menu follows the order elements render in.
|
|
10
|
+
|
|
3
11
|
## 0.4.0
|
|
4
12
|
|
|
5
13
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -26,7 +26,9 @@ Colors come from a selectable [theme](#themes), with context warning thresholds.
|
|
|
26
26
|
- **Worktree root** — the directory whose immediate children are tracked as session worktrees (default `~/repos/worktrees`). `~` and `$HOME` are expanded; a relative path is rejected and the previous value kept.
|
|
27
27
|
- **Repo aliases** — short display names for repositories on the worktree line. Enter edits the selected `repo → alias` pair, `d` deletes it, and `Add alias…` creates one from a `repo=alias` line.
|
|
28
28
|
|
|
29
|
-
Settings live in a single global file, `~/.pi/agent/statusline-settings.json`, written atomically. Only values differing from the defaults are stored, unknown keys from a newer version are preserved, and a missing or malformed file simply yields defaults.
|
|
29
|
+
Settings live in a single global file, `~/.pi/agent/statusline-settings.json`, written atomically. Only values differing from the defaults are stored, unknown keys from a newer version are preserved, and a missing or malformed file simply yields defaults.
|
|
30
|
+
|
|
31
|
+
Saves are per-key rather than whole-file: a change writes only the fields it actually touched over whatever is on disk at that moment. Sessions load settings once at startup, so a whole-file write would let a session that started hours ago revert edits it never saw — including hand edits and changes made in another session. Opening `/statusline` also re-reads the file first, so the menu always edits current state. Two sessions changing the *same* field are still last-writer-wins; everything else merges.
|
|
30
32
|
|
|
31
33
|
## Themes
|
|
32
34
|
|
package/index.ts
CHANGED
|
@@ -22,7 +22,13 @@ import {
|
|
|
22
22
|
createAliasSubmenu,
|
|
23
23
|
createWorktreeRootSubmenu,
|
|
24
24
|
} from "./settings-menu.ts";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
changedSettingKeys,
|
|
27
|
+
defaultSettings,
|
|
28
|
+
repoAlias,
|
|
29
|
+
SettingsStore,
|
|
30
|
+
type StatuslineSettings,
|
|
31
|
+
} from "./settings.ts";
|
|
26
32
|
import { resolvePalette, type StatuslinePalette, STATUSLINE_THEMES } from "./themes.ts";
|
|
27
33
|
import { type UsageSnapshot, usageBand, UsageTracker } from "./usage.ts";
|
|
28
34
|
import {
|
|
@@ -293,7 +299,9 @@ export default function statuslineExtension(pi: ExtensionAPI): void {
|
|
|
293
299
|
requestRender?.();
|
|
294
300
|
|
|
295
301
|
if (!persist) return;
|
|
296
|
-
|
|
302
|
+
const changed = changedSettingKeys(previous, next);
|
|
303
|
+
if (changed.length === 0) return;
|
|
304
|
+
settingsStore.save(next, changed).catch((error: unknown) => {
|
|
297
305
|
ctx.ui.notify(
|
|
298
306
|
`Could not save statusline settings: ${error instanceof Error ? error.message : String(error)}`,
|
|
299
307
|
"warning",
|
|
@@ -309,6 +317,11 @@ export default function statuslineExtension(pi: ExtensionAPI): void {
|
|
|
309
317
|
return;
|
|
310
318
|
}
|
|
311
319
|
|
|
320
|
+
// Settings load once per session; refresh before editing so the menu
|
|
321
|
+
// starts from what is on disk rather than a snapshot that may be hours
|
|
322
|
+
// old and missing another session's changes.
|
|
323
|
+
applySettings(ctx, await settingsStore.load(), false);
|
|
324
|
+
|
|
312
325
|
await ctx.ui.custom<void>((tui, _theme, _keybindings, done) => {
|
|
313
326
|
const tracked = trackSelectedLabel(getSettingsListTheme());
|
|
314
327
|
const settingsTheme = tracked.theme;
|
package/package.json
CHANGED
package/settings-menu.ts
CHANGED
|
@@ -75,21 +75,28 @@ export function buildSettingItems(
|
|
|
75
75
|
currentValue: settings.theme,
|
|
76
76
|
values: [...THEME_NAMES],
|
|
77
77
|
},
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
},
|
|
85
|
-
...BOOLEAN_ROWS.map((row) => ({
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
// Rows follow the order their elements render in, so the celebration sits
|
|
81
|
+
// after the usage meters it is appended to on line 1.
|
|
82
|
+
for (const row of BOOLEAN_ROWS) {
|
|
83
|
+
items.push({
|
|
86
84
|
id: row.id,
|
|
87
85
|
label: row.label,
|
|
88
86
|
description: row.description,
|
|
89
87
|
currentValue: toggleValue(settings[row.id]),
|
|
90
88
|
values: TOGGLE_VALUES,
|
|
91
|
-
})
|
|
92
|
-
|
|
89
|
+
});
|
|
90
|
+
if (row.id === "showUsage") {
|
|
91
|
+
items.push({
|
|
92
|
+
id: CACHE_CELEBRATION_ID,
|
|
93
|
+
label: CACHE_CELEBRATION_LABEL,
|
|
94
|
+
description: "Badge animation after an exceptional prompt-cache hit; previews in the statusline below.",
|
|
95
|
+
currentValue: celebrationValue(settings),
|
|
96
|
+
values: [OFF, ...CELEBRATION_STYLE_NAMES],
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
93
100
|
|
|
94
101
|
items.push({
|
|
95
102
|
id: WORKTREE_ROOT_ID,
|
package/settings.ts
CHANGED
|
@@ -165,6 +165,29 @@ function sameAliases(a: Record<string, string>, b: Record<string, string>): bool
|
|
|
165
165
|
return aKeys.every((key) => a[key] === b[key]);
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
/** Every persisted top-level key, so a diff can enumerate them exhaustively. */
|
|
169
|
+
export const SETTING_KEYS = [
|
|
170
|
+
...BOOLEAN_SETTING_KEYS,
|
|
171
|
+
"theme",
|
|
172
|
+
"cacheCelebrationStyle",
|
|
173
|
+
"worktreeRoot",
|
|
174
|
+
"repoAliases",
|
|
175
|
+
] as const satisfies readonly (keyof StatuslineSettings)[];
|
|
176
|
+
|
|
177
|
+
export type SettingKey = (typeof SETTING_KEYS)[number];
|
|
178
|
+
|
|
179
|
+
/** The keys one edit actually touched; the unit of a merging save. */
|
|
180
|
+
export function changedSettingKeys(
|
|
181
|
+
previous: StatuslineSettings,
|
|
182
|
+
next: StatuslineSettings,
|
|
183
|
+
): SettingKey[] {
|
|
184
|
+
return SETTING_KEYS.filter((key) =>
|
|
185
|
+
key === "repoAliases"
|
|
186
|
+
? !sameAliases(previous.repoAliases, next.repoAliases)
|
|
187
|
+
: previous[key] !== next[key],
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
168
191
|
/**
|
|
169
192
|
* Sparse serialization: only values differing from the defaults are written, so
|
|
170
193
|
* a later default change still reaches hosts that never touched that key.
|
|
@@ -235,10 +258,30 @@ export class SettingsStore {
|
|
|
235
258
|
return settings;
|
|
236
259
|
}
|
|
237
260
|
|
|
238
|
-
/**
|
|
239
|
-
async
|
|
261
|
+
/** Re-read the file as a plain object; anything unusable reads as empty. */
|
|
262
|
+
private async readRaw(): Promise<Record<string, unknown>> {
|
|
263
|
+
try {
|
|
264
|
+
const parsed: unknown = JSON.parse(await readFile(this.path, "utf8"));
|
|
265
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
266
|
+
return { ...(parsed as Record<string, unknown>) };
|
|
267
|
+
} catch {
|
|
268
|
+
return {};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Atomic write via temp file + rename. Rejects so the caller can notify.
|
|
274
|
+
*
|
|
275
|
+
* With `changed`, only those keys are written over whatever is on disk right
|
|
276
|
+
* now. Settings load once per session, so a full-snapshot write would let a
|
|
277
|
+
* session started before an edit silently revert it — including edits made by
|
|
278
|
+
* hand or by another session. Passing the keys one change actually touched
|
|
279
|
+
* keeps a writer from claiming fields it was never asked about.
|
|
280
|
+
*/
|
|
281
|
+
async save(settings: StatuslineSettings = this.current, changed?: readonly SettingKey[]): Promise<void> {
|
|
240
282
|
this.current = settings;
|
|
241
|
-
const
|
|
283
|
+
const object = changed ? await this.merge(settings, changed) : serializeSettings(settings, this.extra, this.home);
|
|
284
|
+
const payload = `${JSON.stringify(object, null, "\t")}\n`;
|
|
242
285
|
const temporary = `${this.path}.${pid}.tmp`;
|
|
243
286
|
await mkdir(dirname(this.path), { recursive: true });
|
|
244
287
|
try {
|
|
@@ -249,4 +292,19 @@ export class SettingsStore {
|
|
|
249
292
|
throw error;
|
|
250
293
|
}
|
|
251
294
|
}
|
|
295
|
+
|
|
296
|
+
private async merge(
|
|
297
|
+
settings: StatuslineSettings,
|
|
298
|
+
changed: readonly SettingKey[],
|
|
299
|
+
): Promise<Record<string, unknown>> {
|
|
300
|
+
const merged = await this.readRaw();
|
|
301
|
+
// Serialization is sparse, so a key absent here is back at its default and
|
|
302
|
+
// must be removed rather than written.
|
|
303
|
+
const desired = serializeSettings(settings, {}, this.home);
|
|
304
|
+
for (const key of changed) {
|
|
305
|
+
if (Object.hasOwn(desired, key)) merged[key] = desired[key];
|
|
306
|
+
else delete merged[key];
|
|
307
|
+
}
|
|
308
|
+
return merged;
|
|
309
|
+
}
|
|
252
310
|
}
|