@narumitw/pi-statusline 0.14.1 → 0.15.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 +3 -1
- package/package.json +1 -1
- package/src/settings.ts +132 -9
- package/src/statusline.ts +3 -1
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ Unset or invalid values fall back to `tokyo-night`. Both presets keep the same e
|
|
|
53
53
|
|
|
54
54
|
## ⚙️ Extension status icons
|
|
55
55
|
|
|
56
|
-
Extension statuses use built-in icons by status key. Override or suppress them in `${PI_CODING_AGENT_DIR:-~/.pi/agent}/pi-statusline
|
|
56
|
+
Extension statuses use built-in icons by status key. Override or suppress them in `${PI_CODING_AGENT_DIR:-~/.pi/agent}/pi-statusline.json`:
|
|
57
57
|
|
|
58
58
|
```json
|
|
59
59
|
{
|
|
@@ -70,6 +70,8 @@ Extension statuses use built-in icons by status key. Override or suppress them i
|
|
|
70
70
|
}
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
Compatibility: a valid legacy `pi-statusline-settings.json` is migrated automatically to `pi-statusline.json`. If both files exist, the new filename takes precedence.
|
|
74
|
+
|
|
73
75
|
- Exact status key: always wins, e.g. `"goal"` or `"foo:server"`.
|
|
74
76
|
- Installed extension id: for installed packages, use the package name/source such as `"@vendor/pi-foo"`, `"npm:@vendor/pi-foo@1.2.3"`, `"pi-foo"`, or the derived key `"foo"`.
|
|
75
77
|
- Namespaced status keys: package `@vendor/pi-foo` can match `foo`, `foo:server`, and `foo/server`, but not fuzzy matches like `foobar`.
|
package/package.json
CHANGED
package/src/settings.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, linkSync, lstatSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
3
4
|
import process from "node:process";
|
|
4
5
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
6
|
import type { SegmentName, StatuslineConfig, StatuslinePresetName } from "../presets/types.js";
|
|
6
7
|
|
|
7
|
-
const SETTINGS_FILE = "pi-statusline
|
|
8
|
+
const SETTINGS_FILE = "pi-statusline.json";
|
|
9
|
+
const LEGACY_SETTINGS_FILE = "pi-statusline-settings.json";
|
|
8
10
|
const DEFAULT_PRESET: StatuslinePresetName = "tokyo-night";
|
|
9
11
|
const DEFAULT_SEGMENTS: SegmentName[] = [
|
|
10
12
|
"brand",
|
|
@@ -18,6 +20,7 @@ const DEFAULT_SEGMENTS: SegmentName[] = [
|
|
|
18
20
|
"cost",
|
|
19
21
|
"time",
|
|
20
22
|
];
|
|
23
|
+
let pendingSettingsNotice: string | undefined;
|
|
21
24
|
|
|
22
25
|
export interface StatuslineSettings {
|
|
23
26
|
extensionStatusIcons: Record<string, string>;
|
|
@@ -35,18 +38,134 @@ export function createDefaultConfig(): StatuslineConfig {
|
|
|
35
38
|
};
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
export function readStatuslineSettings(
|
|
39
|
-
settingsPath
|
|
40
|
-
|
|
41
|
+
export function readStatuslineSettings(settingsPath?: string): StatuslineSettings {
|
|
42
|
+
if (settingsPath) return readSettingsFile(settingsPath);
|
|
43
|
+
pendingSettingsNotice = undefined;
|
|
44
|
+
const canonicalPath = join(getAgentDir(), SETTINGS_FILE);
|
|
45
|
+
const legacyPath = join(getAgentDir(), LEGACY_SETTINGS_FILE);
|
|
46
|
+
if (existsSync(canonicalPath)) {
|
|
47
|
+
const canonical = readSettingsFileResult(canonicalPath);
|
|
48
|
+
const notices: string[] = [];
|
|
49
|
+
if (!canonical.valid) notices.push(`${SETTINGS_FILE} is invalid and was ignored.`);
|
|
50
|
+
if (existsSync(legacyPath)) {
|
|
51
|
+
notices.push(`${LEGACY_SETTINGS_FILE} ignored because ${SETTINGS_FILE} takes precedence.`);
|
|
52
|
+
}
|
|
53
|
+
if (notices.length > 0) pendingSettingsNotice = notices.join("\n");
|
|
54
|
+
return canonical.settings;
|
|
55
|
+
}
|
|
56
|
+
if (!existsSync(legacyPath)) return { extensionStatusIcons: {} };
|
|
57
|
+
const legacy = readSettingsFileResult(legacyPath);
|
|
58
|
+
if (!legacy.valid) {
|
|
59
|
+
pendingSettingsNotice = `${LEGACY_SETTINGS_FILE} is invalid and was ignored.`;
|
|
60
|
+
return legacy.settings;
|
|
61
|
+
}
|
|
62
|
+
let installedIdentity: FileIdentity;
|
|
63
|
+
try {
|
|
64
|
+
installedIdentity = installFileExclusively(canonicalPath, legacy.contents ?? "");
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (existsSync(canonicalPath)) {
|
|
67
|
+
const canonical = readSettingsFileResult(canonicalPath);
|
|
68
|
+
pendingSettingsNotice = [
|
|
69
|
+
...(!canonical.valid ? [`${SETTINGS_FILE} is invalid and was ignored.`] : []),
|
|
70
|
+
`${LEGACY_SETTINGS_FILE} ignored because ${SETTINGS_FILE} was created concurrently.`,
|
|
71
|
+
].join("\n");
|
|
72
|
+
return canonical.settings;
|
|
73
|
+
}
|
|
74
|
+
pendingSettingsNotice = `Statusline settings migration failed: ${formatError(error)}. The legacy file was used for this session.`;
|
|
75
|
+
return legacy.settings;
|
|
76
|
+
}
|
|
77
|
+
if (!fileContentsEqual(legacyPath, legacy.contents ?? "")) {
|
|
78
|
+
pendingSettingsNotice = removeFileIfIdentityMatches(
|
|
79
|
+
canonicalPath,
|
|
80
|
+
installedIdentity,
|
|
81
|
+
legacy.contents ?? "",
|
|
82
|
+
)
|
|
83
|
+
? `${LEGACY_SETTINGS_FILE} changed during migration; the stale ${SETTINGS_FILE} snapshot was removed.`
|
|
84
|
+
: `${LEGACY_SETTINGS_FILE} changed during migration, but ${SETTINGS_FILE} was replaced concurrently and takes precedence on the next load.`;
|
|
85
|
+
return legacy.settings;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
rmSync(legacyPath);
|
|
89
|
+
pendingSettingsNotice = `Statusline settings migrated from ${LEGACY_SETTINGS_FILE} to ${SETTINGS_FILE}.`;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
pendingSettingsNotice = `Statusline settings migrated to ${SETTINGS_FILE}, but ${LEGACY_SETTINGS_FILE} could not be removed: ${formatError(error)}.`;
|
|
92
|
+
}
|
|
93
|
+
return legacy.settings;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
type FileIdentity = { dev: number; ino: number };
|
|
97
|
+
|
|
98
|
+
function installFileExclusively(filePath: string, contents: string): FileIdentity {
|
|
99
|
+
const tempFile = join(dirname(filePath), `.${SETTINGS_FILE}.${randomUUID()}.tmp`);
|
|
100
|
+
try {
|
|
101
|
+
writeFileSync(tempFile, contents, { encoding: "utf8", flag: "wx" });
|
|
102
|
+
const identity = lstatSync(tempFile);
|
|
103
|
+
linkSync(tempFile, filePath);
|
|
104
|
+
return { dev: identity.dev, ino: identity.ino };
|
|
105
|
+
} finally {
|
|
106
|
+
try {
|
|
107
|
+
rmSync(tempFile, { force: true });
|
|
108
|
+
} catch {
|
|
109
|
+
// Preserve the migration result if best-effort temp cleanup fails.
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function removeFileIfIdentityMatches(
|
|
115
|
+
filePath: string,
|
|
116
|
+
expected: FileIdentity,
|
|
117
|
+
expectedContents: string,
|
|
118
|
+
) {
|
|
41
119
|
try {
|
|
42
|
-
|
|
120
|
+
const current = lstatSync(filePath);
|
|
121
|
+
if (current.dev !== expected.dev || current.ino !== expected.ino) return false;
|
|
122
|
+
if (readFileSync(filePath, "utf8") !== expectedContents) return false;
|
|
123
|
+
rmSync(filePath);
|
|
124
|
+
return true;
|
|
43
125
|
} catch {
|
|
44
|
-
return
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function fileContentsEqual(path: string, expected: string) {
|
|
131
|
+
try {
|
|
132
|
+
return readFileSync(path, "utf8") === expected;
|
|
133
|
+
} catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function consumeStatuslineSettingsNotice() {
|
|
139
|
+
const notice = pendingSettingsNotice;
|
|
140
|
+
pendingSettingsNotice = undefined;
|
|
141
|
+
return notice;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function readSettingsFile(settingsPath: string): StatuslineSettings {
|
|
145
|
+
return readSettingsFileResult(settingsPath).settings;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function readSettingsFileResult(settingsPath: string): {
|
|
149
|
+
settings: StatuslineSettings;
|
|
150
|
+
valid: boolean;
|
|
151
|
+
contents?: string;
|
|
152
|
+
} {
|
|
153
|
+
let contents: string;
|
|
154
|
+
try {
|
|
155
|
+
contents = readFileSync(settingsPath, "utf8");
|
|
156
|
+
const parsed = JSON.parse(contents) as unknown;
|
|
157
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
158
|
+
return { settings: { extensionStatusIcons: {} }, valid: false, contents };
|
|
159
|
+
}
|
|
160
|
+
return { settings: normalizeStatuslineSettings(parsed), valid: true, contents };
|
|
161
|
+
} catch {
|
|
162
|
+
return { settings: { extensionStatusIcons: {} }, valid: false };
|
|
45
163
|
}
|
|
46
164
|
}
|
|
47
165
|
|
|
48
166
|
export function normalizeStatuslineSettings(value: unknown): StatuslineSettings {
|
|
49
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
167
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
168
|
+
return { extensionStatusIcons: {} };
|
|
50
169
|
const icons = (value as { extensionStatusIcons?: unknown }).extensionStatusIcons;
|
|
51
170
|
if (!icons || typeof icons !== "object" || Array.isArray(icons)) {
|
|
52
171
|
return { extensionStatusIcons: {} };
|
|
@@ -64,3 +183,7 @@ function readStatuslinePreset(): StatuslinePresetName {
|
|
|
64
183
|
const value = process.env.PI_STATUSLINE_PRESET?.trim().toLowerCase();
|
|
65
184
|
return value === "classic" || value === "tokyo-night" ? value : DEFAULT_PRESET;
|
|
66
185
|
}
|
|
186
|
+
|
|
187
|
+
function formatError(error: unknown) {
|
|
188
|
+
return error instanceof Error ? error.message : String(error);
|
|
189
|
+
}
|
package/src/statusline.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
renderStatusline,
|
|
16
16
|
type RuntimeState,
|
|
17
17
|
} from "./render.js";
|
|
18
|
-
import { createDefaultConfig } from "./settings.js";
|
|
18
|
+
import { consumeStatuslineSettingsNotice, createDefaultConfig } from "./settings.js";
|
|
19
19
|
|
|
20
20
|
const STATUSLINE_KEY = "statusline";
|
|
21
21
|
const GIT_STATUS_REFRESH_INTERVAL_MS = 30_000;
|
|
@@ -162,6 +162,8 @@ export default function statusline(pi: ExtensionAPI) {
|
|
|
162
162
|
};
|
|
163
163
|
|
|
164
164
|
pi.on("session_start", (_event, ctx) => {
|
|
165
|
+
const settingsNotice = consumeStatuslineSettingsNotice();
|
|
166
|
+
if (settingsNotice) ctx.ui.notify(settingsNotice, "warning");
|
|
165
167
|
runtime.thinkingLevel = pi.getThinkingLevel();
|
|
166
168
|
installFooter(ctx);
|
|
167
169
|
});
|