@narumitw/pi-statusline 0.34.0 → 0.36.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 +9 -8
- package/package.json +1 -1
- package/src/commands.ts +85 -28
- package/src/information-profiles.ts +1 -1
- package/src/settings.ts +77 -120
- package/src/statusline.ts +2 -2
package/README.md
CHANGED
|
@@ -8,12 +8,12 @@ without setup and keeps useful context visible as the terminal narrows.
|
|
|
8
8
|
A representative uncolored layout:
|
|
9
9
|
|
|
10
10
|
```text
|
|
11
|
-
░▒▓ 🤖 sonnet-4 🧠 high 📁 pi-extensions 🌿 main ~2 🪟 ctx 42%
|
|
11
|
+
░▒▓ 🤖 sonnet-4 🧠 high 📁 pi-extensions 🌿 main ~2 🪟 ctx 42% 🕒 16:42
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
## ✨ Features
|
|
15
15
|
|
|
16
|
-
- **Zero-config default:** model, thinking, workspace, Git/PR state, context use, and
|
|
16
|
+
- **Zero-config default:** model, thinking, workspace, Git/PR state, context use, and local time.
|
|
17
17
|
- **Responsive:** removes lower-priority segments before important information gets clipped.
|
|
18
18
|
- **Quiet when idle:** activity appears only while Pi is streaming or running tools.
|
|
19
19
|
- **Easy choices:** three information levels and seven previewable color presets.
|
|
@@ -73,7 +73,7 @@ preserved.
|
|
|
73
73
|
| Level | Included segments |
|
|
74
74
|
| --- | --- |
|
|
75
75
|
| **Minimal** | `model cwd branch context` |
|
|
76
|
-
| **Balanced** (default) | `model thinking cwd branch tools context
|
|
76
|
+
| **Balanced** (default) | `model thinking cwd branch tools context time` |
|
|
77
77
|
| **Detailed** | `provider model thinking cwd branch tools context tokens cost time` |
|
|
78
78
|
| **Custom** | Any other segment order, including explicit line breaks |
|
|
79
79
|
|
|
@@ -125,12 +125,13 @@ The extension uses one user-level file:
|
|
|
125
125
|
<getAgentDir()>/pi-statusline.json
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
-
There are no project or environment overrides.
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
There are no project or environment overrides. When the file is absent, pi-statusline uses its
|
|
129
|
+
built-in defaults without creating the file or parent directory. The first successful settings save
|
|
130
|
+
creates a complete editable document atomically. Malformed or unreadable settings are never
|
|
131
|
+
overwritten. Settings reload on startup, `/reload`, and session replacement.
|
|
131
132
|
|
|
132
|
-
A valid legacy `pi-statusline-settings.json`
|
|
133
|
-
exist, `pi-statusline.json` wins.
|
|
133
|
+
A valid legacy `pi-statusline-settings.json` remains readable with a warning and is never modified
|
|
134
|
+
automatically; rename it to `pi-statusline.json`. If both files exist, `pi-statusline.json` wins.
|
|
134
135
|
|
|
135
136
|
### Settings reference
|
|
136
137
|
|
package/package.json
CHANGED
package/src/commands.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { segmentPaletteForPreset } from "./presets/index.js";
|
|
|
24
24
|
import {
|
|
25
25
|
DEFAULT_STATUSLINE_DOCUMENT,
|
|
26
26
|
type LoadedStatuslineSettings,
|
|
27
|
+
removeStatuslineSettingsDocumentIfMatches,
|
|
27
28
|
saveStatuslineSettingsDocument,
|
|
28
29
|
} from "./settings.js";
|
|
29
30
|
import {
|
|
@@ -193,21 +194,31 @@ async function choosePalettePreset(
|
|
|
193
194
|
}
|
|
194
195
|
if (selection === undefined) return;
|
|
195
196
|
|
|
197
|
+
let loaded: LoadedStatuslineSettings;
|
|
196
198
|
try {
|
|
197
199
|
const rawDocument = palettePresetDocument(current, selection);
|
|
198
|
-
|
|
199
|
-
options.settingsPath,
|
|
200
|
-
rawDocument,
|
|
201
|
-
);
|
|
202
|
-
options.apply(loaded, ctx);
|
|
203
|
-
const message =
|
|
204
|
-
loaded.config.palettePreset === "custom"
|
|
205
|
-
? "Custom palette applied. Edit colors via /statusline → Advanced → Edit settings JSON."
|
|
206
|
-
: `Palette preset applied: ${loaded.config.palettePreset}.`;
|
|
207
|
-
ctx.ui.notify(message, "info");
|
|
200
|
+
loaded = (options.save ?? saveStatuslineSettingsDocument)(options.settingsPath, rawDocument);
|
|
208
201
|
} catch (error) {
|
|
209
202
|
ctx.ui.notify(`Palette preset was not saved: ${formatError(error)}`, "error");
|
|
203
|
+
return;
|
|
210
204
|
}
|
|
205
|
+
try {
|
|
206
|
+
options.apply(loaded, ctx);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
const rollbackError = restoreStatuslineSettings(ctx, options, current, loaded);
|
|
209
|
+
ctx.ui.notify(
|
|
210
|
+
rollbackError
|
|
211
|
+
? `Palette preset could not be applied: ${formatError(error)}; rollback failed: ${formatError(rollbackError)}`
|
|
212
|
+
: `Palette preset could not be applied: ${formatError(error)}; previous settings restored.`,
|
|
213
|
+
"error",
|
|
214
|
+
);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const message =
|
|
218
|
+
loaded.config.palettePreset === "custom"
|
|
219
|
+
? "Custom palette applied. Edit colors via /statusline → Advanced → Edit settings JSON."
|
|
220
|
+
: `Palette preset applied: ${loaded.config.palettePreset}.`;
|
|
221
|
+
ctx.ui.notify(message, "info");
|
|
211
222
|
}
|
|
212
223
|
|
|
213
224
|
async function showPalettePresetPicker(
|
|
@@ -587,17 +598,30 @@ async function editSettings(ctx: ExtensionCommandContext, options: StatuslineCom
|
|
|
587
598
|
current.rawDocument ?? DEFAULT_STATUSLINE_DOCUMENT,
|
|
588
599
|
);
|
|
589
600
|
if (edited === undefined) return;
|
|
601
|
+
let loaded: LoadedStatuslineSettings;
|
|
590
602
|
try {
|
|
591
|
-
|
|
592
|
-
options.apply(loaded, ctx);
|
|
593
|
-
const suffix =
|
|
594
|
-
loaded.diagnostics.length > 0
|
|
595
|
-
? ` (${loaded.diagnostics.length} warning${loaded.diagnostics.length === 1 ? "" : "s"})`
|
|
596
|
-
: "";
|
|
597
|
-
ctx.ui.notify(`pi-statusline settings saved and applied${suffix}.`, "info");
|
|
603
|
+
loaded = (options.save ?? saveStatuslineSettingsDocument)(options.settingsPath, edited);
|
|
598
604
|
} catch (error) {
|
|
599
605
|
ctx.ui.notify(`pi-statusline settings were not saved: ${formatError(error)}`, "error");
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
try {
|
|
609
|
+
options.apply(loaded, ctx);
|
|
610
|
+
} catch (error) {
|
|
611
|
+
const rollbackError = restoreStatuslineSettings(ctx, options, current, loaded);
|
|
612
|
+
ctx.ui.notify(
|
|
613
|
+
rollbackError
|
|
614
|
+
? `pi-statusline settings could not be applied: ${formatError(error)}; rollback failed: ${formatError(rollbackError)}`
|
|
615
|
+
: `pi-statusline settings could not be applied: ${formatError(error)}; previous settings restored.`,
|
|
616
|
+
"error",
|
|
617
|
+
);
|
|
618
|
+
return;
|
|
600
619
|
}
|
|
620
|
+
const suffix =
|
|
621
|
+
loaded.diagnostics.length > 0
|
|
622
|
+
? ` (${loaded.diagnostics.length} warning${loaded.diagnostics.length === 1 ? "" : "s"})`
|
|
623
|
+
: "";
|
|
624
|
+
ctx.ui.notify(`pi-statusline settings saved and applied${suffix}.`, "info");
|
|
601
625
|
}
|
|
602
626
|
|
|
603
627
|
function palettePresetDocument(
|
|
@@ -771,15 +795,14 @@ function applySegmentsDocumentChange(
|
|
|
771
795
|
ctx: ExtensionCommandContext,
|
|
772
796
|
options: StatuslineCommandOptions,
|
|
773
797
|
): LoadedStatuslineSettings {
|
|
798
|
+
const previous = options.getLoaded();
|
|
774
799
|
const save = options.save ?? saveStatuslineSettingsDocument;
|
|
775
800
|
const next = save(options.settingsPath, change.nextDocument);
|
|
776
801
|
try {
|
|
777
802
|
options.apply(next, ctx);
|
|
778
803
|
} catch (applyError) {
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
options.apply(restored, ctx);
|
|
782
|
-
} catch (rollbackError) {
|
|
804
|
+
const rollbackError = restoreStatuslineSettings(ctx, options, previous, next);
|
|
805
|
+
if (rollbackError) {
|
|
783
806
|
throw new Error(
|
|
784
807
|
`runtime update failed: ${formatError(applyError)}; rollback failed: ${formatError(rollbackError)}`,
|
|
785
808
|
);
|
|
@@ -789,6 +812,36 @@ function applySegmentsDocumentChange(
|
|
|
789
812
|
return next;
|
|
790
813
|
}
|
|
791
814
|
|
|
815
|
+
function restoreStatuslineSettings(
|
|
816
|
+
ctx: ExtensionCommandContext,
|
|
817
|
+
options: StatuslineCommandOptions,
|
|
818
|
+
previous: LoadedStatuslineSettings,
|
|
819
|
+
saved: LoadedStatuslineSettings,
|
|
820
|
+
): unknown {
|
|
821
|
+
try {
|
|
822
|
+
if (saved.rawDocument === undefined || saved.fileIdentity === undefined) {
|
|
823
|
+
throw new Error("The saved settings document identity is unavailable");
|
|
824
|
+
}
|
|
825
|
+
removeStatuslineSettingsDocumentIfMatches(
|
|
826
|
+
options.settingsPath,
|
|
827
|
+
saved.rawDocument,
|
|
828
|
+
saved.fileIdentity,
|
|
829
|
+
);
|
|
830
|
+
if (previous.rawDocument === undefined || previous.settingsPath !== options.settingsPath) {
|
|
831
|
+
options.apply(previous, ctx);
|
|
832
|
+
return undefined;
|
|
833
|
+
}
|
|
834
|
+
const restored = (options.save ?? saveStatuslineSettingsDocument)(
|
|
835
|
+
options.settingsPath,
|
|
836
|
+
previous.rawDocument,
|
|
837
|
+
);
|
|
838
|
+
options.apply(restored, ctx);
|
|
839
|
+
return undefined;
|
|
840
|
+
} catch (error) {
|
|
841
|
+
return error;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
792
845
|
function normalizeLineBreaks(segments: readonly ConfigSegmentName[]): ConfigSegmentName[] {
|
|
793
846
|
const normalized: ConfigSegmentName[] = [];
|
|
794
847
|
for (const segment of segments) {
|
|
@@ -808,14 +861,17 @@ function editableSettings(
|
|
|
808
861
|
current: LoadedStatuslineSettings,
|
|
809
862
|
action: string,
|
|
810
863
|
): { parsed: Record<string, unknown>; rawDocument: string } {
|
|
811
|
-
|
|
812
|
-
current.source
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
864
|
+
const rawDocument =
|
|
865
|
+
current.source === "user" && current.rawDocument !== undefined
|
|
866
|
+
? current.rawDocument
|
|
867
|
+
: current.source === "built-in" &&
|
|
868
|
+
current.rawDocument === undefined &&
|
|
869
|
+
current.diagnostics.length === 0
|
|
870
|
+
? DEFAULT_STATUSLINE_DOCUMENT
|
|
871
|
+
: undefined;
|
|
872
|
+
if (rawDocument === undefined || current.diagnostics.some((item) => item.code !== "unknown")) {
|
|
816
873
|
throw new Error(`Fix pi-statusline.json before ${action}`);
|
|
817
874
|
}
|
|
818
|
-
const rawDocument = current.rawDocument;
|
|
819
875
|
const parsed = JSON.parse(rawDocument) as unknown;
|
|
820
876
|
if (!isRecord(parsed)) throw new Error("Settings must contain a JSON object");
|
|
821
877
|
return { parsed, rawDocument };
|
|
@@ -831,7 +887,8 @@ function showStatus(ctx: ExtensionCommandContext, options: StatuslineCommandOpti
|
|
|
831
887
|
ctx.ui.notify(
|
|
832
888
|
[
|
|
833
889
|
`pi-statusline source: ${loaded.source}`,
|
|
834
|
-
`path: ${
|
|
890
|
+
`active path: ${loaded.settingsPath}`,
|
|
891
|
+
`save target: ${options.settingsPath}`,
|
|
835
892
|
`palette preset: ${loaded.config.palettePreset}`,
|
|
836
893
|
`density: ${loaded.config.density}`,
|
|
837
894
|
`separator: ${loaded.config.separator}`,
|
|
@@ -8,7 +8,7 @@ export const INFORMATION_PROFILES: Readonly<
|
|
|
8
8
|
Record<InformationProfileName, readonly SegmentName[]>
|
|
9
9
|
> = {
|
|
10
10
|
minimal: ["model", "cwd", "branch", "context"],
|
|
11
|
-
balanced: ["model", "thinking", "cwd", "branch", "tools", "context", "
|
|
11
|
+
balanced: ["model", "thinking", "cwd", "branch", "tools", "context", "time"],
|
|
12
12
|
detailed: [
|
|
13
13
|
"provider",
|
|
14
14
|
"model",
|
package/src/settings.ts
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
linkSync,
|
|
4
|
-
lstatSync,
|
|
5
|
-
mkdirSync,
|
|
6
|
-
readFileSync,
|
|
7
|
-
renameSync,
|
|
8
|
-
rmSync,
|
|
9
|
-
writeFileSync,
|
|
10
|
-
} from "node:fs";
|
|
2
|
+
import { lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
11
3
|
import { dirname, join } from "node:path";
|
|
12
4
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
13
5
|
import { INFORMATION_PROFILES } from "./information-profiles.js";
|
|
@@ -109,16 +101,10 @@ export interface LoadedStatuslineSettings {
|
|
|
109
101
|
source: "built-in" | "user";
|
|
110
102
|
settingsPath: string;
|
|
111
103
|
rawDocument?: string;
|
|
104
|
+
fileIdentity?: { dev: number; ino: number };
|
|
112
105
|
diagnostics: StatuslineConfigDiagnostic[];
|
|
113
106
|
}
|
|
114
107
|
|
|
115
|
-
interface InitialFileSystem {
|
|
116
|
-
mkdirSync: typeof mkdirSync;
|
|
117
|
-
writeFileSync: typeof writeFileSync;
|
|
118
|
-
linkSync: typeof linkSync;
|
|
119
|
-
rmSync: typeof rmSync;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
108
|
interface AtomicFileSystem {
|
|
123
109
|
mkdirSync: typeof mkdirSync;
|
|
124
110
|
writeFileSync: typeof writeFileSync;
|
|
@@ -295,7 +281,7 @@ export function loadStatuslineSettings(settingsPath: string): LoadedStatuslineSe
|
|
|
295
281
|
try {
|
|
296
282
|
rawDocument = readFileSync(settingsPath, "utf8");
|
|
297
283
|
} catch (error) {
|
|
298
|
-
if (!pathExists(settingsPath)) {
|
|
284
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT" && !pathExists(settingsPath)) {
|
|
299
285
|
return builtInSettings(settingsPath);
|
|
300
286
|
}
|
|
301
287
|
return builtInSettings(settingsPath, [
|
|
@@ -324,56 +310,32 @@ export function loadStatuslineSettings(settingsPath: string): LoadedStatuslineSe
|
|
|
324
310
|
};
|
|
325
311
|
}
|
|
326
312
|
|
|
327
|
-
export function
|
|
328
|
-
agentDir = getAgentDir(),
|
|
329
|
-
overrides: Partial<InitialFileSystem> = {},
|
|
330
|
-
): LoadedStatuslineSettings {
|
|
313
|
+
export function loadStatuslineSettingsForAgent(agentDir = getAgentDir()): LoadedStatuslineSettings {
|
|
331
314
|
pendingSettingsNotice = undefined;
|
|
332
315
|
const canonicalPath = settingsFilePath(agentDir);
|
|
333
316
|
const legacyPath = join(agentDir, LEGACY_SETTINGS_FILE_NAME);
|
|
334
|
-
|
|
335
|
-
|
|
317
|
+
const canonical = loadStatuslineSettings(canonicalPath);
|
|
318
|
+
if (!isMissingStatuslineSettings(canonical)) {
|
|
319
|
+
if (!isMissingStatuslineSettings(loadStatuslineSettings(legacyPath))) {
|
|
336
320
|
pendingSettingsNotice = `${LEGACY_SETTINGS_FILE_NAME} ignored because ${SETTINGS_FILE_NAME} takes precedence.`;
|
|
337
321
|
}
|
|
338
|
-
return
|
|
339
|
-
}
|
|
340
|
-
if (pathExists(legacyPath)) return migrateLegacySettings(canonicalPath, legacyPath);
|
|
341
|
-
return createInitialSettings(canonicalPath, overrides);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function createInitialSettings(
|
|
345
|
-
canonicalPath: string,
|
|
346
|
-
overrides: Partial<InitialFileSystem>,
|
|
347
|
-
): LoadedStatuslineSettings {
|
|
348
|
-
const fs = { mkdirSync, writeFileSync, linkSync, rmSync, ...overrides };
|
|
349
|
-
const temporaryPath = temporarySettingsPath(canonicalPath);
|
|
350
|
-
try {
|
|
351
|
-
fs.mkdirSync(dirname(canonicalPath), { recursive: true });
|
|
352
|
-
fs.writeFileSync(temporaryPath, DEFAULT_STATUSLINE_DOCUMENT, {
|
|
353
|
-
encoding: "utf8",
|
|
354
|
-
flag: "wx",
|
|
355
|
-
});
|
|
356
|
-
try {
|
|
357
|
-
fs.linkSync(temporaryPath, canonicalPath);
|
|
358
|
-
} catch (error) {
|
|
359
|
-
if (isAlreadyExistsError(error)) return loadStatuslineSettings(canonicalPath);
|
|
360
|
-
throw error;
|
|
361
|
-
}
|
|
362
|
-
return loadStatuslineSettings(canonicalPath);
|
|
363
|
-
} catch (error) {
|
|
364
|
-
return builtInSettings(canonicalPath, [
|
|
365
|
-
diagnostic("warning", "io", "", `Unable to create default settings: ${formatError(error)}`),
|
|
366
|
-
]);
|
|
367
|
-
} finally {
|
|
368
|
-
removeTemporaryFile(fs.rmSync, temporaryPath);
|
|
322
|
+
return canonical;
|
|
369
323
|
}
|
|
324
|
+
const legacy = loadStatuslineSettings(legacyPath);
|
|
325
|
+
return isMissingStatuslineSettings(legacy)
|
|
326
|
+
? canonical
|
|
327
|
+
: migrateLegacySettings(canonicalPath, legacy);
|
|
370
328
|
}
|
|
371
329
|
|
|
372
330
|
function migrateLegacySettings(
|
|
373
331
|
canonicalPath: string,
|
|
374
|
-
|
|
332
|
+
legacy: LoadedStatuslineSettings,
|
|
375
333
|
): LoadedStatuslineSettings {
|
|
376
|
-
const
|
|
334
|
+
const racedCanonical = loadStatuslineSettings(canonicalPath);
|
|
335
|
+
if (!isMissingStatuslineSettings(racedCanonical)) {
|
|
336
|
+
pendingSettingsNotice = `${LEGACY_SETTINGS_FILE_NAME} ignored because ${SETTINGS_FILE_NAME} was created concurrently.`;
|
|
337
|
+
return racedCanonical;
|
|
338
|
+
}
|
|
377
339
|
if (
|
|
378
340
|
legacy.source !== "user" ||
|
|
379
341
|
legacy.rawDocument === undefined ||
|
|
@@ -382,30 +344,16 @@ function migrateLegacySettings(
|
|
|
382
344
|
pendingSettingsNotice = `${LEGACY_SETTINGS_FILE_NAME} is invalid and was ignored.`;
|
|
383
345
|
return legacy;
|
|
384
346
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
}
|
|
396
|
-
if (!fileContentsEqual(legacyPath, legacy.rawDocument)) {
|
|
397
|
-
pendingSettingsNotice = removeFileIfIdentityMatches(canonicalPath, identity, legacy.rawDocument)
|
|
398
|
-
? `${LEGACY_SETTINGS_FILE_NAME} changed during migration; the stale ${SETTINGS_FILE_NAME} snapshot was removed.`
|
|
399
|
-
: `${LEGACY_SETTINGS_FILE_NAME} changed during migration, but ${SETTINGS_FILE_NAME} was replaced concurrently and takes precedence on the next load.`;
|
|
400
|
-
return legacy;
|
|
401
|
-
}
|
|
402
|
-
try {
|
|
403
|
-
rmSync(legacyPath);
|
|
404
|
-
pendingSettingsNotice = `Statusline settings migrated from ${LEGACY_SETTINGS_FILE_NAME} to ${SETTINGS_FILE_NAME}.`;
|
|
405
|
-
} catch (error) {
|
|
406
|
-
pendingSettingsNotice = `Statusline settings migrated to ${SETTINGS_FILE_NAME}, but ${LEGACY_SETTINGS_FILE_NAME} could not be removed: ${formatError(error)}.`;
|
|
407
|
-
}
|
|
408
|
-
return loadStatuslineSettings(canonicalPath);
|
|
347
|
+
pendingSettingsNotice = `Using legacy ${LEGACY_SETTINGS_FILE_NAME}; rename it to ${SETTINGS_FILE_NAME}. Future saves write ${SETTINGS_FILE_NAME} without modifying the legacy file.`;
|
|
348
|
+
return legacy;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function isMissingStatuslineSettings(settings: LoadedStatuslineSettings): boolean {
|
|
352
|
+
return (
|
|
353
|
+
settings.source === "built-in" &&
|
|
354
|
+
settings.rawDocument === undefined &&
|
|
355
|
+
settings.diagnostics.length === 0
|
|
356
|
+
);
|
|
409
357
|
}
|
|
410
358
|
|
|
411
359
|
export function saveStatuslineSettingsDocument(
|
|
@@ -426,10 +374,17 @@ export function saveStatuslineSettingsDocument(
|
|
|
426
374
|
}
|
|
427
375
|
|
|
428
376
|
const fs = { mkdirSync, writeFileSync, renameSync, rmSync, ...overrides };
|
|
377
|
+
const replaceExisting = pathEntryExists(settingsPath);
|
|
429
378
|
const temporaryPath = temporarySettingsPath(settingsPath);
|
|
379
|
+
let fileIdentity: { dev: number; ino: number } | undefined;
|
|
430
380
|
try {
|
|
431
381
|
fs.mkdirSync(dirname(settingsPath), { recursive: true });
|
|
432
382
|
fs.writeFileSync(temporaryPath, rawDocument, { encoding: "utf8", flag: "wx" });
|
|
383
|
+
const info = lstatSync(temporaryPath);
|
|
384
|
+
fileIdentity = { dev: info.dev, ino: info.ino };
|
|
385
|
+
if (!replaceExisting && pathEntryExists(settingsPath)) {
|
|
386
|
+
throw new Error(`${SETTINGS_FILE_NAME} was created concurrently; reopen settings and retry.`);
|
|
387
|
+
}
|
|
433
388
|
fs.renameSync(temporaryPath, settingsPath);
|
|
434
389
|
} finally {
|
|
435
390
|
removeTemporaryFile(fs.rmSync, temporaryPath);
|
|
@@ -439,10 +394,45 @@ export function saveStatuslineSettingsDocument(
|
|
|
439
394
|
source: "user",
|
|
440
395
|
settingsPath,
|
|
441
396
|
rawDocument,
|
|
397
|
+
...(fileIdentity ? { fileIdentity } : {}),
|
|
442
398
|
diagnostics: normalized.diagnostics,
|
|
443
399
|
};
|
|
444
400
|
}
|
|
445
401
|
|
|
402
|
+
export function removeStatuslineSettingsDocumentIfMatches(
|
|
403
|
+
settingsPath: string,
|
|
404
|
+
expectedRawDocument: string,
|
|
405
|
+
expectedIdentity: { dev: number; ino: number },
|
|
406
|
+
): void {
|
|
407
|
+
const quarantinePath = join(
|
|
408
|
+
dirname(settingsPath),
|
|
409
|
+
`.${SETTINGS_FILE_NAME}.${randomUUID()}.rollback`,
|
|
410
|
+
);
|
|
411
|
+
const before = lstatSync(settingsPath);
|
|
412
|
+
if (before.dev !== expectedIdentity.dev || before.ino !== expectedIdentity.ino) {
|
|
413
|
+
throw new Error("Statusline settings changed concurrently; the newer file was preserved");
|
|
414
|
+
}
|
|
415
|
+
renameSync(settingsPath, quarantinePath);
|
|
416
|
+
const quarantined = lstatSync(quarantinePath);
|
|
417
|
+
const quarantinedSavedFile =
|
|
418
|
+
quarantined.isFile() &&
|
|
419
|
+
!quarantined.isSymbolicLink() &&
|
|
420
|
+
quarantined.dev === expectedIdentity.dev &&
|
|
421
|
+
quarantined.ino === expectedIdentity.ino;
|
|
422
|
+
if (quarantinedSavedFile && readFileSync(quarantinePath, "utf8") === expectedRawDocument) {
|
|
423
|
+
rmSync(quarantinePath);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (quarantinedSavedFile && !pathEntryExists(settingsPath)) {
|
|
427
|
+
try {
|
|
428
|
+
renameSync(quarantinePath, settingsPath);
|
|
429
|
+
} catch {
|
|
430
|
+
// Keep the quarantine for recovery when its atomic restoration fails.
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
throw new Error("Statusline settings changed concurrently; the newer file was preserved");
|
|
434
|
+
}
|
|
435
|
+
|
|
446
436
|
export function consumeStatuslineSettingsNotice(): string | undefined {
|
|
447
437
|
const notice = pendingSettingsNotice;
|
|
448
438
|
pendingSettingsNotice = undefined;
|
|
@@ -452,7 +442,7 @@ export function consumeStatuslineSettingsNotice(): string | undefined {
|
|
|
452
442
|
export function readStatuslineSettings(settingsPath?: string): StatuslineConfig {
|
|
453
443
|
return settingsPath
|
|
454
444
|
? loadStatuslineSettings(settingsPath).config
|
|
455
|
-
:
|
|
445
|
+
: loadStatuslineSettingsForAgent().config;
|
|
456
446
|
}
|
|
457
447
|
|
|
458
448
|
export function normalizeStatuslineSettings(value: unknown): StatuslineConfig {
|
|
@@ -628,58 +618,25 @@ function removeTemporaryFile(remove: typeof rmSync, temporaryPath: string) {
|
|
|
628
618
|
}
|
|
629
619
|
}
|
|
630
620
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
function installFileExclusively(filePath: string, contents: string): FileIdentity {
|
|
634
|
-
mkdirSync(dirname(filePath), { recursive: true });
|
|
635
|
-
const temporaryPath = temporarySettingsPath(filePath);
|
|
636
|
-
try {
|
|
637
|
-
writeFileSync(temporaryPath, contents, { encoding: "utf8", flag: "wx" });
|
|
638
|
-
const identity = lstatSync(temporaryPath);
|
|
639
|
-
linkSync(temporaryPath, filePath);
|
|
640
|
-
return { dev: identity.dev, ino: identity.ino };
|
|
641
|
-
} finally {
|
|
642
|
-
removeTemporaryFile(rmSync, temporaryPath);
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
function removeFileIfIdentityMatches(
|
|
647
|
-
filePath: string,
|
|
648
|
-
expected: FileIdentity,
|
|
649
|
-
expectedContents: string,
|
|
650
|
-
): boolean {
|
|
621
|
+
function pathExists(path: string): boolean {
|
|
651
622
|
try {
|
|
652
|
-
|
|
653
|
-
if (current.dev !== expected.dev || current.ino !== expected.ino) return false;
|
|
654
|
-
if (readFileSync(filePath, "utf8") !== expectedContents) return false;
|
|
655
|
-
rmSync(filePath);
|
|
623
|
+
lstatSync(path);
|
|
656
624
|
return true;
|
|
657
625
|
} catch {
|
|
658
626
|
return false;
|
|
659
627
|
}
|
|
660
628
|
}
|
|
661
629
|
|
|
662
|
-
function
|
|
663
|
-
try {
|
|
664
|
-
return readFileSync(path, "utf8") === expected;
|
|
665
|
-
} catch {
|
|
666
|
-
return false;
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
function pathExists(path: string): boolean {
|
|
630
|
+
function pathEntryExists(path: string): boolean {
|
|
671
631
|
try {
|
|
672
632
|
lstatSync(path);
|
|
673
633
|
return true;
|
|
674
|
-
} catch {
|
|
675
|
-
return false;
|
|
634
|
+
} catch (error) {
|
|
635
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
636
|
+
throw error;
|
|
676
637
|
}
|
|
677
638
|
}
|
|
678
639
|
|
|
679
|
-
function isAlreadyExistsError(error: unknown): boolean {
|
|
680
|
-
return error instanceof Error && "code" in error && error.code === "EEXIST";
|
|
681
|
-
}
|
|
682
|
-
|
|
683
640
|
function formatError(error: unknown): string {
|
|
684
641
|
return error instanceof Error ? error.message : String(error);
|
|
685
642
|
}
|
package/src/statusline.ts
CHANGED
|
@@ -15,8 +15,8 @@ import { type RuntimeState, renderExtensionStatusline, renderStatusline } from "
|
|
|
15
15
|
import {
|
|
16
16
|
consumeStatuslineSettingsNotice,
|
|
17
17
|
type LoadedStatuslineSettings,
|
|
18
|
-
loadOrCreateStatuslineSettings,
|
|
19
18
|
loadStatuslineSettings,
|
|
19
|
+
loadStatuslineSettingsForAgent,
|
|
20
20
|
settingsFilePath,
|
|
21
21
|
} from "./settings.js";
|
|
22
22
|
import type { PalettePreset } from "./types.js";
|
|
@@ -197,7 +197,7 @@ export default function statusline(pi: ExtensionAPI) {
|
|
|
197
197
|
runtime.turnCount = 0;
|
|
198
198
|
runtime.activeTools.clear();
|
|
199
199
|
runtime.isStreaming = false;
|
|
200
|
-
loaded =
|
|
200
|
+
loaded = loadStatuslineSettingsForAgent(agentDir);
|
|
201
201
|
const settingsNotice = consumeStatuslineSettingsNotice();
|
|
202
202
|
if (settingsNotice) ctx.ui.notify(settingsNotice, "warning");
|
|
203
203
|
if (loaded.diagnostics.length > 0) {
|