aiwcli 0.12.6 → 0.12.7
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/bin/dev.cmd +3 -3
- package/bin/dev.js +16 -16
- package/bin/run.cmd +3 -3
- package/bin/run.js +21 -21
- package/dist/commands/branch.js +7 -2
- package/dist/lib/bmad-installer.js +37 -37
- package/dist/lib/terminal.d.ts +2 -0
- package/dist/lib/terminal.js +57 -7
- package/dist/templates/CLAUDE.md +205 -205
- package/dist/templates/_shared/.claude/commands/handoff-resume.md +12 -12
- package/dist/templates/_shared/.claude/commands/handoff.md +12 -12
- package/dist/templates/_shared/.claude/settings.json +65 -65
- package/dist/templates/_shared/.codex/workflows/handoff.md +226 -226
- package/dist/templates/_shared/.windsurf/workflows/handoff.md +226 -226
- package/dist/templates/_shared/handoff-system/CLAUDE.md +421 -421
- package/dist/templates/_shared/handoff-system/lib/document-generator.ts +215 -215
- package/dist/templates/_shared/handoff-system/lib/handoff-reader.ts +158 -158
- package/dist/templates/_shared/handoff-system/scripts/resume_handoff.ts +373 -373
- package/dist/templates/_shared/handoff-system/scripts/save_handoff.ts +469 -469
- package/dist/templates/_shared/handoff-system/workflows/handoff-resume.md +66 -66
- package/dist/templates/_shared/handoff-system/workflows/handoff.md +254 -254
- package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -2
- package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -159
- package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -147
- package/dist/templates/_shared/hooks-ts/file-suggestion.ts +128 -128
- package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -49
- package/dist/templates/_shared/hooks-ts/session_end.ts +196 -196
- package/dist/templates/_shared/hooks-ts/session_start.ts +163 -163
- package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -48
- package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -74
- package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +93 -93
- package/dist/templates/_shared/lib-ts/CLAUDE.md +367 -367
- package/dist/templates/_shared/lib-ts/base/atomic-write.ts +138 -138
- package/dist/templates/_shared/lib-ts/base/constants.ts +303 -303
- package/dist/templates/_shared/lib-ts/base/git-state.ts +58 -58
- package/dist/templates/_shared/lib-ts/base/hook-utils.ts +582 -582
- package/dist/templates/_shared/lib-ts/base/inference.ts +301 -301
- package/dist/templates/_shared/lib-ts/base/logger.ts +247 -247
- package/dist/templates/_shared/lib-ts/base/state-io.ts +202 -202
- package/dist/templates/_shared/lib-ts/base/stop-words.ts +184 -184
- package/dist/templates/_shared/lib-ts/base/utils.ts +184 -184
- package/dist/templates/_shared/lib-ts/context/context-formatter.ts +566 -566
- package/dist/templates/_shared/lib-ts/context/context-selector.ts +524 -524
- package/dist/templates/_shared/lib-ts/context/context-store.ts +712 -712
- package/dist/templates/_shared/lib-ts/context/plan-manager.ts +312 -312
- package/dist/templates/_shared/lib-ts/context/task-tracker.ts +185 -185
- package/dist/templates/_shared/lib-ts/package.json +20 -20
- package/dist/templates/_shared/lib-ts/templates/formatters.ts +102 -102
- package/dist/templates/_shared/lib-ts/templates/plan-context.ts +58 -58
- package/dist/templates/_shared/lib-ts/tsconfig.json +13 -13
- package/dist/templates/_shared/lib-ts/types.ts +186 -186
- package/dist/templates/_shared/scripts/resolve_context.ts +33 -33
- package/dist/templates/_shared/scripts/status_line.ts +690 -690
- package/dist/templates/cc-native/.claude/commands/cc-native/rlm/ask.md +136 -136
- package/dist/templates/cc-native/.claude/commands/cc-native/rlm/index.md +21 -21
- package/dist/templates/cc-native/.claude/commands/cc-native/rlm/overview.md +56 -56
- package/dist/templates/cc-native/.claude/commands/cc-native/specdev.md +10 -10
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/fix.md +8 -8
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/implement.md +8 -8
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/research.md +8 -8
- package/dist/templates/cc-native/CC-NATIVE-README.md +189 -189
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +304 -304
- package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +143 -143
- package/dist/templates/cc-native/_cc-native/agents/PLAN-ORCHESTRATOR.md +213 -213
- package/dist/templates/cc-native/_cc-native/agents/plan-questions/PLAN-QUESTIONER.md +70 -70
- package/dist/templates/cc-native/_cc-native/cc-native.config.json +96 -96
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +247 -247
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +76 -76
- package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_subagent.ts +54 -54
- package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_write.ts +51 -51
- package/dist/templates/cc-native/_cc-native/hooks/mark_questions_asked.ts +53 -53
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -61
- package/dist/templates/cc-native/_cc-native/lib-ts/agent-selection.ts +163 -163
- package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +156 -156
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/format.ts +597 -597
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/index.ts +26 -26
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/tracker.ts +107 -107
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/write.ts +119 -119
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +21 -21
- package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +319 -319
- package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +144 -144
- package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -57
- package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -83
- package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +119 -119
- package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +79 -79
- package/dist/templates/cc-native/_cc-native/lib-ts/graduation.ts +132 -132
- package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +116 -116
- package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +168 -168
- package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +70 -70
- package/dist/templates/cc-native/_cc-native/lib-ts/output-builder.ts +130 -130
- package/dist/templates/cc-native/_cc-native/lib-ts/plan-discovery.ts +80 -80
- package/dist/templates/cc-native/_cc-native/lib-ts/plan-enhancement.ts +41 -41
- package/dist/templates/cc-native/_cc-native/lib-ts/plan-questions.ts +101 -101
- package/dist/templates/cc-native/_cc-native/lib-ts/review-pipeline.ts +511 -511
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +71 -71
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/base/base-agent.ts +217 -217
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +12 -12
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/claude-agent.ts +66 -66
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/codex-agent.ts +184 -184
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/gemini-agent.ts +39 -39
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/orchestrator-claude-agent.ts +196 -196
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/schemas.ts +201 -201
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +21 -21
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/CLAUDE.md +480 -480
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/embedding-indexer.ts +287 -287
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/hyde.ts +148 -148
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/index.ts +54 -54
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/logger.ts +58 -58
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/ollama-client.ts +208 -208
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/retrieval-pipeline.ts +460 -460
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-indexer.ts +446 -446
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-loader.ts +280 -280
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-searcher.ts +274 -274
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/types.ts +201 -201
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/vector-store.ts +278 -278
- package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +184 -184
- package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +275 -275
- package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -18
- package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +329 -329
- package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -72
- package/dist/templates/cc-native/_cc-native/workflows/specdev.md +9 -9
- package/oclif.manifest.json +1 -1
- package/package.json +108 -108
- package/dist/templates/cc-native/_cc-native/lib-ts/nul +0 -3
|
@@ -1,720 +1,720 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* Status line for Claude Code sessions.
|
|
4
|
-
*
|
|
5
|
-
* Renders context window usage and git status with ANSI colors.
|
|
6
|
-
* Optionally persists context_window data to the session's state.json.
|
|
7
|
-
*
|
|
8
|
-
* Context and git sections only.
|
|
9
|
-
*
|
|
10
|
-
* Usage: echo '{"session_id":"...","model":{"display_name":"Opus"},...}' | bun status_line.ts
|
|
11
|
-
*/
|
|
12
|
-
import { execFileSync } from "node:child_process";
|
|
13
|
-
import * as fs from "node:fs";
|
|
14
|
-
import { homedir } from "node:os";
|
|
15
|
-
import * as path from "node:path";
|
|
16
|
-
|
|
17
|
-
import { CONTEXT_BASELINE_TOKENS } from "../lib-ts/base/hook-utils.js";
|
|
18
|
-
import { getContextBySessionId, getContext, loadState, saveState } from "../lib-ts/context/context-store.js";
|
|
19
|
-
import { findLatestPlan } from "../lib-ts/context/plan-manager.js";
|
|
20
|
-
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
// Path setup
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
const SCRIPT_DIR = path.dirname(new URL(import.meta.url).pathname);
|
|
25
|
-
const OUTPUT_DIR = path.join(".", "_output");
|
|
26
|
-
const CACHE_DIR = path.join(OUTPUT_DIR, "cache");
|
|
27
|
-
const STATUSLINE_CACHE = path.join(CACHE_DIR, ".statusline-cache.json");
|
|
28
|
-
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
// NO_COLOR support (https://no-color.org)
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
const NO_COLOR = Boolean(process.env.NO_COLOR);
|
|
33
|
-
|
|
34
|
-
const RESET = NO_COLOR ? "" : "\u001B[0m";
|
|
35
|
-
|
|
36
|
-
// Structural
|
|
37
|
-
const SLATE_300 = NO_COLOR ? "" : "\u001B[38;2;203;213;225m";
|
|
38
|
-
const SLATE_400 = NO_COLOR ? "" : "\u001B[38;2;148;163;184m";
|
|
39
|
-
const SLATE_500 = NO_COLOR ? "" : "\u001B[38;2;100;116;139m";
|
|
40
|
-
const SLATE_600 = NO_COLOR ? "" : "\u001B[38;2;71;85;105m";
|
|
41
|
-
|
|
42
|
-
// Semantic
|
|
43
|
-
const EMERALD = NO_COLOR ? "" : "\u001B[38;2;74;222;128m";
|
|
44
|
-
const ROSE = NO_COLOR ? "" : "\u001B[38;2;251;113;133m";
|
|
45
|
-
const AMBER = NO_COLOR ? "" : "\u001B[38;2;251;191;36m";
|
|
46
|
-
|
|
47
|
-
// Context colors
|
|
48
|
-
const CTX_PRIMARY = NO_COLOR ? "" : "\u001B[38;2;129;140;248m";
|
|
49
|
-
const CTX_SECONDARY = NO_COLOR ? "" : "\u001B[38;2;165;180;252m";
|
|
50
|
-
const CTX_ACCENT = NO_COLOR ? "" : "\u001B[38;2;139;92;246m";
|
|
51
|
-
const CTX_BUCKET_EMPTY = NO_COLOR ? "" : "\u001B[38;2;75;82;95m";
|
|
52
|
-
|
|
53
|
-
// Git colors
|
|
54
|
-
const GIT_PRIMARY = NO_COLOR ? "" : "\u001B[38;2;56;189;248m";
|
|
55
|
-
const GIT_VALUE = NO_COLOR ? "" : "\u001B[38;2;186;230;253m";
|
|
56
|
-
const GIT_DIR = NO_COLOR ? "" : "\u001B[38;2;147;197;253m";
|
|
57
|
-
const GIT_CLEAN = NO_COLOR ? "" : "\u001B[38;2;125;211;252m";
|
|
58
|
-
const GIT_MODIFIED = NO_COLOR ? "" : "\u001B[38;2;96;165;250m";
|
|
59
|
-
const GIT_ADDED = NO_COLOR ? "" : "\u001B[38;2;59;130;246m";
|
|
60
|
-
const GIT_STASH = NO_COLOR ? "" : "\u001B[38;2;165;180;252m";
|
|
61
|
-
const GIT_AGE_FRESH = NO_COLOR ? "" : "\u001B[38;2;125;211;252m";
|
|
62
|
-
const GIT_AGE_RECENT = NO_COLOR ? "" : "\u001B[38;2;96;165;250m";
|
|
63
|
-
const GIT_AGE_STALE = NO_COLOR ? "" : "\u001B[38;2;59;130;246m";
|
|
64
|
-
const GIT_AGE_OLD = NO_COLOR ? "" : "\u001B[38;2;99;102;241m";
|
|
65
|
-
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
// Display modes
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
|
-
|
|
70
|
-
function getTerminalWidth(): number {
|
|
71
|
-
const colsEnv = process.env.COLUMNS;
|
|
72
|
-
if (colsEnv) {
|
|
73
|
-
const cols = parseInt(colsEnv, 10);
|
|
74
|
-
if (cols > 0) return cols;
|
|
75
|
-
}
|
|
76
|
-
try {
|
|
77
|
-
if (process.stdout.columns && process.stdout.columns > 0) {
|
|
78
|
-
return process.stdout.columns;
|
|
79
|
-
}
|
|
80
|
-
} catch { /* ignore */ }
|
|
81
|
-
return 80;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function getDisplayMode(width: number): string {
|
|
85
|
-
if (width < 35) return "nano";
|
|
86
|
-
if (width < 55) return "micro";
|
|
87
|
-
if (width < 80) return "mini";
|
|
88
|
-
return "normal";
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// ---------------------------------------------------------------------------
|
|
92
|
-
// Color helpers
|
|
93
|
-
// ---------------------------------------------------------------------------
|
|
94
|
-
|
|
95
|
-
function getBucketColor(pos: number, maxPos: number): string {
|
|
96
|
-
if (NO_COLOR) return "";
|
|
97
|
-
const pct = Math.floor((pos * 100) / maxPos);
|
|
98
|
-
|
|
99
|
-
let b: number; let g: number; let r: number;
|
|
100
|
-
|
|
101
|
-
if (pct <= 33) {
|
|
102
|
-
r = 74 + Math.floor(((250 - 74) * pct) / 33);
|
|
103
|
-
g = 222 + Math.floor(((204 - 222) * pct) / 33);
|
|
104
|
-
b = 128 + Math.floor(((21 - 128) * pct) / 33);
|
|
105
|
-
} else if (pct <= 66) {
|
|
106
|
-
const t = pct - 33;
|
|
107
|
-
r = 250 + Math.floor(((251 - 250) * t) / 33);
|
|
108
|
-
g = 204 + Math.floor(((146 - 204) * t) / 33);
|
|
109
|
-
b = 21 + Math.floor(((60 - 21) * t) / 33);
|
|
110
|
-
} else {
|
|
111
|
-
const t = pct - 66;
|
|
112
|
-
r = 251 + Math.floor(((239 - 251) * t) / 34);
|
|
113
|
-
g = 146 + Math.floor(((68 - 146) * t) / 34);
|
|
114
|
-
b = 60 + Math.floor(((68 - 60) * t) / 34);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return `\u001B[38;2;${r};${g};${b}m`;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// ---------------------------------------------------------------------------
|
|
121
|
-
// Context bar rendering
|
|
122
|
-
// ---------------------------------------------------------------------------
|
|
123
|
-
|
|
124
|
-
function renderContextBar(width: number, pct: number): [string, string] {
|
|
125
|
-
pct = Math.max(0, Math.min(100, pct));
|
|
126
|
-
const filled = Math.floor((pct * width) / 100);
|
|
127
|
-
let lastColor = EMERALD;
|
|
128
|
-
const parts: string[] = [];
|
|
129
|
-
|
|
130
|
-
for (let i = 1; i <= width; i++) {
|
|
131
|
-
if (i <= filled) {
|
|
132
|
-
const color = getBucketColor(i, width);
|
|
133
|
-
lastColor = color;
|
|
134
|
-
parts.push(`${color}\u26C1${RESET}`);
|
|
135
|
-
} else {
|
|
136
|
-
parts.push(`${CTX_BUCKET_EMPTY}\u26C1${RESET}`);
|
|
137
|
-
}
|
|
138
|
-
if (width > 8) {
|
|
139
|
-
parts.push(" ");
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return [parts.join("").trimEnd(), lastColor];
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// ---------------------------------------------------------------------------
|
|
147
|
-
// Separator
|
|
148
|
-
// ---------------------------------------------------------------------------
|
|
149
|
-
|
|
150
|
-
const SEPARATOR = `${SLATE_600}${"─".repeat(72)}${RESET}`;
|
|
151
|
-
|
|
152
|
-
// ---------------------------------------------------------------------------
|
|
153
|
-
// Context section
|
|
154
|
-
// ---------------------------------------------------------------------------
|
|
155
|
-
|
|
156
|
-
function shortenModel(name: string): string {
|
|
157
|
-
const replacements: [string, string][] = [
|
|
158
|
-
["claude-opus-4-6", "opus-4.6"],
|
|
159
|
-
["claude-opus-4-5", "opus-4.5"],
|
|
160
|
-
["claude-sonnet-4", "sonnet-4"],
|
|
161
|
-
["claude-3-5-sonnet", "sonnet-3.5"],
|
|
162
|
-
["claude-3-5-haiku", "haiku-3.5"],
|
|
163
|
-
["claude-", ""],
|
|
164
|
-
];
|
|
165
|
-
let result = name;
|
|
166
|
-
for (const [old, replacement] of replacements) {
|
|
167
|
-
result = result.replace(old, replacement);
|
|
168
|
-
}
|
|
169
|
-
return result;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function renderContext(
|
|
173
|
-
mode: string,
|
|
174
|
-
contextPct: number,
|
|
175
|
-
contextK: number,
|
|
176
|
-
maxK: number,
|
|
177
|
-
timeDisplay: string,
|
|
178
|
-
modelName: string,
|
|
179
|
-
): void {
|
|
180
|
-
let pctColor: string;
|
|
181
|
-
if (contextPct <= 33) pctColor = EMERALD;
|
|
182
|
-
else if (contextPct <= 66) pctColor = AMBER;
|
|
183
|
-
else pctColor = ROSE;
|
|
184
|
-
|
|
185
|
-
const shortModel = shortenModel(modelName);
|
|
186
|
-
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Status line for Claude Code sessions.
|
|
4
|
+
*
|
|
5
|
+
* Renders context window usage and git status with ANSI colors.
|
|
6
|
+
* Optionally persists context_window data to the session's state.json.
|
|
7
|
+
*
|
|
8
|
+
* Context and git sections only.
|
|
9
|
+
*
|
|
10
|
+
* Usage: echo '{"session_id":"...","model":{"display_name":"Opus"},...}' | bun status_line.ts
|
|
11
|
+
*/
|
|
12
|
+
import { execFileSync } from "node:child_process";
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
|
|
17
|
+
import { CONTEXT_BASELINE_TOKENS } from "../lib-ts/base/hook-utils.js";
|
|
18
|
+
import { getContextBySessionId, getContext, loadState, saveState } from "../lib-ts/context/context-store.js";
|
|
19
|
+
import { findLatestPlan } from "../lib-ts/context/plan-manager.js";
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Path setup
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
const SCRIPT_DIR = path.dirname(new URL(import.meta.url).pathname);
|
|
25
|
+
const OUTPUT_DIR = path.join(".", "_output");
|
|
26
|
+
const CACHE_DIR = path.join(OUTPUT_DIR, "cache");
|
|
27
|
+
const STATUSLINE_CACHE = path.join(CACHE_DIR, ".statusline-cache.json");
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// NO_COLOR support (https://no-color.org)
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
const NO_COLOR = Boolean(process.env.NO_COLOR);
|
|
33
|
+
|
|
34
|
+
const RESET = NO_COLOR ? "" : "\u001B[0m";
|
|
35
|
+
|
|
36
|
+
// Structural
|
|
37
|
+
const SLATE_300 = NO_COLOR ? "" : "\u001B[38;2;203;213;225m";
|
|
38
|
+
const SLATE_400 = NO_COLOR ? "" : "\u001B[38;2;148;163;184m";
|
|
39
|
+
const SLATE_500 = NO_COLOR ? "" : "\u001B[38;2;100;116;139m";
|
|
40
|
+
const SLATE_600 = NO_COLOR ? "" : "\u001B[38;2;71;85;105m";
|
|
41
|
+
|
|
42
|
+
// Semantic
|
|
43
|
+
const EMERALD = NO_COLOR ? "" : "\u001B[38;2;74;222;128m";
|
|
44
|
+
const ROSE = NO_COLOR ? "" : "\u001B[38;2;251;113;133m";
|
|
45
|
+
const AMBER = NO_COLOR ? "" : "\u001B[38;2;251;191;36m";
|
|
46
|
+
|
|
47
|
+
// Context colors
|
|
48
|
+
const CTX_PRIMARY = NO_COLOR ? "" : "\u001B[38;2;129;140;248m";
|
|
49
|
+
const CTX_SECONDARY = NO_COLOR ? "" : "\u001B[38;2;165;180;252m";
|
|
50
|
+
const CTX_ACCENT = NO_COLOR ? "" : "\u001B[38;2;139;92;246m";
|
|
51
|
+
const CTX_BUCKET_EMPTY = NO_COLOR ? "" : "\u001B[38;2;75;82;95m";
|
|
52
|
+
|
|
53
|
+
// Git colors
|
|
54
|
+
const GIT_PRIMARY = NO_COLOR ? "" : "\u001B[38;2;56;189;248m";
|
|
55
|
+
const GIT_VALUE = NO_COLOR ? "" : "\u001B[38;2;186;230;253m";
|
|
56
|
+
const GIT_DIR = NO_COLOR ? "" : "\u001B[38;2;147;197;253m";
|
|
57
|
+
const GIT_CLEAN = NO_COLOR ? "" : "\u001B[38;2;125;211;252m";
|
|
58
|
+
const GIT_MODIFIED = NO_COLOR ? "" : "\u001B[38;2;96;165;250m";
|
|
59
|
+
const GIT_ADDED = NO_COLOR ? "" : "\u001B[38;2;59;130;246m";
|
|
60
|
+
const GIT_STASH = NO_COLOR ? "" : "\u001B[38;2;165;180;252m";
|
|
61
|
+
const GIT_AGE_FRESH = NO_COLOR ? "" : "\u001B[38;2;125;211;252m";
|
|
62
|
+
const GIT_AGE_RECENT = NO_COLOR ? "" : "\u001B[38;2;96;165;250m";
|
|
63
|
+
const GIT_AGE_STALE = NO_COLOR ? "" : "\u001B[38;2;59;130;246m";
|
|
64
|
+
const GIT_AGE_OLD = NO_COLOR ? "" : "\u001B[38;2;99;102;241m";
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Display modes
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
function getTerminalWidth(): number {
|
|
71
|
+
const colsEnv = process.env.COLUMNS;
|
|
72
|
+
if (colsEnv) {
|
|
73
|
+
const cols = parseInt(colsEnv, 10);
|
|
74
|
+
if (cols > 0) return cols;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
if (process.stdout.columns && process.stdout.columns > 0) {
|
|
78
|
+
return process.stdout.columns;
|
|
79
|
+
}
|
|
80
|
+
} catch { /* ignore */ }
|
|
81
|
+
return 80;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getDisplayMode(width: number): string {
|
|
85
|
+
if (width < 35) return "nano";
|
|
86
|
+
if (width < 55) return "micro";
|
|
87
|
+
if (width < 80) return "mini";
|
|
88
|
+
return "normal";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Color helpers
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
function getBucketColor(pos: number, maxPos: number): string {
|
|
96
|
+
if (NO_COLOR) return "";
|
|
97
|
+
const pct = Math.floor((pos * 100) / maxPos);
|
|
98
|
+
|
|
99
|
+
let b: number; let g: number; let r: number;
|
|
100
|
+
|
|
101
|
+
if (pct <= 33) {
|
|
102
|
+
r = 74 + Math.floor(((250 - 74) * pct) / 33);
|
|
103
|
+
g = 222 + Math.floor(((204 - 222) * pct) / 33);
|
|
104
|
+
b = 128 + Math.floor(((21 - 128) * pct) / 33);
|
|
105
|
+
} else if (pct <= 66) {
|
|
106
|
+
const t = pct - 33;
|
|
107
|
+
r = 250 + Math.floor(((251 - 250) * t) / 33);
|
|
108
|
+
g = 204 + Math.floor(((146 - 204) * t) / 33);
|
|
109
|
+
b = 21 + Math.floor(((60 - 21) * t) / 33);
|
|
110
|
+
} else {
|
|
111
|
+
const t = pct - 66;
|
|
112
|
+
r = 251 + Math.floor(((239 - 251) * t) / 34);
|
|
113
|
+
g = 146 + Math.floor(((68 - 146) * t) / 34);
|
|
114
|
+
b = 60 + Math.floor(((68 - 60) * t) / 34);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return `\u001B[38;2;${r};${g};${b}m`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Context bar rendering
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
function renderContextBar(width: number, pct: number): [string, string] {
|
|
125
|
+
pct = Math.max(0, Math.min(100, pct));
|
|
126
|
+
const filled = Math.floor((pct * width) / 100);
|
|
127
|
+
let lastColor = EMERALD;
|
|
128
|
+
const parts: string[] = [];
|
|
129
|
+
|
|
130
|
+
for (let i = 1; i <= width; i++) {
|
|
131
|
+
if (i <= filled) {
|
|
132
|
+
const color = getBucketColor(i, width);
|
|
133
|
+
lastColor = color;
|
|
134
|
+
parts.push(`${color}\u26C1${RESET}`);
|
|
135
|
+
} else {
|
|
136
|
+
parts.push(`${CTX_BUCKET_EMPTY}\u26C1${RESET}`);
|
|
137
|
+
}
|
|
138
|
+
if (width > 8) {
|
|
139
|
+
parts.push(" ");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return [parts.join("").trimEnd(), lastColor];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Separator
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
const SEPARATOR = `${SLATE_600}${"─".repeat(72)}${RESET}`;
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// Context section
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
function shortenModel(name: string): string {
|
|
157
|
+
const replacements: [string, string][] = [
|
|
158
|
+
["claude-opus-4-6", "opus-4.6"],
|
|
159
|
+
["claude-opus-4-5", "opus-4.5"],
|
|
160
|
+
["claude-sonnet-4", "sonnet-4"],
|
|
161
|
+
["claude-3-5-sonnet", "sonnet-3.5"],
|
|
162
|
+
["claude-3-5-haiku", "haiku-3.5"],
|
|
163
|
+
["claude-", ""],
|
|
164
|
+
];
|
|
165
|
+
let result = name;
|
|
166
|
+
for (const [old, replacement] of replacements) {
|
|
167
|
+
result = result.replace(old, replacement);
|
|
168
|
+
}
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function renderContext(
|
|
173
|
+
mode: string,
|
|
174
|
+
contextPct: number,
|
|
175
|
+
contextK: number,
|
|
176
|
+
maxK: number,
|
|
177
|
+
timeDisplay: string,
|
|
178
|
+
modelName: string,
|
|
179
|
+
): void {
|
|
180
|
+
let pctColor: string;
|
|
181
|
+
if (contextPct <= 33) pctColor = EMERALD;
|
|
182
|
+
else if (contextPct <= 66) pctColor = AMBER;
|
|
183
|
+
else pctColor = ROSE;
|
|
184
|
+
|
|
185
|
+
const shortModel = shortenModel(modelName);
|
|
186
|
+
|
|
187
187
|
switch (mode) {
|
|
188
|
-
case "micro": {
|
|
189
|
-
const [bar] = renderContextBar(6, contextPct);
|
|
190
|
-
console.log(
|
|
191
|
-
`${CTX_PRIMARY}\u25C9${RESET} ${CTX_ACCENT}${shortModel}${RESET} ` +
|
|
192
|
-
`${SLATE_600}\u2502${RESET} ` +
|
|
193
|
-
`${bar} ${pctColor}${contextPct}%${RESET} ${SLATE_500}(${contextK}k)${RESET} ` +
|
|
194
|
-
`${CTX_ACCENT}\u23F1${RESET} ${SLATE_300}${timeDisplay}${RESET}`,
|
|
195
|
-
);
|
|
188
|
+
case "micro": {
|
|
189
|
+
const [bar] = renderContextBar(6, contextPct);
|
|
190
|
+
console.log(
|
|
191
|
+
`${CTX_PRIMARY}\u25C9${RESET} ${CTX_ACCENT}${shortModel}${RESET} ` +
|
|
192
|
+
`${SLATE_600}\u2502${RESET} ` +
|
|
193
|
+
`${bar} ${pctColor}${contextPct}%${RESET} ${SLATE_500}(${contextK}k)${RESET} ` +
|
|
194
|
+
`${CTX_ACCENT}\u23F1${RESET} ${SLATE_300}${timeDisplay}${RESET}`,
|
|
195
|
+
);
|
|
196
196
|
|
|
197
197
|
break;
|
|
198
198
|
}
|
|
199
|
-
case "mini": {
|
|
200
|
-
const [bar] = renderContextBar(8, contextPct);
|
|
201
|
-
console.log(
|
|
202
|
-
`${CTX_PRIMARY}\u25C9${RESET} ${CTX_ACCENT}${shortModel}${RESET} ` +
|
|
203
|
-
`${SLATE_600}\u2502${RESET} ` +
|
|
204
|
-
`${CTX_SECONDARY}CTX:${RESET} ${bar} ` +
|
|
205
|
-
`${pctColor}${contextPct}%${RESET} ${SLATE_500}(${contextK}k/${maxK}k)${RESET} ` +
|
|
206
|
-
`${CTX_ACCENT}\u23F1${RESET} ${SLATE_300}${timeDisplay}${RESET}`,
|
|
207
|
-
);
|
|
199
|
+
case "mini": {
|
|
200
|
+
const [bar] = renderContextBar(8, contextPct);
|
|
201
|
+
console.log(
|
|
202
|
+
`${CTX_PRIMARY}\u25C9${RESET} ${CTX_ACCENT}${shortModel}${RESET} ` +
|
|
203
|
+
`${SLATE_600}\u2502${RESET} ` +
|
|
204
|
+
`${CTX_SECONDARY}CTX:${RESET} ${bar} ` +
|
|
205
|
+
`${pctColor}${contextPct}%${RESET} ${SLATE_500}(${contextK}k/${maxK}k)${RESET} ` +
|
|
206
|
+
`${CTX_ACCENT}\u23F1${RESET} ${SLATE_300}${timeDisplay}${RESET}`,
|
|
207
|
+
);
|
|
208
208
|
|
|
209
209
|
break;
|
|
210
210
|
}
|
|
211
|
-
case "nano": {
|
|
212
|
-
const [bar] = renderContextBar(5, contextPct);
|
|
213
|
-
console.log(
|
|
214
|
-
`${CTX_PRIMARY}\u25C9${RESET} ${CTX_ACCENT}${shortModel}${RESET} ` +
|
|
215
|
-
`${bar} ${pctColor}${contextPct}%${RESET} ` +
|
|
216
|
-
`${CTX_ACCENT}\u23F1${RESET} ${SLATE_300}${timeDisplay}${RESET}`,
|
|
217
|
-
);
|
|
211
|
+
case "nano": {
|
|
212
|
+
const [bar] = renderContextBar(5, contextPct);
|
|
213
|
+
console.log(
|
|
214
|
+
`${CTX_PRIMARY}\u25C9${RESET} ${CTX_ACCENT}${shortModel}${RESET} ` +
|
|
215
|
+
`${bar} ${pctColor}${contextPct}%${RESET} ` +
|
|
216
|
+
`${CTX_ACCENT}\u23F1${RESET} ${SLATE_300}${timeDisplay}${RESET}`,
|
|
217
|
+
);
|
|
218
218
|
|
|
219
219
|
break;
|
|
220
220
|
}
|
|
221
|
-
default: {
|
|
222
|
-
const [bar, lastColor] = renderContextBar(16, contextPct);
|
|
223
|
-
console.log(
|
|
224
|
-
`${CTX_PRIMARY}\u25C9${RESET} ${CTX_SECONDARY}Model:${RESET} ${CTX_ACCENT}${shortModel}${RESET} ` +
|
|
225
|
-
`${SLATE_600}\u2502${RESET} ` +
|
|
226
|
-
`${CTX_SECONDARY}Context:${RESET} ${bar} ` +
|
|
227
|
-
`${lastColor}${contextPct}%${RESET} ${SLATE_500}(${contextK}k/${maxK}k)${RESET} ` +
|
|
228
|
-
`${SLATE_600}\u2502${RESET} ` +
|
|
229
|
-
`${CTX_ACCENT}\u23F1${RESET} ${SLATE_300}${timeDisplay}${RESET}`,
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
console.log(SEPARATOR);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// ---------------------------------------------------------------------------
|
|
238
|
-
// Git status
|
|
239
|
-
// ---------------------------------------------------------------------------
|
|
240
|
-
|
|
241
|
-
interface GitStatus {
|
|
242
|
-
branch: string;
|
|
243
|
-
modified: number;
|
|
244
|
-
staged: number;
|
|
245
|
-
untracked: number;
|
|
246
|
-
stash_count: number;
|
|
247
|
-
ahead: number;
|
|
248
|
-
behind: number;
|
|
249
|
-
age_display: string;
|
|
250
|
-
age_color: string;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function runGit(args: string[], cwd: string, timeout = 2000): string | null {
|
|
254
|
-
try {
|
|
255
|
-
const result = execFileSync("git", args, {
|
|
256
|
-
cwd,
|
|
257
|
-
timeout,
|
|
258
|
-
encoding: "utf-8",
|
|
259
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
260
|
-
windowsHide: true,
|
|
261
|
-
});
|
|
262
|
-
return result.trim();
|
|
263
|
-
} catch {
|
|
264
|
-
return null;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function getGitStatus(cwd: string): GitStatus | null {
|
|
269
|
-
if (runGit(["rev-parse", "--git-dir"], cwd) === null) {
|
|
270
|
-
return null;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const status: GitStatus = {
|
|
274
|
-
branch: "detached",
|
|
275
|
-
modified: 0,
|
|
276
|
-
staged: 0,
|
|
277
|
-
untracked: 0,
|
|
278
|
-
stash_count: 0,
|
|
279
|
-
ahead: 0,
|
|
280
|
-
behind: 0,
|
|
281
|
-
age_display: "",
|
|
282
|
-
age_color: GIT_AGE_FRESH,
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
// Branch
|
|
286
|
-
const branch = runGit(["branch", "--show-current"], cwd);
|
|
287
|
-
if (branch) status.branch = branch;
|
|
288
|
-
|
|
289
|
-
// Modified files
|
|
290
|
-
const diff = runGit(["diff", "--name-only"], cwd);
|
|
291
|
-
if (diff) status.modified = diff.split(/\r?\n/).filter(Boolean).length;
|
|
292
|
-
|
|
293
|
-
// Staged files
|
|
294
|
-
const staged = runGit(["diff", "--cached", "--name-only"], cwd);
|
|
295
|
-
if (staged) status.staged = staged.split(/\r?\n/).filter(Boolean).length;
|
|
296
|
-
|
|
297
|
-
// Untracked files
|
|
298
|
-
const untracked = runGit(["ls-files", "--others", "--exclude-standard"], cwd);
|
|
299
|
-
if (untracked) status.untracked = untracked.split(/\r?\n/).filter(Boolean).length;
|
|
300
|
-
|
|
301
|
-
// Stash count
|
|
302
|
-
const stash = runGit(["stash", "list"], cwd);
|
|
303
|
-
if (stash) status.stash_count = stash.split(/\r?\n/).filter(Boolean).length;
|
|
304
|
-
|
|
305
|
-
// Ahead/behind
|
|
306
|
-
const ab = runGit(["rev-list", "--left-right", "--count", "HEAD...@{u}"], cwd);
|
|
307
|
-
if (ab) {
|
|
308
|
-
const parts = ab.split(/\s+/);
|
|
309
|
-
if (parts.length >= 2) {
|
|
310
|
-
status.ahead = parseInt(parts[0]!, 10) || 0;
|
|
311
|
-
status.behind = parseInt(parts[1]!, 10) || 0;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Commit age
|
|
316
|
-
const log = runGit(["log", "-1", "--format=%ct"], cwd);
|
|
317
|
-
if (log) {
|
|
318
|
-
try {
|
|
319
|
-
const lastEpoch = parseInt(log, 10);
|
|
320
|
-
const nowEpoch = Math.floor(Date.now() / 1000);
|
|
321
|
-
const ageSec = nowEpoch - lastEpoch;
|
|
322
|
-
const ageMin = Math.floor(ageSec / 60);
|
|
323
|
-
const ageHrs = Math.floor(ageSec / 3600);
|
|
324
|
-
const ageDays = Math.floor(ageSec / 86_400);
|
|
325
|
-
|
|
326
|
-
if (ageMin < 1) {
|
|
327
|
-
status.age_display = "now";
|
|
328
|
-
status.age_color = GIT_AGE_FRESH;
|
|
329
|
-
} else if (ageHrs < 1) {
|
|
330
|
-
status.age_display = `${ageMin}m`;
|
|
331
|
-
status.age_color = GIT_AGE_FRESH;
|
|
332
|
-
} else if (ageHrs < 24) {
|
|
333
|
-
status.age_display = `${ageHrs}h`;
|
|
334
|
-
status.age_color = GIT_AGE_RECENT;
|
|
335
|
-
} else if (ageDays < 7) {
|
|
336
|
-
status.age_display = `${ageDays}d`;
|
|
337
|
-
status.age_color = GIT_AGE_STALE;
|
|
338
|
-
} else {
|
|
339
|
-
status.age_display = `${ageDays}d`;
|
|
340
|
-
status.age_color = GIT_AGE_OLD;
|
|
341
|
-
}
|
|
342
|
-
} catch { /* ignore */ }
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
return status;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function renderGit(mode: string, git: GitStatus, dirName: string): void {
|
|
349
|
-
const totalChanged = git.modified + git.staged;
|
|
350
|
-
const statusIcon = (totalChanged > 0 || git.untracked > 0) ? "*" : "\u2713";
|
|
351
|
-
|
|
221
|
+
default: {
|
|
222
|
+
const [bar, lastColor] = renderContextBar(16, contextPct);
|
|
223
|
+
console.log(
|
|
224
|
+
`${CTX_PRIMARY}\u25C9${RESET} ${CTX_SECONDARY}Model:${RESET} ${CTX_ACCENT}${shortModel}${RESET} ` +
|
|
225
|
+
`${SLATE_600}\u2502${RESET} ` +
|
|
226
|
+
`${CTX_SECONDARY}Context:${RESET} ${bar} ` +
|
|
227
|
+
`${lastColor}${contextPct}%${RESET} ${SLATE_500}(${contextK}k/${maxK}k)${RESET} ` +
|
|
228
|
+
`${SLATE_600}\u2502${RESET} ` +
|
|
229
|
+
`${CTX_ACCENT}\u23F1${RESET} ${SLATE_300}${timeDisplay}${RESET}`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
console.log(SEPARATOR);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// Git status
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
interface GitStatus {
|
|
242
|
+
branch: string;
|
|
243
|
+
modified: number;
|
|
244
|
+
staged: number;
|
|
245
|
+
untracked: number;
|
|
246
|
+
stash_count: number;
|
|
247
|
+
ahead: number;
|
|
248
|
+
behind: number;
|
|
249
|
+
age_display: string;
|
|
250
|
+
age_color: string;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function runGit(args: string[], cwd: string, timeout = 2000): string | null {
|
|
254
|
+
try {
|
|
255
|
+
const result = execFileSync("git", args, {
|
|
256
|
+
cwd,
|
|
257
|
+
timeout,
|
|
258
|
+
encoding: "utf-8",
|
|
259
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
260
|
+
windowsHide: true,
|
|
261
|
+
});
|
|
262
|
+
return result.trim();
|
|
263
|
+
} catch {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function getGitStatus(cwd: string): GitStatus | null {
|
|
269
|
+
if (runGit(["rev-parse", "--git-dir"], cwd) === null) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const status: GitStatus = {
|
|
274
|
+
branch: "detached",
|
|
275
|
+
modified: 0,
|
|
276
|
+
staged: 0,
|
|
277
|
+
untracked: 0,
|
|
278
|
+
stash_count: 0,
|
|
279
|
+
ahead: 0,
|
|
280
|
+
behind: 0,
|
|
281
|
+
age_display: "",
|
|
282
|
+
age_color: GIT_AGE_FRESH,
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// Branch
|
|
286
|
+
const branch = runGit(["branch", "--show-current"], cwd);
|
|
287
|
+
if (branch) status.branch = branch;
|
|
288
|
+
|
|
289
|
+
// Modified files
|
|
290
|
+
const diff = runGit(["diff", "--name-only"], cwd);
|
|
291
|
+
if (diff) status.modified = diff.split(/\r?\n/).filter(Boolean).length;
|
|
292
|
+
|
|
293
|
+
// Staged files
|
|
294
|
+
const staged = runGit(["diff", "--cached", "--name-only"], cwd);
|
|
295
|
+
if (staged) status.staged = staged.split(/\r?\n/).filter(Boolean).length;
|
|
296
|
+
|
|
297
|
+
// Untracked files
|
|
298
|
+
const untracked = runGit(["ls-files", "--others", "--exclude-standard"], cwd);
|
|
299
|
+
if (untracked) status.untracked = untracked.split(/\r?\n/).filter(Boolean).length;
|
|
300
|
+
|
|
301
|
+
// Stash count
|
|
302
|
+
const stash = runGit(["stash", "list"], cwd);
|
|
303
|
+
if (stash) status.stash_count = stash.split(/\r?\n/).filter(Boolean).length;
|
|
304
|
+
|
|
305
|
+
// Ahead/behind
|
|
306
|
+
const ab = runGit(["rev-list", "--left-right", "--count", "HEAD...@{u}"], cwd);
|
|
307
|
+
if (ab) {
|
|
308
|
+
const parts = ab.split(/\s+/);
|
|
309
|
+
if (parts.length >= 2) {
|
|
310
|
+
status.ahead = parseInt(parts[0]!, 10) || 0;
|
|
311
|
+
status.behind = parseInt(parts[1]!, 10) || 0;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Commit age
|
|
316
|
+
const log = runGit(["log", "-1", "--format=%ct"], cwd);
|
|
317
|
+
if (log) {
|
|
318
|
+
try {
|
|
319
|
+
const lastEpoch = parseInt(log, 10);
|
|
320
|
+
const nowEpoch = Math.floor(Date.now() / 1000);
|
|
321
|
+
const ageSec = nowEpoch - lastEpoch;
|
|
322
|
+
const ageMin = Math.floor(ageSec / 60);
|
|
323
|
+
const ageHrs = Math.floor(ageSec / 3600);
|
|
324
|
+
const ageDays = Math.floor(ageSec / 86_400);
|
|
325
|
+
|
|
326
|
+
if (ageMin < 1) {
|
|
327
|
+
status.age_display = "now";
|
|
328
|
+
status.age_color = GIT_AGE_FRESH;
|
|
329
|
+
} else if (ageHrs < 1) {
|
|
330
|
+
status.age_display = `${ageMin}m`;
|
|
331
|
+
status.age_color = GIT_AGE_FRESH;
|
|
332
|
+
} else if (ageHrs < 24) {
|
|
333
|
+
status.age_display = `${ageHrs}h`;
|
|
334
|
+
status.age_color = GIT_AGE_RECENT;
|
|
335
|
+
} else if (ageDays < 7) {
|
|
336
|
+
status.age_display = `${ageDays}d`;
|
|
337
|
+
status.age_color = GIT_AGE_STALE;
|
|
338
|
+
} else {
|
|
339
|
+
status.age_display = `${ageDays}d`;
|
|
340
|
+
status.age_color = GIT_AGE_OLD;
|
|
341
|
+
}
|
|
342
|
+
} catch { /* ignore */ }
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return status;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function renderGit(mode: string, git: GitStatus, dirName: string): void {
|
|
349
|
+
const totalChanged = git.modified + git.staged;
|
|
350
|
+
const statusIcon = (totalChanged > 0 || git.untracked > 0) ? "*" : "\u2713";
|
|
351
|
+
|
|
352
352
|
switch (mode) {
|
|
353
|
-
case "micro": {
|
|
354
|
-
let line = `${GIT_PRIMARY}\u25C8${RESET} ${GIT_DIR}${dirName}${RESET} ${GIT_VALUE}${git.branch}${RESET}`;
|
|
355
|
-
if (git.age_display) {
|
|
356
|
-
line += ` ${git.age_color}${git.age_display}${RESET}`;
|
|
357
|
-
}
|
|
358
|
-
line += " ";
|
|
359
|
-
if (statusIcon === "\u2713") {
|
|
360
|
-
line += `${GIT_CLEAN}${statusIcon}${RESET}`;
|
|
361
|
-
} else {
|
|
362
|
-
line += `${GIT_MODIFIED}${statusIcon}${totalChanged}${RESET}`;
|
|
363
|
-
}
|
|
364
|
-
console.log(line);
|
|
353
|
+
case "micro": {
|
|
354
|
+
let line = `${GIT_PRIMARY}\u25C8${RESET} ${GIT_DIR}${dirName}${RESET} ${GIT_VALUE}${git.branch}${RESET}`;
|
|
355
|
+
if (git.age_display) {
|
|
356
|
+
line += ` ${git.age_color}${git.age_display}${RESET}`;
|
|
357
|
+
}
|
|
358
|
+
line += " ";
|
|
359
|
+
if (statusIcon === "\u2713") {
|
|
360
|
+
line += `${GIT_CLEAN}${statusIcon}${RESET}`;
|
|
361
|
+
} else {
|
|
362
|
+
line += `${GIT_MODIFIED}${statusIcon}${totalChanged}${RESET}`;
|
|
363
|
+
}
|
|
364
|
+
console.log(line);
|
|
365
365
|
|
|
366
366
|
break;
|
|
367
367
|
}
|
|
368
|
-
case "mini": {
|
|
369
|
-
let line =
|
|
370
|
-
`${GIT_PRIMARY}\u25C8${RESET} ${GIT_DIR}${dirName}${RESET} ` +
|
|
371
|
-
`${SLATE_600}\u2502${RESET} ${GIT_VALUE}${git.branch}${RESET}`;
|
|
372
|
-
if (git.age_display) {
|
|
373
|
-
line += ` ${SLATE_600}\u2502${RESET} ${git.age_color}${git.age_display}${RESET}`;
|
|
374
|
-
}
|
|
375
|
-
line += ` ${SLATE_600}\u2502${RESET} `;
|
|
376
|
-
if (statusIcon === "\u2713") {
|
|
377
|
-
line += `${GIT_CLEAN}${statusIcon}${RESET}`;
|
|
378
|
-
} else {
|
|
379
|
-
line += `${GIT_MODIFIED}${statusIcon}${totalChanged}${RESET}`;
|
|
380
|
-
if (git.untracked > 0) {
|
|
381
|
-
line += ` ${GIT_ADDED}+${git.untracked}${RESET}`;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
console.log(line);
|
|
368
|
+
case "mini": {
|
|
369
|
+
let line =
|
|
370
|
+
`${GIT_PRIMARY}\u25C8${RESET} ${GIT_DIR}${dirName}${RESET} ` +
|
|
371
|
+
`${SLATE_600}\u2502${RESET} ${GIT_VALUE}${git.branch}${RESET}`;
|
|
372
|
+
if (git.age_display) {
|
|
373
|
+
line += ` ${SLATE_600}\u2502${RESET} ${git.age_color}${git.age_display}${RESET}`;
|
|
374
|
+
}
|
|
375
|
+
line += ` ${SLATE_600}\u2502${RESET} `;
|
|
376
|
+
if (statusIcon === "\u2713") {
|
|
377
|
+
line += `${GIT_CLEAN}${statusIcon}${RESET}`;
|
|
378
|
+
} else {
|
|
379
|
+
line += `${GIT_MODIFIED}${statusIcon}${totalChanged}${RESET}`;
|
|
380
|
+
if (git.untracked > 0) {
|
|
381
|
+
line += ` ${GIT_ADDED}+${git.untracked}${RESET}`;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
console.log(line);
|
|
385
385
|
|
|
386
386
|
break;
|
|
387
387
|
}
|
|
388
|
-
case "nano": {
|
|
389
|
-
let line = `${GIT_PRIMARY}\u25C8${RESET} ${GIT_DIR}${dirName}${RESET} ${GIT_VALUE}${git.branch}${RESET} `;
|
|
390
|
-
if (statusIcon === "\u2713") {
|
|
391
|
-
line += `${GIT_CLEAN}\u2713${RESET}`;
|
|
392
|
-
} else {
|
|
393
|
-
line += `${GIT_MODIFIED}*${totalChanged}${RESET}`;
|
|
394
|
-
}
|
|
395
|
-
console.log(line);
|
|
388
|
+
case "nano": {
|
|
389
|
+
let line = `${GIT_PRIMARY}\u25C8${RESET} ${GIT_DIR}${dirName}${RESET} ${GIT_VALUE}${git.branch}${RESET} `;
|
|
390
|
+
if (statusIcon === "\u2713") {
|
|
391
|
+
line += `${GIT_CLEAN}\u2713${RESET}`;
|
|
392
|
+
} else {
|
|
393
|
+
line += `${GIT_MODIFIED}*${totalChanged}${RESET}`;
|
|
394
|
+
}
|
|
395
|
+
console.log(line);
|
|
396
396
|
|
|
397
397
|
break;
|
|
398
398
|
}
|
|
399
|
-
default: {
|
|
400
|
-
let line =
|
|
401
|
-
`${GIT_PRIMARY}\u25C8${RESET} ${GIT_PRIMARY}PWD:${RESET} ${GIT_DIR}${dirName}${RESET} ` +
|
|
402
|
-
`${SLATE_600}\u2502${RESET} ` +
|
|
403
|
-
`${GIT_PRIMARY}Branch:${RESET} ${GIT_VALUE}${git.branch}${RESET}`;
|
|
404
|
-
if (git.age_display) {
|
|
405
|
-
line += ` ${SLATE_600}\u2502${RESET} ${GIT_PRIMARY}Age:${RESET} ${git.age_color}${git.age_display}${RESET}`;
|
|
406
|
-
}
|
|
407
|
-
if (git.stash_count > 0) {
|
|
408
|
-
line += ` ${SLATE_600}\u2502${RESET} ${GIT_PRIMARY}Stash:${RESET} ${GIT_STASH}${git.stash_count}${RESET}`;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
if (totalChanged > 0 || git.untracked > 0) {
|
|
412
|
-
line += ` ${SLATE_600}\u2502${RESET} `;
|
|
413
|
-
if (totalChanged > 0) {
|
|
414
|
-
line += `${GIT_PRIMARY}Mod:${RESET} ${GIT_MODIFIED}${totalChanged}${RESET}`;
|
|
415
|
-
}
|
|
416
|
-
if (git.untracked > 0) {
|
|
417
|
-
if (totalChanged > 0) line += " ";
|
|
418
|
-
line += `${GIT_PRIMARY}New:${RESET} ${GIT_ADDED}${git.untracked}${RESET}`;
|
|
419
|
-
}
|
|
420
|
-
} else {
|
|
421
|
-
line += ` ${SLATE_600}\u2502${RESET} ${GIT_CLEAN}\u2713 clean${RESET}`;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
if (git.ahead > 0 || git.behind > 0) {
|
|
425
|
-
line += ` ${SLATE_600}\u2502${RESET} ${GIT_PRIMARY}Sync:${RESET} `;
|
|
426
|
-
if (git.ahead > 0) {
|
|
427
|
-
line += `${GIT_CLEAN}\u2191${git.ahead}${RESET}`;
|
|
428
|
-
}
|
|
429
|
-
if (git.behind > 0) {
|
|
430
|
-
line += `${GIT_STASH}\u2193${git.behind}${RESET}`;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
console.log(line);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// ---------------------------------------------------------------------------
|
|
439
|
-
// Context manager line (line 3)
|
|
440
|
-
// ---------------------------------------------------------------------------
|
|
441
|
-
|
|
442
|
-
function findActivePlanFile(): string | null {
|
|
443
|
-
try {
|
|
444
|
-
const plansDir = path.join(homedir(), ".claude", "plans");
|
|
445
|
-
if (!fs.existsSync(plansDir)) return null;
|
|
446
|
-
const planFiles = fs.readdirSync(plansDir)
|
|
447
|
-
.filter(f => f.endsWith(".md"))
|
|
448
|
-
.map(f => {
|
|
449
|
-
const fullPath = path.join(plansDir, f);
|
|
450
|
-
return { path: fullPath, mtime: fs.statSync(fullPath).mtimeMs };
|
|
451
|
-
})
|
|
452
|
-
.sort((a, b) => b.mtime - a.mtime);
|
|
453
|
-
return planFiles.length > 0 ? planFiles[0]!.path : null;
|
|
454
|
-
} catch {
|
|
455
|
-
return null;
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function renderContextManager(
|
|
460
|
-
mode: string,
|
|
461
|
-
contextId: string,
|
|
462
|
-
contextState: Record<string, any> | null,
|
|
463
|
-
): void {
|
|
464
|
-
// Strip YYMMDD-HHMM- timestamp prefix from context ID for display
|
|
465
|
-
let displayId = contextId.replace(/^\d{6}-\d{4}-/, "");
|
|
466
|
-
if (!displayId) displayId = contextId;
|
|
467
|
-
|
|
468
|
-
// Truncate display_id per mode
|
|
469
|
-
const maxIdLen: Record<string, number> = { nano: 14, micro: 18, mini: 22, normal: 30 };
|
|
470
|
-
const maxLen = maxIdLen[mode] ?? 30;
|
|
471
|
-
let truncatedId = displayId.slice(0, maxLen);
|
|
472
|
-
if (displayId.length > maxLen) truncatedId += "\u2026";
|
|
473
|
-
|
|
474
|
-
// Read state fields
|
|
475
|
-
const stateMode = contextState?.mode ?? "idle";
|
|
476
|
-
const statePlanPath = contextState?.plan_path ?? null;
|
|
477
|
-
|
|
478
|
-
// Detect plan mode heuristic
|
|
479
|
-
const activePlanFile = findActivePlanFile();
|
|
480
|
-
const isPlanning = stateMode === "idle" && activePlanFile !== null;
|
|
481
|
-
|
|
482
|
-
// Build mode badge
|
|
483
|
-
let modeBadge = "";
|
|
484
|
-
if (isPlanning) {
|
|
485
|
-
const label = mode === "nano" ? "Plan" : "Planning";
|
|
486
|
-
modeBadge = ` ${SLATE_600}\u2502${RESET} ${CTX_SECONDARY}Mode:${RESET} ${AMBER}${label}${RESET}`;
|
|
487
|
-
} else if (stateMode === "has_plan") {
|
|
488
|
-
const label = mode === "nano" ? "Ready" : "Plan Ready";
|
|
489
|
-
modeBadge = ` ${SLATE_600}\u2502${RESET} ${CTX_SECONDARY}Mode:${RESET} ${EMERALD}${label}${RESET}`;
|
|
490
|
-
} else if (stateMode === "active") {
|
|
491
|
-
const label = "Active";
|
|
492
|
-
modeBadge = ` ${SLATE_600}\u2502${RESET} ${CTX_SECONDARY}Mode:${RESET} ${CTX_ACCENT}${label}${RESET}`;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// Resolve plan file path for display
|
|
496
|
-
let planFilePath: string | null = null;
|
|
497
|
-
if (isPlanning) {
|
|
498
|
-
planFilePath = activePlanFile;
|
|
499
|
-
} else if (statePlanPath) {
|
|
500
|
-
planFilePath = statePlanPath;
|
|
501
|
-
} else if (stateMode === "has_plan" || stateMode === "active") {
|
|
502
|
-
try {
|
|
503
|
-
planFilePath = findLatestPlan(contextId) ?? null;
|
|
504
|
-
} catch { /* ignore */ }
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Build plan name (mini/normal only)
|
|
508
|
-
let planPart = "";
|
|
509
|
-
if ((mode === "mini" || mode === "normal") && planFilePath) {
|
|
510
|
-
const planStem = path.basename(planFilePath, path.extname(planFilePath))
|
|
511
|
-
.replace(/^\d{4}-\d{2}-\d{2}-(\d{4}-)?/, "");
|
|
512
|
-
const maxPlanLen = mode === "mini" ? 20 : 30;
|
|
513
|
-
let truncatedPlan = planStem.slice(0, maxPlanLen);
|
|
514
|
-
if (planStem.length > maxPlanLen) truncatedPlan += "\u2026";
|
|
515
|
-
planPart = ` ${SLATE_600}\u2502${RESET} ${CTX_SECONDARY}Plan:${RESET} ${SLATE_300}${truncatedPlan}${RESET}`;
|
|
516
|
-
}
|
|
517
|
-
|
|
399
|
+
default: {
|
|
400
|
+
let line =
|
|
401
|
+
`${GIT_PRIMARY}\u25C8${RESET} ${GIT_PRIMARY}PWD:${RESET} ${GIT_DIR}${dirName}${RESET} ` +
|
|
402
|
+
`${SLATE_600}\u2502${RESET} ` +
|
|
403
|
+
`${GIT_PRIMARY}Branch:${RESET} ${GIT_VALUE}${git.branch}${RESET}`;
|
|
404
|
+
if (git.age_display) {
|
|
405
|
+
line += ` ${SLATE_600}\u2502${RESET} ${GIT_PRIMARY}Age:${RESET} ${git.age_color}${git.age_display}${RESET}`;
|
|
406
|
+
}
|
|
407
|
+
if (git.stash_count > 0) {
|
|
408
|
+
line += ` ${SLATE_600}\u2502${RESET} ${GIT_PRIMARY}Stash:${RESET} ${GIT_STASH}${git.stash_count}${RESET}`;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (totalChanged > 0 || git.untracked > 0) {
|
|
412
|
+
line += ` ${SLATE_600}\u2502${RESET} `;
|
|
413
|
+
if (totalChanged > 0) {
|
|
414
|
+
line += `${GIT_PRIMARY}Mod:${RESET} ${GIT_MODIFIED}${totalChanged}${RESET}`;
|
|
415
|
+
}
|
|
416
|
+
if (git.untracked > 0) {
|
|
417
|
+
if (totalChanged > 0) line += " ";
|
|
418
|
+
line += `${GIT_PRIMARY}New:${RESET} ${GIT_ADDED}${git.untracked}${RESET}`;
|
|
419
|
+
}
|
|
420
|
+
} else {
|
|
421
|
+
line += ` ${SLATE_600}\u2502${RESET} ${GIT_CLEAN}\u2713 clean${RESET}`;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (git.ahead > 0 || git.behind > 0) {
|
|
425
|
+
line += ` ${SLATE_600}\u2502${RESET} ${GIT_PRIMARY}Sync:${RESET} `;
|
|
426
|
+
if (git.ahead > 0) {
|
|
427
|
+
line += `${GIT_CLEAN}\u2191${git.ahead}${RESET}`;
|
|
428
|
+
}
|
|
429
|
+
if (git.behind > 0) {
|
|
430
|
+
line += `${GIT_STASH}\u2193${git.behind}${RESET}`;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
console.log(line);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ---------------------------------------------------------------------------
|
|
439
|
+
// Context manager line (line 3)
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
|
|
442
|
+
function findActivePlanFile(): string | null {
|
|
443
|
+
try {
|
|
444
|
+
const plansDir = path.join(homedir(), ".claude", "plans");
|
|
445
|
+
if (!fs.existsSync(plansDir)) return null;
|
|
446
|
+
const planFiles = fs.readdirSync(plansDir)
|
|
447
|
+
.filter(f => f.endsWith(".md"))
|
|
448
|
+
.map(f => {
|
|
449
|
+
const fullPath = path.join(plansDir, f);
|
|
450
|
+
return { path: fullPath, mtime: fs.statSync(fullPath).mtimeMs };
|
|
451
|
+
})
|
|
452
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
453
|
+
return planFiles.length > 0 ? planFiles[0]!.path : null;
|
|
454
|
+
} catch {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function renderContextManager(
|
|
460
|
+
mode: string,
|
|
461
|
+
contextId: string,
|
|
462
|
+
contextState: Record<string, any> | null,
|
|
463
|
+
): void {
|
|
464
|
+
// Strip YYMMDD-HHMM- timestamp prefix from context ID for display
|
|
465
|
+
let displayId = contextId.replace(/^\d{6}-\d{4}-/, "");
|
|
466
|
+
if (!displayId) displayId = contextId;
|
|
467
|
+
|
|
468
|
+
// Truncate display_id per mode
|
|
469
|
+
const maxIdLen: Record<string, number> = { nano: 14, micro: 18, mini: 22, normal: 30 };
|
|
470
|
+
const maxLen = maxIdLen[mode] ?? 30;
|
|
471
|
+
let truncatedId = displayId.slice(0, maxLen);
|
|
472
|
+
if (displayId.length > maxLen) truncatedId += "\u2026";
|
|
473
|
+
|
|
474
|
+
// Read state fields
|
|
475
|
+
const stateMode = contextState?.mode ?? "idle";
|
|
476
|
+
const statePlanPath = contextState?.plan_path ?? null;
|
|
477
|
+
|
|
478
|
+
// Detect plan mode heuristic
|
|
479
|
+
const activePlanFile = findActivePlanFile();
|
|
480
|
+
const isPlanning = stateMode === "idle" && activePlanFile !== null;
|
|
481
|
+
|
|
482
|
+
// Build mode badge
|
|
483
|
+
let modeBadge = "";
|
|
484
|
+
if (isPlanning) {
|
|
485
|
+
const label = mode === "nano" ? "Plan" : "Planning";
|
|
486
|
+
modeBadge = ` ${SLATE_600}\u2502${RESET} ${CTX_SECONDARY}Mode:${RESET} ${AMBER}${label}${RESET}`;
|
|
487
|
+
} else if (stateMode === "has_plan") {
|
|
488
|
+
const label = mode === "nano" ? "Ready" : "Plan Ready";
|
|
489
|
+
modeBadge = ` ${SLATE_600}\u2502${RESET} ${CTX_SECONDARY}Mode:${RESET} ${EMERALD}${label}${RESET}`;
|
|
490
|
+
} else if (stateMode === "active") {
|
|
491
|
+
const label = "Active";
|
|
492
|
+
modeBadge = ` ${SLATE_600}\u2502${RESET} ${CTX_SECONDARY}Mode:${RESET} ${CTX_ACCENT}${label}${RESET}`;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Resolve plan file path for display
|
|
496
|
+
let planFilePath: string | null = null;
|
|
497
|
+
if (isPlanning) {
|
|
498
|
+
planFilePath = activePlanFile;
|
|
499
|
+
} else if (statePlanPath) {
|
|
500
|
+
planFilePath = statePlanPath;
|
|
501
|
+
} else if (stateMode === "has_plan" || stateMode === "active") {
|
|
502
|
+
try {
|
|
503
|
+
planFilePath = findLatestPlan(contextId) ?? null;
|
|
504
|
+
} catch { /* ignore */ }
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Build plan name (mini/normal only)
|
|
508
|
+
let planPart = "";
|
|
509
|
+
if ((mode === "mini" || mode === "normal") && planFilePath) {
|
|
510
|
+
const planStem = path.basename(planFilePath, path.extname(planFilePath))
|
|
511
|
+
.replace(/^\d{4}-\d{2}-\d{2}-(\d{4}-)?/, "");
|
|
512
|
+
const maxPlanLen = mode === "mini" ? 20 : 30;
|
|
513
|
+
let truncatedPlan = planStem.slice(0, maxPlanLen);
|
|
514
|
+
if (planStem.length > maxPlanLen) truncatedPlan += "\u2026";
|
|
515
|
+
planPart = ` ${SLATE_600}\u2502${RESET} ${CTX_SECONDARY}Plan:${RESET} ${SLATE_300}${truncatedPlan}${RESET}`;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
518
|
switch (mode) {
|
|
519
|
-
case "micro": {
|
|
520
|
-
console.log(`${CTX_ACCENT}\u25C6${RESET} ${SLATE_400}${truncatedId}${RESET}${modeBadge}`);
|
|
519
|
+
case "micro": {
|
|
520
|
+
console.log(`${CTX_ACCENT}\u25C6${RESET} ${SLATE_400}${truncatedId}${RESET}${modeBadge}`);
|
|
521
521
|
|
|
522
522
|
break;
|
|
523
523
|
}
|
|
524
|
-
case "mini": {
|
|
525
|
-
console.log(
|
|
526
|
-
`${CTX_ACCENT}\u25C6${RESET} ${SLATE_400}${truncatedId}${RESET}` +
|
|
527
|
-
`${modeBadge}${planPart}`,
|
|
528
|
-
);
|
|
524
|
+
case "mini": {
|
|
525
|
+
console.log(
|
|
526
|
+
`${CTX_ACCENT}\u25C6${RESET} ${SLATE_400}${truncatedId}${RESET}` +
|
|
527
|
+
`${modeBadge}${planPart}`,
|
|
528
|
+
);
|
|
529
529
|
|
|
530
530
|
break;
|
|
531
531
|
}
|
|
532
|
-
case "nano": {
|
|
533
|
-
console.log(`${CTX_ACCENT}\u25C6${RESET} ${SLATE_400}${truncatedId}${RESET}${modeBadge}`);
|
|
532
|
+
case "nano": {
|
|
533
|
+
console.log(`${CTX_ACCENT}\u25C6${RESET} ${SLATE_400}${truncatedId}${RESET}${modeBadge}`);
|
|
534
534
|
|
|
535
535
|
break;
|
|
536
536
|
}
|
|
537
|
-
default: {
|
|
538
|
-
console.log(
|
|
539
|
-
`${CTX_ACCENT}\u25C6${RESET} ${CTX_SECONDARY}Context:${RESET} ${SLATE_300}${truncatedId}${RESET}` +
|
|
540
|
-
`${modeBadge}${planPart}`,
|
|
541
|
-
);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function renderNoContext(mode: string): void {
|
|
547
|
-
const warn = `${ROSE}\u26A0 ${RESET}`;
|
|
548
|
-
if (mode === "normal") {
|
|
549
|
-
console.log(`${warn} ${ROSE}NO CONTEXT${RESET} ${SLATE_500}\u2014 type ^ for context manager${RESET}`);
|
|
550
|
-
} else {
|
|
551
|
-
console.log(`${warn} ${ROSE}NO CONTEXT${RESET}`);
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
// ---------------------------------------------------------------------------
|
|
556
|
-
// Context persistence
|
|
557
|
-
// ---------------------------------------------------------------------------
|
|
558
|
-
|
|
559
|
-
interface StatuslineCache {
|
|
560
|
-
sessions?: Record<string, { context_id: string | null }>;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
function loadCache(): StatuslineCache {
|
|
564
|
-
try {
|
|
565
|
-
if (fs.existsSync(STATUSLINE_CACHE)) {
|
|
566
|
-
return JSON.parse(fs.readFileSync(STATUSLINE_CACHE, "utf-8"));
|
|
567
|
-
}
|
|
568
|
-
} catch { /* ignore */ }
|
|
569
|
-
return {};
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
function saveCache(cache: StatuslineCache): void {
|
|
573
|
-
try {
|
|
574
|
-
fs.mkdirSync(path.dirname(STATUSLINE_CACHE), { recursive: true });
|
|
575
|
-
fs.writeFileSync(STATUSLINE_CACHE, JSON.stringify(cache, null, 2), "utf-8");
|
|
576
|
-
} catch { /* ignore */ }
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
function resolveContextId(sessionId: string): string | null {
|
|
580
|
-
if (!sessionId || sessionId === "unknown") return null;
|
|
581
|
-
|
|
582
|
-
// Check cache first
|
|
583
|
-
const cache = loadCache();
|
|
584
|
-
const cachedEntry = cache.sessions?.[sessionId];
|
|
585
|
-
if (cachedEntry && cachedEntry.context_id !== undefined) {
|
|
586
|
-
return cachedEntry.context_id;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// Cache miss — look up via context manager
|
|
590
|
-
try {
|
|
591
|
-
const context = getContextBySessionId(sessionId);
|
|
592
|
-
if (context) {
|
|
593
|
-
if (!cache.sessions) cache.sessions = {};
|
|
594
|
-
cache.sessions[sessionId] = { context_id: (context as any).id };
|
|
595
|
-
saveCache(cache);
|
|
596
|
-
return (context as any).id;
|
|
597
|
-
}
|
|
598
|
-
} catch { /* ignore */ }
|
|
599
|
-
|
|
600
|
-
// Don't cache negative results — context may be bound by a later hook
|
|
601
|
-
return null;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
function loadContextState(contextId: string): Record<string, any> | null {
|
|
605
|
-
try {
|
|
606
|
-
return loadState(contextId) as Record<string, any> | null;
|
|
607
|
-
} catch {
|
|
608
|
-
return null;
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
function writeContextWindow(contextId: string, contextWindowData: Record<string, any>): void {
|
|
613
|
-
try {
|
|
614
|
-
const state = getContext(contextId) as Record<string, any> | null;
|
|
615
|
-
if (state) {
|
|
616
|
-
if (!state.last_session) state.last_session = {};
|
|
617
|
-
state.last_session.context_remaining_pct = contextWindowData.remaining_percentage;
|
|
618
|
-
saveState(contextId, state as any);
|
|
619
|
-
}
|
|
620
|
-
} catch { /* ignore */ }
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
// ---------------------------------------------------------------------------
|
|
624
|
-
// Main
|
|
625
|
-
// ---------------------------------------------------------------------------
|
|
626
|
-
|
|
627
|
-
function main(): void {
|
|
628
|
-
// Read JSON from stdin
|
|
629
|
-
let inputData: Record<string, any>;
|
|
630
|
-
try {
|
|
631
|
-
inputData = JSON.parse(fs.readFileSync(0, "utf-8"));
|
|
632
|
-
} catch {
|
|
633
|
-
inputData = {};
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
// Terminal width and mode
|
|
637
|
-
const termWidth = getTerminalWidth();
|
|
638
|
-
const mode = getDisplayMode(termWidth);
|
|
639
|
-
|
|
640
|
-
// Extract input fields
|
|
641
|
-
const sessionId = inputData.session_id ?? "";
|
|
642
|
-
const modelName = inputData.model?.display_name ?? "unknown";
|
|
643
|
-
const cost = inputData.cost ?? {};
|
|
644
|
-
const durationMs: number = cost.total_duration_ms ?? 0;
|
|
645
|
-
const workspace = inputData.workspace ?? {};
|
|
646
|
-
const currentDir: string = workspace.project_dir ?? process.cwd();
|
|
647
|
-
const dirName = path.basename(currentDir);
|
|
648
|
-
|
|
649
|
-
// Context window data
|
|
650
|
-
const ctxWin = inputData.context_window ?? {};
|
|
651
|
-
const usage = ctxWin.current_usage ?? {};
|
|
652
|
-
const cacheRead: number = usage.cache_read_input_tokens ?? 0;
|
|
653
|
-
const inputTokens: number = usage.input_tokens ?? 0;
|
|
654
|
-
const cacheCreation: number = usage.cache_creation_input_tokens ?? 0;
|
|
655
|
-
const outputTokens: number = usage.output_tokens ?? 0;
|
|
656
|
-
const contextMax: number = ctxWin.context_window_size ?? 200_000;
|
|
657
|
-
|
|
658
|
-
// Calculate context percentage
|
|
659
|
-
const usedPct = ctxWin.used_percentage;
|
|
660
|
-
let contextPct: number;
|
|
661
|
-
const totalInput = cacheRead + inputTokens + cacheCreation;
|
|
662
|
-
const contextUsed = totalInput + outputTokens + CONTEXT_BASELINE_TOKENS;
|
|
663
|
-
|
|
664
|
-
if (usedPct !== undefined && usedPct !== null) {
|
|
665
|
-
contextPct = Math.floor(usedPct);
|
|
666
|
-
} else {
|
|
667
|
-
contextPct = contextMax > 0 ? Math.floor((contextUsed * 100) / contextMax) : 0;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
const contextK = Math.floor(contextUsed / 1000);
|
|
671
|
-
const maxK = Math.floor(contextMax / 1000);
|
|
672
|
-
|
|
673
|
-
// Format duration
|
|
674
|
-
const durationSec = Math.floor(durationMs / 1000);
|
|
675
|
-
let timeDisplay: string;
|
|
676
|
-
if (durationSec >= 3600) {
|
|
677
|
-
timeDisplay = `${Math.floor(durationSec / 3600)}h${Math.floor((durationSec % 3600) / 60)}m`;
|
|
678
|
-
} else if (durationSec >= 60) {
|
|
679
|
-
timeDisplay = `${Math.floor(durationSec / 60)}m${durationSec % 60}s`;
|
|
680
|
-
} else {
|
|
681
|
-
timeDisplay = `${durationSec}s`;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
// Resolve context ID for display and persistence
|
|
685
|
-
const contextId = resolveContextId(sessionId);
|
|
686
|
-
|
|
687
|
-
// Render context section
|
|
688
|
-
renderContext(mode, contextPct, contextK, maxK, timeDisplay, modelName);
|
|
689
|
-
|
|
690
|
-
// Render git section
|
|
691
|
-
const git = getGitStatus(currentDir);
|
|
692
|
-
if (git) {
|
|
693
|
-
renderGit(mode, git, dirName);
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
// Render context manager line (line 3) with separator
|
|
697
|
-
console.log(SEPARATOR);
|
|
698
|
-
if (contextId) {
|
|
699
|
-
const contextState = loadContextState(contextId);
|
|
700
|
-
renderContextManager(mode, contextId, contextState);
|
|
701
|
-
} else {
|
|
702
|
-
renderNoContext(mode);
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
// Persist context_window to state.json
|
|
706
|
-
if (contextId) {
|
|
707
|
-
writeContextWindow(contextId, {
|
|
708
|
-
used_percentage: contextPct,
|
|
709
|
-
remaining_percentage: 100 - contextPct,
|
|
710
|
-
context_window_size: contextMax,
|
|
711
|
-
tokens_used: contextUsed,
|
|
712
|
-
total_input_tokens: totalInput,
|
|
713
|
-
total_output_tokens: outputTokens,
|
|
714
|
-
model: modelName,
|
|
715
|
-
last_updated: new Date().toISOString().split(".")[0],
|
|
716
|
-
});
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
main();
|
|
537
|
+
default: {
|
|
538
|
+
console.log(
|
|
539
|
+
`${CTX_ACCENT}\u25C6${RESET} ${CTX_SECONDARY}Context:${RESET} ${SLATE_300}${truncatedId}${RESET}` +
|
|
540
|
+
`${modeBadge}${planPart}`,
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function renderNoContext(mode: string): void {
|
|
547
|
+
const warn = `${ROSE}\u26A0 ${RESET}`;
|
|
548
|
+
if (mode === "normal") {
|
|
549
|
+
console.log(`${warn} ${ROSE}NO CONTEXT${RESET} ${SLATE_500}\u2014 type ^ for context manager${RESET}`);
|
|
550
|
+
} else {
|
|
551
|
+
console.log(`${warn} ${ROSE}NO CONTEXT${RESET}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ---------------------------------------------------------------------------
|
|
556
|
+
// Context persistence
|
|
557
|
+
// ---------------------------------------------------------------------------
|
|
558
|
+
|
|
559
|
+
interface StatuslineCache {
|
|
560
|
+
sessions?: Record<string, { context_id: string | null }>;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function loadCache(): StatuslineCache {
|
|
564
|
+
try {
|
|
565
|
+
if (fs.existsSync(STATUSLINE_CACHE)) {
|
|
566
|
+
return JSON.parse(fs.readFileSync(STATUSLINE_CACHE, "utf-8"));
|
|
567
|
+
}
|
|
568
|
+
} catch { /* ignore */ }
|
|
569
|
+
return {};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function saveCache(cache: StatuslineCache): void {
|
|
573
|
+
try {
|
|
574
|
+
fs.mkdirSync(path.dirname(STATUSLINE_CACHE), { recursive: true });
|
|
575
|
+
fs.writeFileSync(STATUSLINE_CACHE, JSON.stringify(cache, null, 2), "utf-8");
|
|
576
|
+
} catch { /* ignore */ }
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function resolveContextId(sessionId: string): string | null {
|
|
580
|
+
if (!sessionId || sessionId === "unknown") return null;
|
|
581
|
+
|
|
582
|
+
// Check cache first
|
|
583
|
+
const cache = loadCache();
|
|
584
|
+
const cachedEntry = cache.sessions?.[sessionId];
|
|
585
|
+
if (cachedEntry && cachedEntry.context_id !== undefined) {
|
|
586
|
+
return cachedEntry.context_id;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Cache miss — look up via context manager
|
|
590
|
+
try {
|
|
591
|
+
const context = getContextBySessionId(sessionId);
|
|
592
|
+
if (context) {
|
|
593
|
+
if (!cache.sessions) cache.sessions = {};
|
|
594
|
+
cache.sessions[sessionId] = { context_id: (context as any).id };
|
|
595
|
+
saveCache(cache);
|
|
596
|
+
return (context as any).id;
|
|
597
|
+
}
|
|
598
|
+
} catch { /* ignore */ }
|
|
599
|
+
|
|
600
|
+
// Don't cache negative results — context may be bound by a later hook
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function loadContextState(contextId: string): Record<string, any> | null {
|
|
605
|
+
try {
|
|
606
|
+
return loadState(contextId) as Record<string, any> | null;
|
|
607
|
+
} catch {
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function writeContextWindow(contextId: string, contextWindowData: Record<string, any>): void {
|
|
613
|
+
try {
|
|
614
|
+
const state = getContext(contextId) as Record<string, any> | null;
|
|
615
|
+
if (state) {
|
|
616
|
+
if (!state.last_session) state.last_session = {};
|
|
617
|
+
state.last_session.context_remaining_pct = contextWindowData.remaining_percentage;
|
|
618
|
+
saveState(contextId, state as any);
|
|
619
|
+
}
|
|
620
|
+
} catch { /* ignore */ }
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// ---------------------------------------------------------------------------
|
|
624
|
+
// Main
|
|
625
|
+
// ---------------------------------------------------------------------------
|
|
626
|
+
|
|
627
|
+
function main(): void {
|
|
628
|
+
// Read JSON from stdin
|
|
629
|
+
let inputData: Record<string, any>;
|
|
630
|
+
try {
|
|
631
|
+
inputData = JSON.parse(fs.readFileSync(0, "utf-8"));
|
|
632
|
+
} catch {
|
|
633
|
+
inputData = {};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Terminal width and mode
|
|
637
|
+
const termWidth = getTerminalWidth();
|
|
638
|
+
const mode = getDisplayMode(termWidth);
|
|
639
|
+
|
|
640
|
+
// Extract input fields
|
|
641
|
+
const sessionId = inputData.session_id ?? "";
|
|
642
|
+
const modelName = inputData.model?.display_name ?? "unknown";
|
|
643
|
+
const cost = inputData.cost ?? {};
|
|
644
|
+
const durationMs: number = cost.total_duration_ms ?? 0;
|
|
645
|
+
const workspace = inputData.workspace ?? {};
|
|
646
|
+
const currentDir: string = workspace.project_dir ?? process.cwd();
|
|
647
|
+
const dirName = path.basename(currentDir);
|
|
648
|
+
|
|
649
|
+
// Context window data
|
|
650
|
+
const ctxWin = inputData.context_window ?? {};
|
|
651
|
+
const usage = ctxWin.current_usage ?? {};
|
|
652
|
+
const cacheRead: number = usage.cache_read_input_tokens ?? 0;
|
|
653
|
+
const inputTokens: number = usage.input_tokens ?? 0;
|
|
654
|
+
const cacheCreation: number = usage.cache_creation_input_tokens ?? 0;
|
|
655
|
+
const outputTokens: number = usage.output_tokens ?? 0;
|
|
656
|
+
const contextMax: number = ctxWin.context_window_size ?? 200_000;
|
|
657
|
+
|
|
658
|
+
// Calculate context percentage
|
|
659
|
+
const usedPct = ctxWin.used_percentage;
|
|
660
|
+
let contextPct: number;
|
|
661
|
+
const totalInput = cacheRead + inputTokens + cacheCreation;
|
|
662
|
+
const contextUsed = totalInput + outputTokens + CONTEXT_BASELINE_TOKENS;
|
|
663
|
+
|
|
664
|
+
if (usedPct !== undefined && usedPct !== null) {
|
|
665
|
+
contextPct = Math.floor(usedPct);
|
|
666
|
+
} else {
|
|
667
|
+
contextPct = contextMax > 0 ? Math.floor((contextUsed * 100) / contextMax) : 0;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const contextK = Math.floor(contextUsed / 1000);
|
|
671
|
+
const maxK = Math.floor(contextMax / 1000);
|
|
672
|
+
|
|
673
|
+
// Format duration
|
|
674
|
+
const durationSec = Math.floor(durationMs / 1000);
|
|
675
|
+
let timeDisplay: string;
|
|
676
|
+
if (durationSec >= 3600) {
|
|
677
|
+
timeDisplay = `${Math.floor(durationSec / 3600)}h${Math.floor((durationSec % 3600) / 60)}m`;
|
|
678
|
+
} else if (durationSec >= 60) {
|
|
679
|
+
timeDisplay = `${Math.floor(durationSec / 60)}m${durationSec % 60}s`;
|
|
680
|
+
} else {
|
|
681
|
+
timeDisplay = `${durationSec}s`;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Resolve context ID for display and persistence
|
|
685
|
+
const contextId = resolveContextId(sessionId);
|
|
686
|
+
|
|
687
|
+
// Render context section
|
|
688
|
+
renderContext(mode, contextPct, contextK, maxK, timeDisplay, modelName);
|
|
689
|
+
|
|
690
|
+
// Render git section
|
|
691
|
+
const git = getGitStatus(currentDir);
|
|
692
|
+
if (git) {
|
|
693
|
+
renderGit(mode, git, dirName);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Render context manager line (line 3) with separator
|
|
697
|
+
console.log(SEPARATOR);
|
|
698
|
+
if (contextId) {
|
|
699
|
+
const contextState = loadContextState(contextId);
|
|
700
|
+
renderContextManager(mode, contextId, contextState);
|
|
701
|
+
} else {
|
|
702
|
+
renderNoContext(mode);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Persist context_window to state.json
|
|
706
|
+
if (contextId) {
|
|
707
|
+
writeContextWindow(contextId, {
|
|
708
|
+
used_percentage: contextPct,
|
|
709
|
+
remaining_percentage: 100 - contextPct,
|
|
710
|
+
context_window_size: contextMax,
|
|
711
|
+
tokens_used: contextUsed,
|
|
712
|
+
total_input_tokens: totalInput,
|
|
713
|
+
total_output_tokens: outputTokens,
|
|
714
|
+
model: modelName,
|
|
715
|
+
last_updated: new Date().toISOString().split(".")[0],
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
main();
|