@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 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. The file is read once at session start and is not watched, so hand edits apply to the next session. Concurrent pi sessions are last-writer-wins.
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 { defaultSettings, repoAlias, SettingsStore, type StatuslineSettings } from "./settings.ts";
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
- settingsStore.save(next).catch((error: unknown) => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hank-warren/pi-statusline",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Compact Pi footer statusline with Git/worktree context, token usage, and neon celebrations for exceptional prompt-cache hits.",
5
5
  "type": "module",
6
6
  "keywords": [
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
- id: CACHE_CELEBRATION_ID,
80
- label: CACHE_CELEBRATION_LABEL,
81
- description: "Badge animation after an exceptional prompt-cache hit; previews in the statusline below.",
82
- currentValue: celebrationValue(settings),
83
- values: [OFF, ...CELEBRATION_STYLE_NAMES],
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
- /** Atomic write via temp file + rename. Rejects so the caller can notify. */
239
- async save(settings: StatuslineSettings = this.current): Promise<void> {
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 payload = `${JSON.stringify(serializeSettings(settings, this.extra, this.home), null, "\t")}\n`;
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
  }