aiwcli 0.13.1 → 0.13.3
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.
|
@@ -14,9 +14,38 @@ import * as fs from "node:fs";
|
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import * as path from "node:path";
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
// PAI infrastructure imports — graceful fallback when libs aren't available
|
|
18
|
+
let CONTEXT_BASELINE_TOKENS = 22_600;
|
|
19
|
+
let getContextBySessionId: (id: string) => Record<string, unknown> | null =
|
|
20
|
+
() => null;
|
|
21
|
+
let getContext: (id: string) => Record<string, unknown> | null = () => null;
|
|
22
|
+
let loadState: (id: string) => Record<string, unknown> | null = () => null;
|
|
23
|
+
let saveState: (id: string, state: unknown) => void = () => {};
|
|
24
|
+
let findLatestPlan: (contextId: string) => string | null = () => null;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const hookUtils = await import("../lib-ts/base/hook-utils.js");
|
|
28
|
+
CONTEXT_BASELINE_TOKENS = hookUtils.CONTEXT_BASELINE_TOKENS;
|
|
29
|
+
} catch {
|
|
30
|
+
/* PAI hook-utils not available */
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const ctxStore = await import("../lib-ts/context/context-store.js");
|
|
35
|
+
getContextBySessionId = ctxStore.getContextBySessionId;
|
|
36
|
+
getContext = ctxStore.getContext;
|
|
37
|
+
loadState = ctxStore.loadState;
|
|
38
|
+
saveState = ctxStore.saveState;
|
|
39
|
+
} catch {
|
|
40
|
+
/* PAI context-store not available */
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const planMgr = await import("../lib-ts/context/plan-manager.js");
|
|
45
|
+
findLatestPlan = planMgr.findLatestPlan;
|
|
46
|
+
} catch {
|
|
47
|
+
/* PAI plan-manager not available */
|
|
48
|
+
}
|
|
20
49
|
|
|
21
50
|
// ---------------------------------------------------------------------------
|
|
22
51
|
// Path setup
|
|
@@ -71,24 +100,26 @@ const GIT_AGE_OLD = NO_COLOR ? "" : "\u001B[38;2;99;102;241m";
|
|
|
71
100
|
// ---------------------------------------------------------------------------
|
|
72
101
|
|
|
73
102
|
function getTerminalWidth(): number {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
103
|
+
const colsEnv = process.env.COLUMNS;
|
|
104
|
+
if (colsEnv) {
|
|
105
|
+
const cols = parseInt(colsEnv, 10);
|
|
106
|
+
if (cols > 0) return cols;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
if (process.stdout.columns && process.stdout.columns > 0) {
|
|
110
|
+
return process.stdout.columns;
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
/* ignore */
|
|
114
|
+
}
|
|
115
|
+
return 80;
|
|
85
116
|
}
|
|
86
117
|
|
|
87
118
|
function getDisplayMode(width: number): string {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
119
|
+
if (width < 35) return "nano";
|
|
120
|
+
if (width < 55) return "micro";
|
|
121
|
+
if (width < 80) return "mini";
|
|
122
|
+
return "normal";
|
|
92
123
|
}
|
|
93
124
|
|
|
94
125
|
// ---------------------------------------------------------------------------
|
|
@@ -96,28 +127,30 @@ function getDisplayMode(width: number): string {
|
|
|
96
127
|
// ---------------------------------------------------------------------------
|
|
97
128
|
|
|
98
129
|
function getBucketColor(pos: number, maxPos: number): string {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
130
|
+
if (NO_COLOR) return "";
|
|
131
|
+
const pct = Math.floor((pos * 100) / maxPos);
|
|
132
|
+
|
|
133
|
+
let b: number;
|
|
134
|
+
let g: number;
|
|
135
|
+
let r: number;
|
|
136
|
+
|
|
137
|
+
if (pct <= 33) {
|
|
138
|
+
r = 74 + Math.floor(((250 - 74) * pct) / 33);
|
|
139
|
+
g = 222 + Math.floor(((204 - 222) * pct) / 33);
|
|
140
|
+
b = 128 + Math.floor(((21 - 128) * pct) / 33);
|
|
141
|
+
} else if (pct <= 66) {
|
|
142
|
+
const t = pct - 33;
|
|
143
|
+
r = 250 + Math.floor(((251 - 250) * t) / 33);
|
|
144
|
+
g = 204 + Math.floor(((146 - 204) * t) / 33);
|
|
145
|
+
b = 21 + Math.floor(((60 - 21) * t) / 33);
|
|
146
|
+
} else {
|
|
147
|
+
const t = pct - 66;
|
|
148
|
+
r = 251 + Math.floor(((239 - 251) * t) / 34);
|
|
149
|
+
g = 146 + Math.floor(((68 - 146) * t) / 34);
|
|
150
|
+
b = 60 + Math.floor(((68 - 60) * t) / 34);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return `\u001B[38;2;${r};${g};${b}m`;
|
|
121
154
|
}
|
|
122
155
|
|
|
123
156
|
// ---------------------------------------------------------------------------
|
|
@@ -125,116 +158,192 @@ function getBucketColor(pos: number, maxPos: number): string {
|
|
|
125
158
|
// ---------------------------------------------------------------------------
|
|
126
159
|
|
|
127
160
|
function renderContextBar(width: number, pct: number): [string, string] {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
161
|
+
pct = Math.max(0, Math.min(100, pct));
|
|
162
|
+
const filled = Math.floor((pct * width) / 100);
|
|
163
|
+
let lastColor = EMERALD;
|
|
164
|
+
const parts: string[] = [];
|
|
165
|
+
|
|
166
|
+
for (let i = 1; i <= width; i++) {
|
|
167
|
+
if (i <= filled) {
|
|
168
|
+
const color = getBucketColor(i, width);
|
|
169
|
+
lastColor = color;
|
|
170
|
+
parts.push(`${color}\u26C1${RESET}`);
|
|
171
|
+
} else {
|
|
172
|
+
parts.push(`${CTX_BUCKET_EMPTY}\u26C1${RESET}`);
|
|
173
|
+
}
|
|
174
|
+
if (width > 8) {
|
|
175
|
+
parts.push(" ");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return [parts.join("").trimEnd(), lastColor];
|
|
147
180
|
}
|
|
148
181
|
|
|
149
182
|
// ---------------------------------------------------------------------------
|
|
150
|
-
//
|
|
183
|
+
// Display width helper — accounts for wide Unicode and ANSI escapes
|
|
151
184
|
// ---------------------------------------------------------------------------
|
|
152
185
|
|
|
153
|
-
|
|
186
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape stripping requires \x1b
|
|
187
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
188
|
+
|
|
189
|
+
function isWideChar(cp: number): boolean {
|
|
190
|
+
return (
|
|
191
|
+
cp === 0x26c1 || // ⛁ draughts man
|
|
192
|
+
cp === 0x23f1 || // ⏱ stopwatch
|
|
193
|
+
(cp >= 0x2600 && cp <= 0x26ff) || // misc symbols
|
|
194
|
+
(cp >= 0x2700 && cp <= 0x27bf) || // dingbats
|
|
195
|
+
(cp >= 0x1f300 && cp <= 0x1faff) || // emoji
|
|
196
|
+
(cp >= 0xfe00 && cp <= 0xfe0f) || // variation selectors
|
|
197
|
+
(cp >= 0x4e00 && cp <= 0x9fff) || // CJK
|
|
198
|
+
(cp >= 0x3000 && cp <= 0x303f) // CJK symbols
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function truncateToWidth(str: string, maxWidth: number): string {
|
|
203
|
+
// Walk through string preserving ANSI codes (zero-width) and measuring visible chars
|
|
204
|
+
let w = 0;
|
|
205
|
+
let lastAnsiEnd = 0;
|
|
206
|
+
const segments: string[] = [];
|
|
207
|
+
const raw = str;
|
|
208
|
+
|
|
209
|
+
// Split into text and ANSI segments, tracking width
|
|
210
|
+
let truncated = false;
|
|
211
|
+
ANSI_RE.lastIndex = 0;
|
|
212
|
+
let match = ANSI_RE.exec(raw);
|
|
213
|
+
while (match !== null) {
|
|
214
|
+
// Text before this ANSI code
|
|
215
|
+
const textBefore = raw.slice(lastAnsiEnd, match.index);
|
|
216
|
+
for (const ch of textBefore) {
|
|
217
|
+
const cw = isWideChar(ch.codePointAt(0) ?? 0) ? 2 : 1;
|
|
218
|
+
if (w + cw > maxWidth) {
|
|
219
|
+
truncated = true;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
w += cw;
|
|
223
|
+
segments.push(ch);
|
|
224
|
+
}
|
|
225
|
+
if (truncated) break;
|
|
226
|
+
segments.push(match[0]); // ANSI code (zero width)
|
|
227
|
+
lastAnsiEnd = match.index + match[0].length;
|
|
228
|
+
match = ANSI_RE.exec(raw);
|
|
229
|
+
}
|
|
230
|
+
if (!truncated) {
|
|
231
|
+
// Remaining text after last ANSI
|
|
232
|
+
const remaining = raw.slice(lastAnsiEnd);
|
|
233
|
+
for (const ch of remaining) {
|
|
234
|
+
const cw = isWideChar(ch.codePointAt(0) ?? 0) ? 2 : 1;
|
|
235
|
+
if (w + cw > maxWidth) break;
|
|
236
|
+
w += cw;
|
|
237
|
+
segments.push(ch);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return segments.join("") + RESET;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// Separator (dynamic width)
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
function makeSeparator(termWidth: number): string {
|
|
248
|
+
const w = Math.min(termWidth, 120);
|
|
249
|
+
return `${SLATE_600}${"─".repeat(w)}${RESET}`;
|
|
250
|
+
}
|
|
154
251
|
|
|
155
252
|
// ---------------------------------------------------------------------------
|
|
156
253
|
// Context section
|
|
157
254
|
// ---------------------------------------------------------------------------
|
|
158
255
|
|
|
159
256
|
function shortenModel(name: string): string {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
257
|
+
const replacements: [string, string][] = [
|
|
258
|
+
["claude-opus-4-6", "opus-4.6"],
|
|
259
|
+
["claude-opus-4-5", "opus-4.5"],
|
|
260
|
+
["claude-sonnet-4", "sonnet-4"],
|
|
261
|
+
["claude-3-5-sonnet", "sonnet-3.5"],
|
|
262
|
+
["claude-3-5-haiku", "haiku-3.5"],
|
|
263
|
+
["claude-", ""],
|
|
264
|
+
];
|
|
265
|
+
let result = name;
|
|
266
|
+
for (const [old, replacement] of replacements) {
|
|
267
|
+
result = result.replace(old, replacement);
|
|
268
|
+
}
|
|
269
|
+
return result;
|
|
173
270
|
}
|
|
174
271
|
|
|
175
272
|
function renderContext(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
273
|
+
mode: string,
|
|
274
|
+
termWidth: number,
|
|
275
|
+
contextPct: number,
|
|
276
|
+
contextK: number,
|
|
277
|
+
maxK: number,
|
|
278
|
+
modelName: string,
|
|
182
279
|
): void {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
280
|
+
let pctColor: string;
|
|
281
|
+
if (contextPct <= 33) pctColor = EMERALD;
|
|
282
|
+
else if (contextPct <= 66) pctColor = AMBER;
|
|
283
|
+
else pctColor = ROSE;
|
|
284
|
+
|
|
285
|
+
const shortModel = shortenModel(modelName);
|
|
286
|
+
let line: string;
|
|
287
|
+
|
|
288
|
+
switch (mode) {
|
|
289
|
+
case "micro": {
|
|
290
|
+
const [bar] = renderContextBar(6, contextPct);
|
|
291
|
+
line =
|
|
292
|
+
`${CTX_PRIMARY}\u25C9${RESET} ${CTX_ACCENT}${shortModel}${RESET} ` +
|
|
293
|
+
`${SLATE_600}\u2502${RESET} ` +
|
|
294
|
+
`${bar} ${pctColor}${contextPct}%${RESET} ${SLATE_500}(${contextK}k)${RESET}`;
|
|
295
|
+
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
case "mini": {
|
|
299
|
+
const [bar] = renderContextBar(8, contextPct);
|
|
300
|
+
line =
|
|
301
|
+
`${CTX_PRIMARY}\u25C9${RESET} ${CTX_ACCENT}${shortModel}${RESET} ` +
|
|
302
|
+
`${SLATE_600}\u2502${RESET} ` +
|
|
303
|
+
`${CTX_SECONDARY}CTX:${RESET} ${bar} ` +
|
|
304
|
+
`${pctColor}${contextPct}%${RESET} ${SLATE_500}(${contextK}k/${maxK}k)${RESET}`;
|
|
305
|
+
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
case "nano": {
|
|
309
|
+
const [bar] = renderContextBar(5, contextPct);
|
|
310
|
+
line =
|
|
311
|
+
`${CTX_PRIMARY}\u25C9${RESET} ${CTX_ACCENT}${shortModel}${RESET} ` +
|
|
312
|
+
`${bar} ${pctColor}${contextPct}%${RESET}`;
|
|
313
|
+
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
default: {
|
|
317
|
+
// Calculate how many bar buckets fit. Each bucket = ⛁ (2 cols) + space (1 col) = 3 cols.
|
|
318
|
+
// Fixed parts: "◉ Model: {model} │ Context: {pct}% ({Xk}/{Yk})"
|
|
319
|
+
const fixedWidth =
|
|
320
|
+
2 +
|
|
321
|
+
8 +
|
|
322
|
+
shortModel.length +
|
|
323
|
+
3 +
|
|
324
|
+
10 +
|
|
325
|
+
1 +
|
|
326
|
+
`${contextPct}`.length +
|
|
327
|
+
2 +
|
|
328
|
+
`${contextK}k/${maxK}k`.length +
|
|
329
|
+
2;
|
|
330
|
+
const availableForBar = termWidth - fixedWidth;
|
|
331
|
+
const buckets = Math.max(
|
|
332
|
+
4,
|
|
333
|
+
Math.min(24, Math.floor(availableForBar / 3)),
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const [bar, lastColor] = renderContextBar(buckets, contextPct);
|
|
337
|
+
line =
|
|
338
|
+
`${CTX_PRIMARY}\u25C9${RESET} ${CTX_SECONDARY}Model:${RESET} ${CTX_ACCENT}${shortModel}${RESET} ` +
|
|
339
|
+
`${SLATE_600}\u2502${RESET} ` +
|
|
340
|
+
`${CTX_SECONDARY}Context:${RESET} ${bar} ` +
|
|
341
|
+
`${lastColor}${contextPct}%${RESET} ${SLATE_500}(${contextK}k/${maxK}k)${RESET}`;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
console.log(truncateToWidth(line, termWidth));
|
|
346
|
+
console.log(makeSeparator(termWidth));
|
|
238
347
|
}
|
|
239
348
|
|
|
240
349
|
// ---------------------------------------------------------------------------
|
|
@@ -242,205 +351,214 @@ function renderContext(
|
|
|
242
351
|
// ---------------------------------------------------------------------------
|
|
243
352
|
|
|
244
353
|
interface GitStatus {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
354
|
+
branch: string;
|
|
355
|
+
modified: number;
|
|
356
|
+
staged: number;
|
|
357
|
+
untracked: number;
|
|
358
|
+
stash_count: number;
|
|
359
|
+
ahead: number;
|
|
360
|
+
behind: number;
|
|
361
|
+
age_display: string;
|
|
362
|
+
age_color: string;
|
|
254
363
|
}
|
|
255
364
|
|
|
256
365
|
function runGit(args: string[], cwd: string, timeout = 2000): string | null {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
366
|
+
try {
|
|
367
|
+
const result = execFileSync("git", args, {
|
|
368
|
+
cwd,
|
|
369
|
+
timeout,
|
|
370
|
+
encoding: "utf-8",
|
|
371
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
372
|
+
windowsHide: true,
|
|
373
|
+
});
|
|
374
|
+
return result.trim();
|
|
375
|
+
} catch {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
269
378
|
}
|
|
270
379
|
|
|
271
380
|
function getGitStatus(cwd: string): GitStatus | null {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
381
|
+
// One call replaces 6: branch, modified, staged, untracked, ahead/behind, and repo check.
|
|
382
|
+
// porcelain=v2 -b gives structured header lines + per-file XY status codes.
|
|
383
|
+
const porcelain = runGit(["status", "--porcelain=v2", "-b"], cwd);
|
|
384
|
+
if (porcelain === null) return null;
|
|
385
|
+
|
|
386
|
+
const status: GitStatus = {
|
|
387
|
+
branch: "detached",
|
|
388
|
+
modified: 0,
|
|
389
|
+
staged: 0,
|
|
390
|
+
untracked: 0,
|
|
391
|
+
stash_count: 0,
|
|
392
|
+
ahead: 0,
|
|
393
|
+
behind: 0,
|
|
394
|
+
age_display: "",
|
|
395
|
+
age_color: GIT_AGE_FRESH,
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
for (const line of porcelain.split(/\r?\n/)) {
|
|
399
|
+
if (line.startsWith("# branch.head ")) {
|
|
400
|
+
const b = line.slice(14).trim();
|
|
401
|
+
status.branch = b === "(detached)" ? "detached" : b;
|
|
402
|
+
} else if (line.startsWith("# branch.ab ")) {
|
|
403
|
+
// Format: "+<ahead> -<behind>"
|
|
404
|
+
const m = line.match(/\+(\d+) -(\d+)/);
|
|
405
|
+
if (m) {
|
|
406
|
+
status.ahead = parseInt(m[1] ?? "0", 10);
|
|
407
|
+
status.behind = parseInt(m[2] ?? "0", 10);
|
|
408
|
+
}
|
|
409
|
+
} else if (line.startsWith("1 ") || line.startsWith("2 ")) {
|
|
410
|
+
// Changed tracked file: "1 XY ..." where X=staged Y=unstaged, '.'=unchanged
|
|
411
|
+
const x = line[2] ?? ".";
|
|
412
|
+
const y = line[3] ?? ".";
|
|
413
|
+
if (x !== ".") status.staged++;
|
|
414
|
+
if (y !== ".") status.modified++;
|
|
415
|
+
} else if (line.startsWith("? ")) {
|
|
416
|
+
status.untracked++;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Stash count (no equivalent in status output)
|
|
421
|
+
const stash = runGit(["stash", "list"], cwd);
|
|
422
|
+
if (stash) status.stash_count = stash.split(/\r?\n/).filter(Boolean).length;
|
|
423
|
+
|
|
424
|
+
// Commit age
|
|
425
|
+
const log = runGit(["log", "-1", "--format=%ct"], cwd);
|
|
426
|
+
if (log) {
|
|
427
|
+
try {
|
|
428
|
+
const lastEpoch = parseInt(log, 10);
|
|
429
|
+
const nowEpoch = Math.floor(Date.now() / 1000);
|
|
430
|
+
const ageSec = nowEpoch - lastEpoch;
|
|
431
|
+
const ageMin = Math.floor(ageSec / 60);
|
|
432
|
+
const ageHrs = Math.floor(ageSec / 3600);
|
|
433
|
+
const ageDays = Math.floor(ageSec / 86_400);
|
|
434
|
+
|
|
435
|
+
if (ageMin < 1) {
|
|
436
|
+
status.age_display = "now";
|
|
437
|
+
status.age_color = GIT_AGE_FRESH;
|
|
438
|
+
} else if (ageHrs < 1) {
|
|
439
|
+
status.age_display = `${ageMin}m`;
|
|
440
|
+
status.age_color = GIT_AGE_FRESH;
|
|
441
|
+
} else if (ageHrs < 24) {
|
|
442
|
+
status.age_display = `${ageHrs}h`;
|
|
443
|
+
status.age_color = GIT_AGE_RECENT;
|
|
444
|
+
} else if (ageDays < 7) {
|
|
445
|
+
status.age_display = `${ageDays}d`;
|
|
446
|
+
status.age_color = GIT_AGE_STALE;
|
|
447
|
+
} else {
|
|
448
|
+
status.age_display = `${ageDays}d`;
|
|
449
|
+
status.age_color = GIT_AGE_OLD;
|
|
450
|
+
}
|
|
451
|
+
} catch {
|
|
452
|
+
/* ignore */
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return status;
|
|
346
457
|
}
|
|
347
458
|
|
|
348
|
-
function renderGit(
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
459
|
+
function renderGit(
|
|
460
|
+
mode: string,
|
|
461
|
+
termWidth: number,
|
|
462
|
+
git: GitStatus | null,
|
|
463
|
+
dirName: string,
|
|
464
|
+
): void {
|
|
465
|
+
const totalChanged = git ? git.modified + git.staged : 0;
|
|
466
|
+
const statusIcon =
|
|
467
|
+
git && (totalChanged > 0 || git.untracked > 0) ? "*" : "\u2713";
|
|
468
|
+
|
|
469
|
+
switch (mode) {
|
|
470
|
+
case "micro": {
|
|
471
|
+
let line = `${GIT_PRIMARY}\u25C8${RESET} ${GIT_DIR}${dirName}${RESET}`;
|
|
472
|
+
if (git) {
|
|
473
|
+
line += ` ${GIT_VALUE}${git.branch}${RESET}`;
|
|
474
|
+
if (git.age_display) {
|
|
475
|
+
line += ` ${git.age_color}${git.age_display}${RESET}`;
|
|
476
|
+
}
|
|
477
|
+
line += " ";
|
|
478
|
+
if (statusIcon === "\u2713") {
|
|
479
|
+
line += `${GIT_CLEAN}${statusIcon}${RESET}`;
|
|
480
|
+
} else {
|
|
481
|
+
line += `${GIT_MODIFIED}${statusIcon}${totalChanged}${RESET}`;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
console.log(truncateToWidth(line, termWidth));
|
|
485
|
+
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
case "mini": {
|
|
489
|
+
let line = `${GIT_PRIMARY}\u25C8${RESET} ${GIT_DIR}${dirName}${RESET}`;
|
|
490
|
+
if (git) {
|
|
491
|
+
line += ` ${SLATE_600}\u2502${RESET} ${GIT_VALUE}${git.branch}${RESET}`;
|
|
492
|
+
if (git.age_display) {
|
|
493
|
+
line += ` ${SLATE_600}\u2502${RESET} ${git.age_color}${git.age_display}${RESET}`;
|
|
494
|
+
}
|
|
495
|
+
line += ` ${SLATE_600}\u2502${RESET} `;
|
|
496
|
+
if (statusIcon === "\u2713") {
|
|
497
|
+
line += `${GIT_CLEAN}${statusIcon}${RESET}`;
|
|
498
|
+
} else {
|
|
499
|
+
line += `${GIT_MODIFIED}${statusIcon}${totalChanged}${RESET}`;
|
|
500
|
+
if (git.untracked > 0) {
|
|
501
|
+
line += ` ${GIT_ADDED}+${git.untracked}${RESET}`;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
console.log(truncateToWidth(line, termWidth));
|
|
506
|
+
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
case "nano": {
|
|
510
|
+
let line = `${GIT_PRIMARY}\u25C8${RESET} ${GIT_DIR}${dirName}${RESET}`;
|
|
511
|
+
if (git) {
|
|
512
|
+
line += ` ${GIT_VALUE}${git.branch}${RESET} `;
|
|
513
|
+
if (statusIcon === "\u2713") {
|
|
514
|
+
line += `${GIT_CLEAN}\u2713${RESET}`;
|
|
515
|
+
} else {
|
|
516
|
+
line += `${GIT_MODIFIED}*${totalChanged}${RESET}`;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
console.log(truncateToWidth(line, termWidth));
|
|
520
|
+
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
default: {
|
|
524
|
+
let line = `${GIT_PRIMARY}\u25C8${RESET} ${GIT_PRIMARY}PWD:${RESET} ${GIT_DIR}${dirName}${RESET}`;
|
|
525
|
+
if (git) {
|
|
526
|
+
line +=
|
|
527
|
+
` ${SLATE_600}\u2502${RESET} ` +
|
|
528
|
+
`${GIT_PRIMARY}Branch:${RESET} ${GIT_VALUE}${git.branch}${RESET}`;
|
|
529
|
+
if (git.age_display) {
|
|
530
|
+
line += ` ${SLATE_600}\u2502${RESET} ${GIT_PRIMARY}Age:${RESET} ${git.age_color}${git.age_display}${RESET}`;
|
|
531
|
+
}
|
|
532
|
+
if (git.stash_count > 0) {
|
|
533
|
+
line += ` ${SLATE_600}\u2502${RESET} ${GIT_PRIMARY}Stash:${RESET} ${GIT_STASH}${git.stash_count}${RESET}`;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (totalChanged > 0 || git.untracked > 0) {
|
|
537
|
+
line += ` ${SLATE_600}\u2502${RESET} `;
|
|
538
|
+
if (totalChanged > 0) {
|
|
539
|
+
line += `${GIT_PRIMARY}Mod:${RESET} ${GIT_MODIFIED}${totalChanged}${RESET}`;
|
|
540
|
+
}
|
|
541
|
+
if (git.untracked > 0) {
|
|
542
|
+
if (totalChanged > 0) line += " ";
|
|
543
|
+
line += `${GIT_PRIMARY}New:${RESET} ${GIT_ADDED}${git.untracked}${RESET}`;
|
|
544
|
+
}
|
|
545
|
+
} else {
|
|
546
|
+
line += ` ${SLATE_600}\u2502${RESET} ${GIT_CLEAN}\u2713 clean${RESET}`;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (git.ahead > 0 || git.behind > 0) {
|
|
550
|
+
line += ` ${SLATE_600}\u2502${RESET} ${GIT_PRIMARY}Sync:${RESET} `;
|
|
551
|
+
if (git.ahead > 0) {
|
|
552
|
+
line += `${GIT_CLEAN}\u2191${git.ahead}${RESET}`;
|
|
553
|
+
}
|
|
554
|
+
if (git.behind > 0) {
|
|
555
|
+
line += `${GIT_STASH}\u2193${git.behind}${RESET}`;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
console.log(truncateToWidth(line, termWidth));
|
|
560
|
+
}
|
|
561
|
+
}
|
|
444
562
|
}
|
|
445
563
|
|
|
446
564
|
// ---------------------------------------------------------------------------
|
|
@@ -448,116 +566,131 @@ function renderGit(mode: string, git: GitStatus | null, dirName: string): void {
|
|
|
448
566
|
// ---------------------------------------------------------------------------
|
|
449
567
|
|
|
450
568
|
function findActivePlanFile(): string | null {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
569
|
+
try {
|
|
570
|
+
const plansDir = path.join(homedir(), ".claude", "plans");
|
|
571
|
+
if (!fs.existsSync(plansDir)) return null;
|
|
572
|
+
const planFiles = fs
|
|
573
|
+
.readdirSync(plansDir)
|
|
574
|
+
.filter((f) => f.endsWith(".md"))
|
|
575
|
+
.map((f) => {
|
|
576
|
+
const fullPath = path.join(plansDir, f);
|
|
577
|
+
return { path: fullPath, mtime: fs.statSync(fullPath).mtimeMs };
|
|
578
|
+
})
|
|
579
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
580
|
+
return planFiles.length > 0 ? (planFiles[0]?.path ?? null) : null;
|
|
581
|
+
} catch {
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
465
584
|
}
|
|
466
585
|
|
|
467
586
|
function renderContextManager(
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
587
|
+
mode: string,
|
|
588
|
+
contextId: string,
|
|
589
|
+
contextState: Record<string, unknown> | null,
|
|
471
590
|
): void {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
591
|
+
// Strip YYMMDD-HHMM- timestamp prefix from context ID for display
|
|
592
|
+
let displayId = contextId.replace(/^\d{6}-\d{4}-/, "");
|
|
593
|
+
if (!displayId) displayId = contextId;
|
|
594
|
+
|
|
595
|
+
// Truncate display_id per mode
|
|
596
|
+
const maxIdLen: Record<string, number> = {
|
|
597
|
+
nano: 14,
|
|
598
|
+
micro: 18,
|
|
599
|
+
mini: 22,
|
|
600
|
+
normal: 30,
|
|
601
|
+
};
|
|
602
|
+
const maxLen = maxIdLen[mode] ?? 30;
|
|
603
|
+
let truncatedId = displayId.slice(0, maxLen);
|
|
604
|
+
if (displayId.length > maxLen) truncatedId += "\u2026";
|
|
605
|
+
|
|
606
|
+
// Read state fields
|
|
607
|
+
const stateMode = contextState?.mode ?? "idle";
|
|
608
|
+
const statePlanPath = contextState?.plan_path ?? null;
|
|
609
|
+
|
|
610
|
+
// Detect plan mode heuristic
|
|
611
|
+
const activePlanFile = findActivePlanFile();
|
|
612
|
+
const isPlanning = stateMode === "idle" && activePlanFile !== null;
|
|
613
|
+
|
|
614
|
+
// Build mode badge
|
|
615
|
+
let modeBadge = "";
|
|
616
|
+
if (isPlanning) {
|
|
617
|
+
const label = mode === "nano" ? "Plan" : "Planning";
|
|
618
|
+
modeBadge = ` ${SLATE_600}\u2502${RESET} ${CTX_SECONDARY}Mode:${RESET} ${AMBER}${label}${RESET}`;
|
|
619
|
+
} else if (stateMode === "has_plan") {
|
|
620
|
+
const label = mode === "nano" ? "Ready" : "Plan Ready";
|
|
621
|
+
modeBadge = ` ${SLATE_600}\u2502${RESET} ${CTX_SECONDARY}Mode:${RESET} ${EMERALD}${label}${RESET}`;
|
|
622
|
+
} else if (stateMode === "active") {
|
|
623
|
+
const label = "Active";
|
|
624
|
+
modeBadge = ` ${SLATE_600}\u2502${RESET} ${CTX_SECONDARY}Mode:${RESET} ${CTX_ACCENT}${label}${RESET}`;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Resolve plan file path for display
|
|
628
|
+
let planFilePath: string | null = null;
|
|
629
|
+
if (isPlanning) {
|
|
630
|
+
planFilePath = activePlanFile;
|
|
631
|
+
} else if (statePlanPath) {
|
|
632
|
+
planFilePath = statePlanPath;
|
|
633
|
+
} else if (stateMode === "has_plan" || stateMode === "active") {
|
|
634
|
+
try {
|
|
635
|
+
planFilePath = findLatestPlan(contextId) ?? null;
|
|
636
|
+
} catch {
|
|
637
|
+
/* ignore */
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Build plan name (mini/normal only)
|
|
642
|
+
let planPart = "";
|
|
643
|
+
if ((mode === "mini" || mode === "normal") && planFilePath) {
|
|
644
|
+
const planStem = path
|
|
645
|
+
.basename(planFilePath, path.extname(planFilePath))
|
|
646
|
+
.replace(/^\d{4}-\d{2}-\d{2}-(\d{4}-)?/, "");
|
|
647
|
+
const maxPlanLen = mode === "mini" ? 20 : 30;
|
|
648
|
+
let truncatedPlan = planStem.slice(0, maxPlanLen);
|
|
649
|
+
if (planStem.length > maxPlanLen) truncatedPlan += "\u2026";
|
|
650
|
+
planPart = ` ${SLATE_600}\u2502${RESET} ${CTX_SECONDARY}Plan:${RESET} ${SLATE_300}${truncatedPlan}${RESET}`;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
switch (mode) {
|
|
654
|
+
case "micro": {
|
|
655
|
+
console.log(
|
|
656
|
+
`${CTX_ACCENT}\u25C6${RESET} ${SLATE_400}${truncatedId}${RESET}${modeBadge}`,
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
case "mini": {
|
|
662
|
+
console.log(
|
|
663
|
+
`${CTX_ACCENT}\u25C6${RESET} ${SLATE_400}${truncatedId}${RESET}` +
|
|
664
|
+
`${modeBadge}${planPart}`,
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
break;
|
|
668
|
+
}
|
|
669
|
+
case "nano": {
|
|
670
|
+
console.log(
|
|
671
|
+
`${CTX_ACCENT}\u25C6${RESET} ${SLATE_400}${truncatedId}${RESET}${modeBadge}`,
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
676
|
+
default: {
|
|
677
|
+
console.log(
|
|
678
|
+
`${CTX_ACCENT}\u25C6${RESET} ${CTX_SECONDARY}Context:${RESET} ${SLATE_300}${truncatedId}${RESET}` +
|
|
679
|
+
`${modeBadge}${planPart}`,
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
552
683
|
}
|
|
553
684
|
|
|
554
685
|
function renderNoContext(mode: string): void {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
686
|
+
const warn = `${ROSE}\u26A0 ${RESET}`;
|
|
687
|
+
if (mode === "normal") {
|
|
688
|
+
console.log(
|
|
689
|
+
`${warn} ${ROSE}NO CONTEXT${RESET} ${SLATE_500}\u2014 type ^ for context manager${RESET}`,
|
|
690
|
+
);
|
|
691
|
+
} else {
|
|
692
|
+
console.log(`${warn} ${ROSE}NO CONTEXT${RESET}`);
|
|
693
|
+
}
|
|
561
694
|
}
|
|
562
695
|
|
|
563
696
|
// ---------------------------------------------------------------------------
|
|
@@ -565,67 +698,80 @@ function renderNoContext(mode: string): void {
|
|
|
565
698
|
// ---------------------------------------------------------------------------
|
|
566
699
|
|
|
567
700
|
interface StatuslineCache {
|
|
568
|
-
|
|
701
|
+
sessions?: Record<string, { context_id: string | null }>;
|
|
569
702
|
}
|
|
570
703
|
|
|
571
704
|
function loadCache(): StatuslineCache {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
705
|
+
try {
|
|
706
|
+
if (fs.existsSync(STATUSLINE_CACHE)) {
|
|
707
|
+
return JSON.parse(fs.readFileSync(STATUSLINE_CACHE, "utf-8"));
|
|
708
|
+
}
|
|
709
|
+
} catch {
|
|
710
|
+
/* ignore */
|
|
711
|
+
}
|
|
712
|
+
return {};
|
|
578
713
|
}
|
|
579
714
|
|
|
580
715
|
function saveCache(cache: StatuslineCache): void {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
716
|
+
try {
|
|
717
|
+
fs.mkdirSync(path.dirname(STATUSLINE_CACHE), { recursive: true });
|
|
718
|
+
fs.writeFileSync(STATUSLINE_CACHE, JSON.stringify(cache, null, 2), "utf-8");
|
|
719
|
+
} catch {
|
|
720
|
+
/* ignore */
|
|
721
|
+
}
|
|
585
722
|
}
|
|
586
723
|
|
|
587
724
|
function resolveContextId(sessionId: string): string | null {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
725
|
+
if (!sessionId || sessionId === "unknown") return null;
|
|
726
|
+
|
|
727
|
+
// Check cache first
|
|
728
|
+
const cache = loadCache();
|
|
729
|
+
const cachedEntry = cache.sessions?.[sessionId];
|
|
730
|
+
if (cachedEntry && cachedEntry.context_id !== undefined) {
|
|
731
|
+
return cachedEntry.context_id;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Cache miss — look up via context manager
|
|
735
|
+
try {
|
|
736
|
+
const context = getContextBySessionId(sessionId);
|
|
737
|
+
if (context) {
|
|
738
|
+
if (!cache.sessions) cache.sessions = {};
|
|
739
|
+
const ctxId = (context as Record<string, unknown>).id as string;
|
|
740
|
+
cache.sessions[sessionId] = { context_id: ctxId };
|
|
741
|
+
saveCache(cache);
|
|
742
|
+
return ctxId;
|
|
743
|
+
}
|
|
744
|
+
} catch {
|
|
745
|
+
/* ignore */
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Don't cache negative results — context may be bound by a later hook
|
|
749
|
+
return null;
|
|
610
750
|
}
|
|
611
751
|
|
|
612
|
-
function loadContextState(contextId: string): Record<string,
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
752
|
+
function loadContextState(contextId: string): Record<string, unknown> | null {
|
|
753
|
+
try {
|
|
754
|
+
return loadState(contextId) as Record<string, unknown> | null;
|
|
755
|
+
} catch {
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
618
758
|
}
|
|
619
759
|
|
|
620
|
-
function writeContextWindow(
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
760
|
+
function writeContextWindow(
|
|
761
|
+
contextId: string,
|
|
762
|
+
contextWindowData: Record<string, unknown>,
|
|
763
|
+
): void {
|
|
764
|
+
try {
|
|
765
|
+
const state = getContext(contextId) as Record<string, unknown> | null;
|
|
766
|
+
if (state) {
|
|
767
|
+
if (!state.last_session) state.last_session = {};
|
|
768
|
+
state.last_session.context_remaining_pct =
|
|
769
|
+
contextWindowData.remaining_percentage;
|
|
770
|
+
saveState(contextId, state as unknown);
|
|
771
|
+
}
|
|
772
|
+
} catch {
|
|
773
|
+
/* ignore */
|
|
774
|
+
}
|
|
629
775
|
}
|
|
630
776
|
|
|
631
777
|
// ---------------------------------------------------------------------------
|
|
@@ -633,94 +779,82 @@ function writeContextWindow(contextId: string, contextWindowData: Record<string,
|
|
|
633
779
|
// ---------------------------------------------------------------------------
|
|
634
780
|
|
|
635
781
|
function main(): void {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
if (contextId) {
|
|
713
|
-
writeContextWindow(contextId, {
|
|
714
|
-
used_percentage: contextPct,
|
|
715
|
-
remaining_percentage: 100 - contextPct,
|
|
716
|
-
context_window_size: contextMax,
|
|
717
|
-
tokens_used: contextUsed,
|
|
718
|
-
total_input_tokens: totalInput,
|
|
719
|
-
total_output_tokens: outputTokens,
|
|
720
|
-
model: modelName,
|
|
721
|
-
last_updated: new Date().toISOString().split(".")[0],
|
|
722
|
-
});
|
|
723
|
-
}
|
|
782
|
+
// Read JSON from stdin
|
|
783
|
+
let inputData: Record<string, unknown>;
|
|
784
|
+
try {
|
|
785
|
+
inputData = JSON.parse(fs.readFileSync(0, "utf-8"));
|
|
786
|
+
} catch {
|
|
787
|
+
inputData = {};
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Terminal width and mode
|
|
791
|
+
const termWidth = getTerminalWidth();
|
|
792
|
+
const mode = getDisplayMode(termWidth);
|
|
793
|
+
|
|
794
|
+
// Extract input fields
|
|
795
|
+
const sessionId = inputData.session_id ?? "";
|
|
796
|
+
const modelName = inputData.model?.display_name ?? "unknown";
|
|
797
|
+
const workspace = inputData.workspace ?? {};
|
|
798
|
+
const currentDir: string = workspace.project_dir ?? process.cwd();
|
|
799
|
+
const dirName = path.basename(currentDir);
|
|
800
|
+
|
|
801
|
+
// Context window data
|
|
802
|
+
const ctxWin = inputData.context_window ?? {};
|
|
803
|
+
const usage = ctxWin.current_usage ?? {};
|
|
804
|
+
const cacheRead: number = usage.cache_read_input_tokens ?? 0;
|
|
805
|
+
const inputTokens: number = usage.input_tokens ?? 0;
|
|
806
|
+
const cacheCreation: number = usage.cache_creation_input_tokens ?? 0;
|
|
807
|
+
const outputTokens: number = usage.output_tokens ?? 0;
|
|
808
|
+
const contextMax: number = ctxWin.context_window_size ?? 200_000;
|
|
809
|
+
|
|
810
|
+
// Calculate context percentage
|
|
811
|
+
const usedPct = ctxWin.used_percentage;
|
|
812
|
+
let contextPct: number;
|
|
813
|
+
const totalInput = cacheRead + inputTokens + cacheCreation;
|
|
814
|
+
const contextUsed = totalInput + outputTokens + CONTEXT_BASELINE_TOKENS;
|
|
815
|
+
|
|
816
|
+
if (usedPct !== undefined && usedPct !== null) {
|
|
817
|
+
contextPct = Math.floor(usedPct);
|
|
818
|
+
} else {
|
|
819
|
+
contextPct =
|
|
820
|
+
contextMax > 0 ? Math.floor((contextUsed * 100) / contextMax) : 0;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
const contextK = Math.floor(contextUsed / 1000);
|
|
824
|
+
const maxK = Math.floor(contextMax / 1000);
|
|
825
|
+
|
|
826
|
+
// Resolve context ID for display and persistence
|
|
827
|
+
const contextId = resolveContextId(sessionId);
|
|
828
|
+
|
|
829
|
+
// Render context section
|
|
830
|
+
renderContext(mode, termWidth, contextPct, contextK, maxK, modelName);
|
|
831
|
+
|
|
832
|
+
// Render PWD + git section (PWD always shown, git stats only when in a repo)
|
|
833
|
+
const git = getGitStatus(currentDir);
|
|
834
|
+
renderGit(mode, termWidth, git, dirName);
|
|
835
|
+
|
|
836
|
+
// Render context manager line (line 3) with separator
|
|
837
|
+
console.log(makeSeparator(termWidth));
|
|
838
|
+
if (contextId) {
|
|
839
|
+
const contextState = loadContextState(contextId);
|
|
840
|
+
renderContextManager(mode, contextId, contextState);
|
|
841
|
+
} else {
|
|
842
|
+
renderNoContext(mode);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Persist context_window to state.json
|
|
846
|
+
if (contextId) {
|
|
847
|
+
writeContextWindow(contextId, {
|
|
848
|
+
used_percentage: contextPct,
|
|
849
|
+
remaining_percentage: 100 - contextPct,
|
|
850
|
+
context_window_size: contextMax,
|
|
851
|
+
tokens_used: contextUsed,
|
|
852
|
+
total_input_tokens: totalInput,
|
|
853
|
+
total_output_tokens: outputTokens,
|
|
854
|
+
model: modelName,
|
|
855
|
+
last_updated: new Date().toISOString().split(".")[0],
|
|
856
|
+
});
|
|
857
|
+
}
|
|
724
858
|
}
|
|
725
859
|
|
|
726
860
|
main();
|