@hank-warren/pi-statusline 0.2.4 → 0.4.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/CHANGELOG.md +20 -0
- package/README.md +57 -4
- package/celebration-preview.ts +125 -0
- package/celebration-styles.ts +158 -0
- package/index.ts +237 -72
- package/package.json +6 -1
- package/settings-menu.ts +342 -0
- package/settings.ts +252 -0
- package/themes.ts +129 -0
- package/worktrees.ts +63 -37
package/themes.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Statusline colour palettes. Every role is a full SGR prefix so a theme can use
|
|
3
|
+
* a true-colour value, a plain attribute (`\x1b[2m`), or both.
|
|
4
|
+
*/
|
|
5
|
+
export interface StatuslinePalette {
|
|
6
|
+
/** Active model id. */
|
|
7
|
+
model: string;
|
|
8
|
+
/** Repository and directory names. */
|
|
9
|
+
path: string;
|
|
10
|
+
/** Git branch names. */
|
|
11
|
+
branch: string;
|
|
12
|
+
/** Neutral figures: the context window total and the usage provider icons. */
|
|
13
|
+
text: string;
|
|
14
|
+
/** Separators, the session id, and unknown values. */
|
|
15
|
+
dim: string;
|
|
16
|
+
/** Healthy: low context use, high remaining usage, an open PR. */
|
|
17
|
+
ok: string;
|
|
18
|
+
/** First warning step, and the dirty-checkout marker. */
|
|
19
|
+
warn: string;
|
|
20
|
+
/** Second warning step, and the commits-behind marker. */
|
|
21
|
+
caution: string;
|
|
22
|
+
/** Exhausted: high context use, low remaining usage, a closed PR. */
|
|
23
|
+
danger: string;
|
|
24
|
+
/** Merged pull requests. */
|
|
25
|
+
accent: string;
|
|
26
|
+
/** The two colours the cache-hit badge alternates between. */
|
|
27
|
+
celebration: readonly [string, string];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const rgb = (hex: string): string => {
|
|
31
|
+
const value = Number.parseInt(hex.slice(1), 16);
|
|
32
|
+
return `\x1b[38;2;${(value >> 16) & 0xff};${(value >> 8) & 0xff};${value & 0xff}m`;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** The palette this package shipped before themes existed. */
|
|
36
|
+
const DEFAULT: StatuslinePalette = {
|
|
37
|
+
model: rgb("#0099ff"),
|
|
38
|
+
path: rgb("#dcdcdc"),
|
|
39
|
+
branch: rgb("#56b6c2"),
|
|
40
|
+
text: rgb("#dcdcdc"),
|
|
41
|
+
dim: "\x1b[2m",
|
|
42
|
+
ok: rgb("#00af50"),
|
|
43
|
+
warn: rgb("#e6c800"),
|
|
44
|
+
caution: rgb("#ffb055"),
|
|
45
|
+
danger: rgb("#ff5555"),
|
|
46
|
+
accent: rgb("#be78ff"),
|
|
47
|
+
celebration: [rgb("#ff00ff"), rgb("#00ffff")],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Dracula, with the pink branch colour from Hank's Herdr sidebar config. */
|
|
51
|
+
const DRACULA: StatuslinePalette = {
|
|
52
|
+
model: rgb("#bd93f9"),
|
|
53
|
+
path: rgb("#f8f8f2"),
|
|
54
|
+
branch: rgb("#ff79c6"),
|
|
55
|
+
text: rgb("#f8f8f2"),
|
|
56
|
+
dim: rgb("#6272a4"),
|
|
57
|
+
ok: rgb("#50fa7b"),
|
|
58
|
+
warn: rgb("#f1fa8c"),
|
|
59
|
+
caution: rgb("#ffb86c"),
|
|
60
|
+
danger: rgb("#ff5555"),
|
|
61
|
+
accent: rgb("#8be9fd"),
|
|
62
|
+
celebration: [rgb("#ff79c6"), rgb("#8be9fd")],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const GITHUB_DARK: StatuslinePalette = {
|
|
66
|
+
model: rgb("#58a6ff"),
|
|
67
|
+
path: rgb("#c9d1d9"),
|
|
68
|
+
branch: rgb("#39c5cf"),
|
|
69
|
+
text: rgb("#c9d1d9"),
|
|
70
|
+
dim: rgb("#8b949e"),
|
|
71
|
+
ok: rgb("#3fb950"),
|
|
72
|
+
warn: rgb("#d29922"),
|
|
73
|
+
caution: rgb("#db6d28"),
|
|
74
|
+
danger: rgb("#f85149"),
|
|
75
|
+
accent: rgb("#bc8cff"),
|
|
76
|
+
celebration: [rgb("#bc8cff"), rgb("#39c5cf")],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const CATPPUCCIN_MOCHA: StatuslinePalette = {
|
|
80
|
+
model: rgb("#cba6f7"),
|
|
81
|
+
path: rgb("#cdd6f4"),
|
|
82
|
+
branch: rgb("#89dceb"),
|
|
83
|
+
text: rgb("#cdd6f4"),
|
|
84
|
+
dim: rgb("#6c7086"),
|
|
85
|
+
ok: rgb("#a6e3a1"),
|
|
86
|
+
warn: rgb("#f9e2af"),
|
|
87
|
+
caution: rgb("#fab387"),
|
|
88
|
+
danger: rgb("#f38ba8"),
|
|
89
|
+
accent: rgb("#f5c2e7"),
|
|
90
|
+
celebration: [rgb("#cba6f7"), rgb("#89dceb")],
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** No colour at all: white text, dimmed punctuation, a grey badge flash. */
|
|
94
|
+
const WHITE: StatuslinePalette = {
|
|
95
|
+
model: rgb("#ffffff"),
|
|
96
|
+
path: rgb("#ffffff"),
|
|
97
|
+
branch: rgb("#ffffff"),
|
|
98
|
+
text: rgb("#ffffff"),
|
|
99
|
+
dim: "\x1b[2m",
|
|
100
|
+
ok: rgb("#ffffff"),
|
|
101
|
+
warn: rgb("#ffffff"),
|
|
102
|
+
caution: rgb("#ffffff"),
|
|
103
|
+
danger: rgb("#ffffff"),
|
|
104
|
+
accent: rgb("#ffffff"),
|
|
105
|
+
celebration: [rgb("#ffffff"), rgb("#888888")],
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export const STATUSLINE_THEMES = {
|
|
109
|
+
default: DEFAULT,
|
|
110
|
+
dracula: DRACULA,
|
|
111
|
+
"github-dark": GITHUB_DARK,
|
|
112
|
+
"catppuccin-mocha": CATPPUCCIN_MOCHA,
|
|
113
|
+
white: WHITE,
|
|
114
|
+
} as const satisfies Record<string, StatuslinePalette>;
|
|
115
|
+
|
|
116
|
+
export type StatuslineThemeName = keyof typeof STATUSLINE_THEMES;
|
|
117
|
+
|
|
118
|
+
export const DEFAULT_THEME: StatuslineThemeName = "default";
|
|
119
|
+
export const THEME_NAMES = Object.keys(STATUSLINE_THEMES) as StatuslineThemeName[];
|
|
120
|
+
|
|
121
|
+
export function isThemeName(value: unknown): value is StatuslineThemeName {
|
|
122
|
+
// hasOwn, not `in`: "__proto__" and "toString" are on the prototype chain.
|
|
123
|
+
return typeof value === "string" && Object.hasOwn(STATUSLINE_THEMES, value);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Resolve a theme name to its palette; anything unknown falls back to the default. */
|
|
127
|
+
export function resolvePalette(name: string): StatuslinePalette {
|
|
128
|
+
return isThemeName(name) ? STATUSLINE_THEMES[name] : STATUSLINE_THEMES[DEFAULT_THEME];
|
|
129
|
+
}
|
package/worktrees.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { basename, join, resolve } from "node:path";
|
|
3
4
|
import type { ExecResult } from "@earendil-works/pi-coding-agent";
|
|
4
5
|
|
|
@@ -34,7 +35,10 @@ export interface WorktreeTrackerHost {
|
|
|
34
35
|
args: string[],
|
|
35
36
|
options: { cwd?: string; timeout?: number; signal?: AbortSignal },
|
|
36
37
|
): Promise<ExecResult>;
|
|
37
|
-
|
|
38
|
+
/** Directory whose immediate children are tracked as session worktrees. */
|
|
39
|
+
worktreeRoot: string;
|
|
40
|
+
/** Home directory used to recognise `~`/`$HOME` spellings of the root. */
|
|
41
|
+
home?: string;
|
|
38
42
|
now?: () => number;
|
|
39
43
|
onChange?: () => void;
|
|
40
44
|
}
|
|
@@ -94,24 +98,48 @@ function visitStrings(value: unknown, visit: (value: string) => void): void {
|
|
|
94
98
|
for (const item of Object.values(value)) visitStrings(item, visit);
|
|
95
99
|
}
|
|
96
100
|
|
|
97
|
-
|
|
101
|
+
function trimTrailingSlashes(path: string): string {
|
|
102
|
+
return path.replace(/\/+$/, "") || path.slice(0, 1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Spellings of the configured root that may appear in tool arguments: the
|
|
107
|
+
* absolute path, plus the `~/` and `$HOME/` forms when it lives under home.
|
|
108
|
+
*/
|
|
109
|
+
export function worktreeRootNeedles(worktreeRoot: string, home: string = homedir()): string[] {
|
|
110
|
+
const root = trimTrailingSlashes(worktreeRoot);
|
|
111
|
+
if (root.length === 0) return [];
|
|
112
|
+
const needles = [`${root}/`];
|
|
113
|
+
const base = trimTrailingSlashes(home ?? "");
|
|
114
|
+
if (base.length > 0 && root.startsWith(`${base}/`)) {
|
|
115
|
+
const relative = root.slice(base.length + 1);
|
|
116
|
+
if (relative.length > 0) needles.push(`~/${relative}/`, `$HOME/${relative}/`);
|
|
117
|
+
}
|
|
118
|
+
return needles;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function extractWorktreePaths(value: unknown, worktreeRoot: string, home: string = homedir()): string[] {
|
|
98
122
|
const names = new Set<string>();
|
|
99
|
-
const
|
|
123
|
+
const needles = worktreeRootNeedles(worktreeRoot, home);
|
|
124
|
+
if (needles.length === 0) return [];
|
|
100
125
|
|
|
101
126
|
visitStrings(value, (text) => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
127
|
+
for (const needle of needles) {
|
|
128
|
+
let rest = text;
|
|
129
|
+
while (true) {
|
|
130
|
+
const index = rest.indexOf(needle);
|
|
131
|
+
if (index < 0) break;
|
|
132
|
+
rest = rest.slice(index + needle.length);
|
|
133
|
+
const name = rest.match(/^[a-zA-Z0-9._-]+/)?.[0];
|
|
134
|
+
if (name) names.add(name);
|
|
135
|
+
if (rest.length === 0) break;
|
|
136
|
+
rest = rest.slice(Math.max(1, name?.length ?? 0));
|
|
137
|
+
}
|
|
111
138
|
}
|
|
112
139
|
});
|
|
113
140
|
|
|
114
|
-
|
|
141
|
+
const root = trimTrailingSlashes(worktreeRoot);
|
|
142
|
+
return [...names].sort().map((name) => join(root, name));
|
|
115
143
|
}
|
|
116
144
|
|
|
117
145
|
function stripHeredocBodies(command: string): string {
|
|
@@ -137,17 +165,28 @@ function stripHeredocBodies(command: string): string {
|
|
|
137
165
|
return visible.join("\n");
|
|
138
166
|
}
|
|
139
167
|
|
|
140
|
-
export function extractWorktreePathsFromToolCall(
|
|
168
|
+
export function extractWorktreePathsFromToolCall(
|
|
169
|
+
toolName: string,
|
|
170
|
+
input: unknown,
|
|
171
|
+
worktreeRoot: string,
|
|
172
|
+
home: string = homedir(),
|
|
173
|
+
): string[] {
|
|
141
174
|
const baseName = toolName.slice(toolName.lastIndexOf(".") + 1);
|
|
142
175
|
if (baseName !== "bash" || !input || typeof input !== "object" || Array.isArray(input)) {
|
|
143
|
-
return extractWorktreePaths(input, home);
|
|
176
|
+
return extractWorktreePaths(input, worktreeRoot, home);
|
|
144
177
|
}
|
|
145
178
|
|
|
146
179
|
const command = (input as { command?: unknown }).command;
|
|
147
|
-
return typeof command === "string"
|
|
180
|
+
return typeof command === "string"
|
|
181
|
+
? extractWorktreePaths(stripHeredocBodies(command), worktreeRoot, home)
|
|
182
|
+
: [];
|
|
148
183
|
}
|
|
149
184
|
|
|
150
|
-
export function extractWorktreePathsFromEntries(
|
|
185
|
+
export function extractWorktreePathsFromEntries(
|
|
186
|
+
entries: readonly unknown[],
|
|
187
|
+
worktreeRoot: string,
|
|
188
|
+
home: string = homedir(),
|
|
189
|
+
): string[] {
|
|
151
190
|
const paths = new Set<string>();
|
|
152
191
|
|
|
153
192
|
for (const entry of entries) {
|
|
@@ -161,30 +200,15 @@ export function extractWorktreePathsFromEntries(entries: readonly unknown[], hom
|
|
|
161
200
|
if (!block || typeof block !== "object") continue;
|
|
162
201
|
const toolCall = block as { type?: unknown; name?: unknown; arguments?: unknown };
|
|
163
202
|
if (toolCall.type !== "toolCall" || typeof toolCall.name !== "string") continue;
|
|
164
|
-
for (const path of extractWorktreePathsFromToolCall(toolCall.name, toolCall.arguments, home))
|
|
203
|
+
for (const path of extractWorktreePathsFromToolCall(toolCall.name, toolCall.arguments, worktreeRoot, home)) {
|
|
204
|
+
paths.add(path);
|
|
205
|
+
}
|
|
165
206
|
}
|
|
166
207
|
}
|
|
167
208
|
|
|
168
209
|
return [...paths].sort();
|
|
169
210
|
}
|
|
170
211
|
|
|
171
|
-
export function repoAlias(repo: string): string {
|
|
172
|
-
switch (repo) {
|
|
173
|
-
case "model-runtime-engine":
|
|
174
|
-
return "mre";
|
|
175
|
-
case "frontend":
|
|
176
|
-
return "fe";
|
|
177
|
-
case "infrastructure":
|
|
178
|
-
return "infra";
|
|
179
|
-
case "platform-releases":
|
|
180
|
-
return "rel";
|
|
181
|
-
case "np-integration-testing":
|
|
182
|
-
return "tests";
|
|
183
|
-
default:
|
|
184
|
-
return repo.startsWith("platform-") ? repo.slice("platform-".length) : repo;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
212
|
export async function readWorktreeMetadata(path: string): Promise<WorktreeMetadata | undefined> {
|
|
189
213
|
try {
|
|
190
214
|
const gitFile = await readFile(join(path, ".git"), "utf8");
|
|
@@ -242,13 +266,15 @@ export class SessionWorktreeTracker {
|
|
|
242
266
|
}
|
|
243
267
|
|
|
244
268
|
async seedFromEntries(entries: readonly unknown[]): Promise<void> {
|
|
245
|
-
for (const path of extractWorktreePathsFromEntries(entries, this.host.
|
|
269
|
+
for (const path of extractWorktreePathsFromEntries(entries, this.host.worktreeRoot, this.host.home)) {
|
|
270
|
+
this.trackedPaths.add(path);
|
|
271
|
+
}
|
|
246
272
|
await this.refresh();
|
|
247
273
|
}
|
|
248
274
|
|
|
249
275
|
async observeToolInput(toolName: string, input: unknown): Promise<void> {
|
|
250
276
|
const added: string[] = [];
|
|
251
|
-
for (const path of extractWorktreePathsFromToolCall(toolName, input, this.host.home)) {
|
|
277
|
+
for (const path of extractWorktreePathsFromToolCall(toolName, input, this.host.worktreeRoot, this.host.home)) {
|
|
252
278
|
if (this.trackedPaths.has(path)) continue;
|
|
253
279
|
this.trackedPaths.add(path);
|
|
254
280
|
added.push(path);
|