A configurable status bar for Claude Code — band, status line, pane, and toasts driven by one config; configured with npx @sushant-kum/ccstatus or /ccstatus.

A configurable status bar for Claude Code — the mod-system analog of ccstatusline. One JSON config drives four surfaces — the above-prompt band, the native status line, an openable pane, and threshold toasts — edited with an interactive TUI and rendered live by a Claude Code mod. The TUI preview and the mod paint from the same engine, so what you preview is what you get.
Status: v1, pre-release (
0.1.0). Not yet published to npm or a hosted marketplace — the published commands below work once it ships; until then use the from-source setup.
ccstatus has two halves that share one config file. _(Published install — these work once ccstatus is on npm and the marketplace; to try it before then, see Development.)_
claude plugin marketplace add sushant-kum/ccstatus
claude plugin install ccstatus@ccstatus
Then /ccstatus toggles the band; /ccstatus pane | reload | theme <name> | edit cover the rest.
npx @sushant-kum/ccstatus
An interactive TUI with a live band preview on every screen. Changes are saved to the shared config file and the running mod hot-reloads them.
On launch, npx @sushant-kum/ccstatus checks whether the mod is installed; if it isn't, it shows a banner and offers to install it for you (press i). A successful install takes effect after you restart Claude Code or run /reload-plugins.
All four are driven by the same config and the same 21 widget types (model, context length/%, tokens, git branch/changes/worktree, session clock, cost, rate limits, block timer, cwd, custom text/command, flex separator, …), with 9 built-in themes and a configurable Powerline separator.
One JSON file at $XDG_CONFIG_HOME/ccstatus/config.json (else ~/.config/ccstatus/config.json), shared by the mod and the TUI. It is hand-editable: invalid input never crashes either half — it degrades with a warning. Colors are the named terminal palette (v1; no hex yet).
A demo gif will be added here (docs/demo.gif).
npm-workspaces monorepo: @ccstatus/core (pure engine), @sushant-kum/ccstatus (the TUI bin), and plugin/ (the mod). See CLAUDE.md and the decision/flow records under docs/.
npm install
npm test -w @ccstatus/core
npm test -w @sushant-kum/ccstatus
npm run build:plugin-core && claude plugin test plugin
MIT © Sushant Kumar
hooks/register.tsx 462 lines1import { atom, read, update } from 'claude-code';
2import type { EngineInterface, Register } from 'claude-code';
3
4import { configPath, parseConfig, shouldReload, snapshotPath } from './config-io.js';
5import { render } from './core.js';
6import type { Config, Snapshot } from './core.js';
7import { parseGit } from './git-parse.js';
8import { paintModel } from './paint.js';
9import { buildSnapshot, type RawUsage } from './snapshot-build.js';
10import { dueToasts, evalWhen } from './toasts.js';
11
12const visible = atom({ plugin: 'ccstatus', key: 'visible' } as const, true);
13const snapshot = atom({ plugin: 'ccstatus', key: 'snapshot' } as const, null);
14const config = atom({ plugin: 'ccstatus', key: 'config' } as const, null);
15const paneOpen = atom({ plugin: 'ccstatus', key: 'paneOpen' } as const, false);
16const toastFired = atom(
17 { plugin: 'ccstatus', key: 'toastFired' } as const,
18 {} as Record<string, boolean>
19);
20const configMtime = atom(
21 { plugin: 'ccstatus', key: 'configMtime' } as const,
22 null as number | null
23);
24const effort = atom({ plugin: 'ccstatus', key: 'effort' } as const, null);
25
26const GIT =
27 'r=$(git rev-parse --show-toplevel 2>/dev/null)||{ printf NO;exit 0;};' +
28 'b=$(git rev-parse --abbrev-ref HEAD 2>/dev/null);' +
29 'gd=$(git rev-parse --git-dir 2>/dev/null); case "$gd" in */worktrees/*) w=$(basename "$gd");; *) w="";; esac;' +
30 's=$(git status --porcelain 2>/dev/null);' +
31 'a=$(printf "%s\\n" "$s"|grep -cE "^(A.|.A|\\?\\?)");' +
32 'm=$(printf "%s\\n" "$s"|grep -cE "^(M.|.M)");' +
33 'd=$(printf "%s\\n" "$s"|grep -cE "^(D.|.D)");' +
34 'printf "YES\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s" "$(basename "$r")" "$b" "$w" "$a" "$m" "$d"';
35
36/**
37 * Resolve the config file path using the engine's environment.
38 * @param $ - The engine interface.
39 * @returns The absolute path to the config file.
40 */
41async function resolveConfigPath($: EngineInterface): Promise<string> {
42 const xdg = await $.env.get('XDG_CONFIG_HOME').catch(() => undefined);
43 const home = await $.env.get('HOME').catch(() => undefined);
44 return configPath({ XDG_CONFIG_HOME: xdg, HOME: home });
45}
46
47/**
48 * Read and parse the config from disk, falling back to defaults on any error.
49 * @param $ - The engine interface.
50 * @returns The loaded config and any warnings raised while validating it.
51 */
52async function loadConfigFromDisk(
53 $: EngineInterface
54): Promise<{ config: Config; warnings: string[] }> {
55 try {
56 const text = await $.fs.read(await resolveConfigPath($)).catch(() => null);
57 return parseConfig(typeof text === 'string' ? text : null);
58 } catch {
59 return parseConfig(null);
60 }
61}
62
63/**
64 * Re-read the config when its mtime changed, or always when forced.
65 * @param $ - The engine interface.
66 * @param force - When `true`, reload regardless of the file's mtime.
67 * @returns `true` when the config was reloaded, `false` otherwise.
68 */
69async function reloadConfig($: EngineInterface, force: boolean): Promise<boolean> {
70 try {
71 const path = await resolveConfigPath($);
72 const stat = await $.fs.stat(path).catch(() => null);
73 const prev = await read($, configMtime);
74 if (!force && (!stat || !shouldReload(prev, stat))) {
75 return false;
76 }
77 const { config: cfg, warnings } = await loadConfigFromDisk($);
78 await update($, config, () => cfg);
79 await update($, configMtime, () => (stat ? stat.mtimeMs : null));
80 // Make a rejected/degraded config observable instead of silently showing defaults.
81 if (warnings.length) {
82 try {
83 await $.ui.toast(
84 `ccstatus: config had ${warnings.length} problem(s); using defaults where needed — run \`npx @sushant-kum/ccstatus\` to fix`
85 );
86 } catch {
87 /* guarded: never throw from a reload */
88 }
89 }
90 return true;
91 } catch {
92 return false;
93 }
94}
95
96/**
97 * Switch the active theme and persist it to the config file atomically.
98 * @param $ - The engine interface.
99 * @param name - The theme name to switch to.
100 * @returns A status message describing the outcome.
101 */
102async function setTheme($: EngineInterface, name: string): Promise<string> {
103 const cfg = (await read($, config)) as Config | null;
104 if (!cfg) {
105 return 'ccstatus: config not loaded yet.';
106 }
107 if (!name || !cfg.themes[name]) {
108 return `ccstatus: unknown theme "${name}". Available: ${Object.keys(cfg.themes).join(', ')}`;
109 }
110 const next: Config = { ...cfg, theme: name };
111 await update($, config, () => next);
112 const path = await resolveConfigPath($);
113 const tmp = path + '.tmp';
114 try {
115 // Atomic write, matching the TUI's saveConfigFile discipline. The sandbox fs
116 // has no rename, but $.process.run does (the same shell-out the git snapshot
117 // uses), and rename within one directory is atomic — so a reader never sees a
118 // half-written config.json, and a .bak keeps the prior file recoverable.
119 const existing = await $.fs.read(path).catch(() => null);
120 if (typeof existing === 'string') {
121 await $.fs.write(path + '.bak', existing).catch(() => {
122 /* no-op: a missing backup must not abort the write */
123 });
124 }
125 await $.fs.write(tmp, JSON.stringify(next, null, 2) + '\n');
126 const { exitCode } = await $.process.run(['mv', '-f', tmp, path], { timeoutMs: 5000 });
127 if (exitCode !== 0) {
128 return `ccstatus: theme set to ${name} (could not save the config file).`;
129 }
130 } catch {
131 return `ccstatus: theme set to ${name} (could not save the config file).`;
132 }
133 return `ccstatus: theme set to ${name}.`;
134}
135
136type UsageRaw = Awaited<ReturnType<EngineInterface['session']['usage']>>;
137
138/**
139 * Run the git probe in a directory, returning no-repo fields on any failure.
140 * @param $ - The engine interface.
141 * @param cwd - The directory to probe, or an empty string to skip the probe.
142 * @returns The parsed git fields for the directory.
143 */
144async function probeGit($: EngineInterface, cwd: string): Promise<ReturnType<typeof parseGit>> {
145 if (!cwd) {
146 return parseGit('NO');
147 }
148 try {
149 const { stdout } = await $.process.run(['sh', '-c', GIT], { cwd, timeoutMs: 5000 });
150 return parseGit(stdout || '');
151 } catch {
152 return parseGit('NO');
153 }
154}
155
156type UsageContext = NonNullable<UsageRaw>['context'];
157type UsageRateLimits = NonNullable<UsageRaw>['rateLimits'];
158
159/**
160 * Reduce the context portion of the raw usage report into snapshot fields.
161 * @param context - The raw context report, or `null`/`undefined` when absent.
162 * @returns The normalized context usage figures.
163 */
164function extractContextUsage(
165 context: UsageContext | null
166): Pick<RawUsage & object, 'ctxTokens' | 'ctxPct' | 'cached' | 'input' | 'output'> {
167 const b = context?.breakdown ?? null;
168 const u = b?.apiUsage ?? null;
169 return {
170 ctxTokens: b?.totalTokens ?? context?.tokens ?? 0,
171 ctxPct: b?.percentage ?? context?.percent ?? 0,
172 cached: u ? u.cache_read_input_tokens + u.cache_creation_input_tokens : 0,
173 input: u?.input_tokens ?? 0,
174 output: u?.output_tokens ?? 0,
175 };
176}
177
178/**
179 * Reduce the rate-limit portion of the raw usage report into snapshot fields.
180 * @param rl - The raw rate-limit entries.
181 * @returns The normalized five-hour and weekly usage figures.
182 */
183function extractRateLimitUsage(
184 rl: UsageRateLimits
185): Pick<RawUsage & object, 'fivePct' | 'fiveReset' | 'weekPct' | 'weekReset'> {
186 const five = rl.find((r) => r.kind === 'five_hour') ?? null;
187 const week = rl.find((r) => r.kind === 'seven_day') ?? null;
188 return {
189 fivePct: five?.percentUsed ?? null,
190 fiveReset: five?.resetsAt ?? null,
191 weekPct: week?.percentUsed ?? null,
192 weekReset: week?.resetsAt ?? null,
193 };
194}
195
196/**
197 * Reduce the engine's raw usage report into the fields the snapshot needs.
198 * @param usageRaw - The raw usage report, or `null` when it could not be read.
199 * @returns The normalized usage figures.
200 */
201function extractUsage(usageRaw: UsageRaw | null): RawUsage {
202 const context = usageRaw?.context ?? null;
203 const rl = usageRaw?.rateLimits ?? [];
204 return {
205 ...extractContextUsage(context),
206 ...extractRateLimitUsage(rl),
207 startedAt: usageRaw?.startedAt ?? 0,
208 };
209}
210
211/**
212 * Gather a live snapshot from the session, git, and usage sources.
213 * @param $ - The engine interface.
214 * @param eff - The active reasoning effort, or `null` when unknown.
215 * @returns The assembled snapshot.
216 */
217async function measure($: EngineInterface, eff: string | null): Promise<Snapshot> {
218 const [version, model, cwd, usageRaw] = await Promise.all([
219 $.session
220 .version()
221 .then((v) => v.version)
222 .catch(() => ''),
223 $.session.model().catch(() => ''),
224 $.session.cwd().catch(() => ''),
225 $.session.usage({ breakdown: 'summary' }).catch(() => null),
226 ]);
227 const git = await probeGit($, cwd);
228 const usage = extractUsage(usageRaw);
229 return buildSnapshot({ version, model, effort: eff, cwd, now: await $.clock.now(), git, usage });
230}
231
232/**
233 * Fire any due toasts and update the per-rule fired state, never throwing.
234 * @param $ - The engine interface.
235 * @param cfg - The active config.
236 * @param snap - The current snapshot the toast rules are evaluated against.
237 */
238async function fireToasts($: EngineInterface, cfg: Config, snap: Snapshot): Promise<void> {
239 try {
240 if (!cfg.surfaces.toasts.enabled) {
241 return;
242 }
243 const rules = cfg.surfaces.toasts.rules;
244 const fired = (await read($, toastFired)) ?? {};
245 const next: Record<string, boolean> = { ...fired };
246 for (const t of dueToasts(rules, snap, fired)) {
247 try {
248 await $.ui.toast(t.text);
249 } catch {
250 /* guarded */
251 }
252 next[t.key] = true;
253 }
254 for (const r of rules) {
255 if (!evalWhen(r.when, snap)) {
256 delete next[r.when];
257 }
258 }
259 if (JSON.stringify(next) !== JSON.stringify(fired)) {
260 await update($, toastFired, () => next);
261 }
262 } catch {
263 /* never throw from a refresh */
264 }
265}
266
267/**
268 * Fire any due toasts for a snapshot, never throwing. The native status line is
269 * rendered by the `ccstatus statusline` settings command, not the mod.
270 * @param $ - The engine interface.
271 * @param snap - The current snapshot to evaluate toasts against.
272 */
273async function applySurfaces($: EngineInterface, snap: Snapshot): Promise<void> {
274 try {
275 const cfg = (await read($, config)) as Config | null;
276 if (!cfg) {
277 return;
278 }
279 await fireToasts($, cfg, snap);
280 } catch {
281 /* never throw from a refresh */
282 }
283}
284
285let refreshing = false;
286/**
287 * Measure a fresh snapshot, persist it, and repaint all surfaces.
288 * @param $ - The engine interface.
289 */
290async function refresh($: EngineInterface): Promise<void> {
291 if (refreshing) {
292 return;
293 }
294 refreshing = true;
295 try {
296 const eff = await read($, effort).catch(() => null);
297 const s = await measure($, eff); // compute first, then write (updater is sync)
298 await update($, snapshot, () => s);
299 try {
300 const env = {
301 HOME: await $.env.get('HOME').catch(() => undefined),
302 XDG_CONFIG_HOME: await $.env.get('XDG_CONFIG_HOME').catch(() => undefined),
303 };
304 await $.fs.write(snapshotPath(env), JSON.stringify(s)).catch(() => {
305 /* no-op: snapshot persistence is best-effort */
306 });
307 } catch {
308 /* ignore snapshot persistence errors */
309 }
310 await applySurfaces($, s);
311 } catch {
312 /* keep last snapshot */
313 } finally {
314 refreshing = false;
315 }
316}
317
318/**
319 * Toggle the ccstatus pane open or closed.
320 * @param $ - The engine interface.
321 * @returns A status message describing the new pane state.
322 */
323async function togglePane($: EngineInterface): Promise<string> {
324 if (await read($, paneOpen)) {
325 await $.ui.close({ id: 'ccstatus' });
326 await update($, paneOpen, () => false);
327 return 'ccstatus pane closed.';
328 }
329 await $.ui.open({ id: 'ccstatus', title: 'ccstatus' });
330 await update($, paneOpen, () => true);
331 return 'ccstatus pane opened.';
332}
333
334/**
335 * Capture the active reasoning effort from an engine event into plugin state.
336 * @param $ - The engine interface.
337 * @param e - The engine event, inspected for an `effort.active` field.
338 * @returns A promise that resolves once the effort has been recorded.
339 */
340function captureEffort($: EngineInterface, e: unknown): Promise<void> {
341 const active = (e as { effort?: { active?: unknown } })?.effort?.active;
342 if (typeof active === 'string' && active) {
343 return update($, effort, () => active).then(
344 () => undefined,
345 () => undefined
346 );
347 }
348 return Promise.resolve();
349}
350
351export const register: Register = (on) => {
352 on('session.start', async ($, e, next) => {
353 try {
354 await $.command.register({
355 name: 'ccstatus',
356 description: 'Toggle the ccstatus band above the prompt',
357 });
358 await reloadConfig($, true);
359 await refresh($);
360 $.clock.every(2000, () => void reloadConfig($, false).then(() => refresh($)));
361 } catch {
362 /* never block session start */
363 }
364 return next(e);
365 });
366
367 on('turn.complete', async ($, e, next) => {
368 await captureEffort($, e);
369 await refresh($);
370 return next(e);
371 });
372
373 on('tool.call', async ($, e, next) => {
374 await captureEffort($, e);
375 return next(e);
376 });
377
378 on('command.run', { command: 'ccstatus' }, async ($, e) => {
379 try {
380 const args = String((e as { args?: unknown })?.args ?? '').trim();
381 const [sub = '', ...rest] = args.split(/\s+/);
382 if (sub === 'pane') {
383 return { text: await togglePane($) };
384 }
385 if (sub === 'reload') {
386 await reloadConfig($, true);
387 await refresh($);
388 return { text: 'ccstatus config reloaded.' };
389 }
390 if (sub === 'theme') {
391 return { text: await setTheme($, rest[0] ?? '') };
392 }
393 if (sub === 'edit') {
394 return {
395 text:
396 'Edit your layout with the ccstatus editor: run `npx @sushant-kum/ccstatus` in a terminal. ' +
397 'Changes are picked up automatically.',
398 };
399 }
400 const now = !(await read($, visible));
401 await update($, visible, () => now);
402 return { text: now ? 'ccstatus band shown.' : 'ccstatus band hidden.' };
403 } catch {
404 return { text: 'ccstatus: command failed.' };
405 }
406 });
407
408 on('ui.close', async ($, e, next) => {
409 try {
410 if ((e as { id?: string }).id === 'ccstatus') {
411 await update($, paneOpen, () => false);
412 }
413 } catch {
414 /* ignore */
415 }
416 return next(e);
417 });
418
419 on('ui.render', { component: 'Pane', requestId: 'ccstatus' }, async ($, e, next) => {
420 try {
421 const cfg = await read($, config);
422 const snap = await read($, snapshot);
423 if (!cfg || !snap) {
424 return next(e);
425 }
426 const model = render(cfg, snap, { surface: 'pane', width: e.props.bodyColumns || 80 });
427 if (model.lines.length === 0) {
428 return next(e);
429 }
430 // eslint-disable-next-line @typescript-eslint/no-explicit-any -- engine boundary: $.ui.resolve + handler element types
431 return paintModel(model, $.ui.resolve(e) as any) as any;
432 } catch {
433 return next(e);
434 }
435 });
436
437 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
438 try {
439 if (e.props.hasSurvey || !(await read($, visible))) {
440 return next(e);
441 }
442 const cfg = await read($, config);
443 const snap = await read($, snapshot);
444 if (!cfg || !snap) {
445 return next(e);
446 }
447 const width = e.props.bodyColumns;
448 if (!width) {
449 return next(e);
450 } // not measured yet: let the engine draw
451 const model = render(cfg, snap, { surface: 'band', width });
452 if (model.lines.length === 0) {
453 return next(e);
454 }
455 // eslint-disable-next-line @typescript-eslint/no-explicit-any -- engine boundary: $.ui.resolve + handler element types
456 return paintModel(model, $.ui.resolve(e) as any) as any;
457 } catch {
458 return next(e);
459 }
460 });
461};
462hooks/config-io.ts 55 lines1import { loadConfig } from './core.js';
2import type { Config } from './core.js';
3
4/**
5 * Resolve the absolute path to the shared config file from the environment.
6 * @param env - The process environment, read for `XDG_CONFIG_HOME` and `HOME`.
7 * @returns The absolute path to `config.json`.
8 */
9export function configPath(env: Record<string, string | undefined>): string {
10 const xdg = env['XDG_CONFIG_HOME'];
11 const base = xdg && xdg.startsWith('/') ? xdg : `${env['HOME'] ?? ''}/.config`;
12 return `${base}/ccstatus/config.json`;
13}
14
15/**
16 * Resolve the absolute path to the shared snapshot file from the environment.
17 * @param env - The process environment, read for `XDG_CONFIG_HOME` and `HOME`.
18 * @returns The absolute path to `snapshot.json`.
19 */
20export function snapshotPath(env: Record<string, string | undefined>): string {
21 const xdg = env['XDG_CONFIG_HOME'];
22 const base = xdg && xdg.startsWith('/') ? xdg : `${env['HOME'] ?? ''}/.config`;
23 return `${base}/ccstatus/snapshot.json`;
24}
25
26/**
27 * Parse config file text into a validated config, never throwing.
28 * @param text - The raw config file contents, or `null` when the file is absent.
29 * @returns The loaded config and any warnings raised while validating it.
30 */
31export function parseConfig(text: string | null): { config: Config; warnings: string[] } {
32 if (text === null) {
33 return loadConfig(undefined);
34 }
35 let parsed: unknown;
36 try {
37 parsed = JSON.parse(text);
38 } catch {
39 const fallback = loadConfig(undefined);
40 return { config: fallback.config, warnings: ['config file is not valid JSON; using defaults'] };
41 }
42 return loadConfig(parsed);
43}
44
45/**
46 * Decide whether the config should be reloaded given its last-seen mtime.
47 * @param prevMtime - The previously observed modification time, or `null` when never read.
48 * @param stat - The current file stat.
49 * @param stat.mtimeMs - The current modification time in milliseconds.
50 * @returns `true` when the config has not been read before or has changed since.
51 */
52export function shouldReload(prevMtime: number | null, stat: { mtimeMs: number }): boolean {
53 return prevMtime === null || stat.mtimeMs !== prevMtime;
54}
55hooks/core.js 4928 lines1var __defProp = Object.defineProperty;
2var __export = (target, all) => {
3 for (var name in all)
4 __defProp(target, name, { get: all[name], enumerable: true });
5};
6
7// packages/core/src/theme/themes.ts
8function resolveColors(item, config) {
9 var _a;
10 const themeEntry = (_a = config.themes[config.theme]) == null ? void 0 : _a[item.type];
11 const fg2 = item.type === "flex-separator" ? void 0 : item.fg;
12 const bg2 = item.type === "flex-separator" ? void 0 : item.bg;
13 return {
14 fg: fg2 ?? (themeEntry == null ? void 0 : themeEntry.fg),
15 bg: bg2 ?? (themeEntry == null ? void 0 : themeEntry.bg)
16 };
17}
18var WIDGETS = [
19 "model",
20 "version",
21 "context-percentage",
22 "git-branch",
23 "git-changes",
24 "tokens-input",
25 "tokens-output",
26 "tokens-total"
27];
28var mk = (rows) => {
29 const t = {};
30 WIDGETS.forEach((w, i) => {
31 const row = rows[i];
32 if (!row) {
33 throw new Error(`theme definition is missing a color row for widget "${w}"`);
34 }
35 const [fg2, bg2] = row;
36 t[w] = { fg: fg2, bg: bg2 };
37 });
38 return t;
39};
40var builtinThemes = {
41 default: {
42 model: { fg: "black", bg: "bgCyan" },
43 version: { fg: "white", bg: "bgBlue" },
44 "context-percentage": { fg: "black", bg: "bgBrightYellow" },
45 "git-branch": { fg: "black", bg: "bgCyan" },
46 "git-changes": { fg: "white", bg: "bgBrightBlack" }
47 },
48 classic: mk([
49 ["black", "bgYellow"],
50 ["black", "bgWhite"],
51 ["black", "bgGreen"],
52 ["white", "bgBlue"],
53 ["black", "bgMagenta"],
54 ["black", "bgCyan"],
55 ["black", "bgBrightYellow"],
56 ["white", "bgRed"]
57 ]),
58 nord: mk([
59 ["black", "bgBrightCyan"],
60 ["black", "bgCyan"],
61 ["black", "bgBlue"],
62 ["white", "bgBrightBlack"],
63 ["black", "bgBrightBlue"],
64 ["black", "bgGreen"],
65 ["black", "bgYellow"],
66 ["white", "bgBlack"]
67 ]),
68 dracula: mk([
69 ["black", "bgMagenta"],
70 ["black", "bgBrightMagenta"],
71 ["black", "bgBrightGreen"],
72 ["black", "bgBrightCyan"],
73 ["black", "bgBrightYellow"],
74 ["black", "bgBrightRed"],
75 ["white", "bgBrightBlack"],
76 ["white", "bgMagenta"]
77 ]),
78 gruvbox: mk([
79 ["black", "bgYellow"],
80 ["black", "bgBrightYellow"],
81 ["black", "bgGreen"],
82 ["black", "bgCyan"],
83 ["black", "bgRed"],
84 ["black", "bgMagenta"],
85 ["white", "bgBrightBlack"],
86 ["white", "bgBlack"]
87 ]),
88 monokai: mk([
89 ["black", "bgBrightGreen"],
90 ["black", "bgBrightYellow"],
91 ["black", "bgBrightMagenta"],
92 ["black", "bgBrightCyan"],
93 ["black", "bgBrightRed"],
94 ["white", "bgBrightBlack"],
95 ["black", "bgYellow"],
96 ["white", "bgBlack"]
97 ]),
98 "one-dark": mk([
99 ["black", "bgBlue"],
100 ["black", "bgBrightBlue"],
101 ["black", "bgGreen"],
102 ["black", "bgMagenta"],
103 ["black", "bgYellow"],
104 ["black", "bgCyan"],
105 ["white", "bgBrightBlack"],
106 ["white", "bgBlack"]
107 ]),
108 solarized: mk([
109 ["white", "bgBlue"],
110 ["black", "bgCyan"],
111 ["black", "bgYellow"],
112 ["white", "bgGreen"],
113 ["white", "bgMagenta"],
114 ["white", "bgRed"],
115 ["white", "bgBrightBlack"],
116 ["white", "bgBlack"]
117 ]),
118 "tokyo-night": mk([
119 ["black", "bgBrightBlue"],
120 ["black", "bgBlue"],
121 ["black", "bgBrightGreen"],
122 ["black", "bgBrightMagenta"],
123 ["black", "bgBrightYellow"],
124 ["black", "bgBrightCyan"],
125 ["white", "bgBrightBlack"],
126 ["white", "bgBlack"]
127 ])
128};
129
130// packages/core/src/widgets/registry.ts
131var registry = {};
132function register(def) {
133 registry[def.type] = def;
134}
135
136// packages/core/src/layout/cells.ts
137function itemToCell(item, config, snapshot, commandOutputs) {
138 const def = registry[item.type];
139 if (!def) {
140 return null;
141 }
142 const value = def.format({ snapshot, item, commandOutputs });
143 if (value === null) {
144 return null;
145 }
146 const flex = item.type === "flex-separator";
147 const pad = flex ? "" : " ".repeat(Math.max(0, Math.floor(config.defaults.padding)));
148 const { fg: fg2, bg: bg2 } = resolveColors(item, config);
149 const merge = item.type === "flex-separator" ? false : item.merge ?? false;
150 return { text: flex ? "" : `${pad}${value}${pad}`, fg: fg2, bg: bg2, merge, flex };
151}
152function lineToCells(items, config, snapshot, commandOutputs) {
153 return items.map((it) => itemToCell(it, config, snapshot, commandOutputs)).filter((c) => c !== null);
154}
155
156// packages/core/src/layout/compose.ts
157function bgToFg(bg2) {
158 if (!bg2) {
159 return void 0;
160 }
161 if (!bg2.startsWith("bg")) {
162 return bg2;
163 }
164 const rest = bg2.slice(2);
165 return rest.charAt(0).toLowerCase() + rest.slice(1);
166}
167function mergeCells(cells) {
168 const out = [];
169 for (const c of cells) {
170 const prev = out[out.length - 1];
171 if (c.merge && prev && !prev.flex && !c.flex) {
172 prev.text = `${prev.text}${c.text}`;
173 } else {
174 out.push({ ...c });
175 }
176 }
177 return out;
178}
179function truncate(segs, width) {
180 const total = segs.reduce((n, s) => n + s.text.length, 0);
181 if (total <= width) {
182 return segs;
183 }
184 let budget = width;
185 const out = [];
186 for (const s of segs) {
187 if (s.kind === "cap") {
188 continue;
189 }
190 if (budget <= 0) {
191 break;
192 }
193 if (s.text.length <= budget) {
194 out.push(s);
195 budget -= s.text.length;
196 } else {
197 out.push({ ...s, text: s.text.slice(0, budget) });
198 budget = 0;
199 }
200 }
201 return out;
202}
203function composeLine(cells, mode, width, glyph = "\u2592") {
204 var _a, _b;
205 const merged = mergeCells(cells);
206 const content = merged.filter((c) => !c.flex);
207 const flexCount = merged.filter((c) => c.flex).length;
208 const contentWidth = content.reduce((n, c) => n + c.text.length, 0);
209 let sepWidth = 0;
210 if (mode === "powerline") {
211 sepWidth = Math.max(0, content.length - 1) + (content.length ? 2 : 0);
212 } else if (mode === "space") {
213 sepWidth = Math.max(0, content.length - 1);
214 }
215 const leftover = Math.max(0, width - contentWidth - sepWidth);
216 const per = flexCount ? Math.floor(leftover / flexCount) : 0;
217 let extra = flexCount ? leftover - per * flexCount : 0;
218 const flexWidth = () => {
219 const w = per + (extra > 0 ? 1 : 0);
220 if (extra > 0) {
221 extra -= 1;
222 }
223 return w;
224 };
225 const segs = [];
226 const emitStartCap = mode === "powerline" && content.length > 0;
227 if (emitStartCap) {
228 segs.push({ kind: "cap", text: glyph, fg: bgToFg((_a = content[0]) == null ? void 0 : _a.bg) });
229 }
230 let ci = 0;
231 for (const c of merged) {
232 if (c.flex) {
233 segs.push({ kind: "flex", text: " ".repeat(flexWidth()) });
234 continue;
235 }
236 segs.push({ kind: "cell", text: c.text, fg: c.fg, bg: c.bg });
237 const isLast = ci === content.length - 1;
238 const next = content[ci + 1];
239 if (!isLast && next) {
240 if (mode === "powerline") {
241 segs.push({ kind: "separator", text: glyph, fg: bgToFg(c.bg), bg: next.bg });
242 } else if (mode === "space") {
243 segs.push({ kind: "separator", text: " " });
244 }
245 }
246 ci += 1;
247 }
248 if (emitStartCap) {
249 segs.push({ kind: "cap", text: glyph, fg: bgToFg((_b = content[content.length - 1]) == null ? void 0 : _b.bg) });
250 }
251 return truncate(segs, width);
252}
253
254// packages/core/src/format.ts
255function fmtNum(n) {
256 const trim = (v, suffix) => `${v.toFixed(1).replace(/\.0$/, "")}${suffix}`;
257 if (Math.abs(n) >= 1e6) {
258 return trim(n / 1e6, "M");
259 }
260 if (Math.abs(n) >= 1e3) {
261 return trim(n / 1e3, "k");
262 }
263 return String(Math.round(n));
264}
265function fmtDur(ms) {
266 let s = Math.max(0, Math.floor(ms / 1e3));
267 const h = Math.floor(s / 3600);
268 s -= h * 3600;
269 const m = Math.floor(s / 60);
270 s -= m * 60;
271 if (h > 0) {
272 return `${h}h ${m}m`;
273 }
274 if (m > 0) {
275 return `${m}m ${s}s`;
276 }
277 return `${s}s`;
278}
279function pct(x) {
280 return `${Math.round(x)}%`;
281}
282function bar(x, width = 5) {
283 const filled = Math.floor(Math.min(100, Math.max(0, x)) / 100 * width);
284 return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
285}
286
287// packages/core/src/widgets/data-widgets.ts
288var labeled = (raw, label, value) => raw ? value : `${label}: ${value}`;
289var rawOf = (item) => "rawValue" in item ? item.rawValue : void 0;
290register({ type: "model", label: "Model", format: ({ snapshot }) => snapshot.model || null });
291register({
292 type: "version",
293 label: "Version",
294 format: ({ snapshot, item }) => snapshot.version ? rawOf(item) ? snapshot.version : `v${snapshot.version}` : null
295});
296register({
297 type: "context-length",
298 label: "Ctx",
299 format: ({ snapshot, item }) => labeled(rawOf(item), "Ctx", fmtNum(snapshot.ctxTokens))
300});
301register({
302 type: "context-percentage",
303 label: "Ctx Used",
304 format: ({ snapshot, item }) => labeled(rawOf(item), "Ctx Used", pct(snapshot.ctxPct))
305});
306register({
307 type: "tokens-input",
308 label: "In",
309 format: ({ snapshot, item }) => labeled(rawOf(item), "In", fmtNum(snapshot.input))
310});
311register({
312 type: "tokens-output",
313 label: "Out",
314 format: ({ snapshot, item }) => labeled(rawOf(item), "Out", fmtNum(snapshot.output))
315});
316register({
317 type: "tokens-cached",
318 label: "Cached",
319 format: ({ snapshot, item }) => labeled(rawOf(item), "Cached", fmtNum(snapshot.cached))
320});
321register({
322 type: "tokens-total",
323 label: "Total",
324 format: ({ snapshot, item }) => labeled(rawOf(item), "Total", fmtNum(snapshot.total))
325});
326register({
327 type: "session-clock",
328 label: "Session",
329 format: ({ snapshot, item }) => labeled(rawOf(item), "Session", fmtDur(snapshot.now - snapshot.startedAt))
330});
331register({
332 type: "cwd",
333 label: "cwd",
334 format: ({ snapshot }) => {
335 if (!snapshot.cwd) {
336 return null;
337 }
338 const parts = snapshot.cwd.split("/").filter(Boolean);
339 return parts[parts.length - 1] ?? snapshot.cwd;
340 }
341});
342
343// packages/core/src/widgets/git-widgets.ts
344register({
345 type: "git-branch",
346 label: "Branch",
347 format: ({ snapshot }) => snapshot.repo ? `\u270E ${snapshot.gitBranch || "?"}` : null
348});
349register({
350 type: "git-root-dir",
351 label: "Git root",
352 format: ({ snapshot }) => snapshot.repo ? `\u2302 ${snapshot.gitRoot || "?"}` : null
353});
354register({
355 type: "git-changes",
356 label: "Changes",
357 format: ({ snapshot }) => snapshot.repo ? `(+${snapshot.added} ~${snapshot.modified} -${snapshot.deleted})` : null
358});
359register({
360 type: "git-worktree",
361 label: "Worktree",
362 format: ({ snapshot }) => snapshot.repo && snapshot.gitWorktree ? `\u2387 ${snapshot.gitWorktree}` : null
363});
364
365// packages/core/src/widgets/usage-widgets.ts
366function until(iso, now) {
367 if (!iso) {
368 return null;
369 }
370 const t = Date.parse(iso);
371 if (Number.isNaN(t)) {
372 return null;
373 }
374 const d = t - now;
375 return d > 0 ? d : null;
376}
377register({
378 type: "cost",
379 label: "Cost",
380 format: ({ snapshot }) => snapshot.cost === null ? null : `$${snapshot.cost.toFixed(2)}`
381});
382register({
383 type: "rate-limit-5h",
384 label: "Session limit",
385 format: ({ snapshot }) => {
386 if (snapshot.fivePct === null) {
387 return "Session: \u2014";
388 }
389 const reset = until(snapshot.fiveReset, snapshot.now);
390 return `Session: ${bar(snapshot.fivePct)} ${pct(snapshot.fivePct)}${reset !== null ? ` ${fmtDur(reset)}` : ""}`;
391 }
392});
393register({
394 type: "rate-limit-week",
395 label: "Weekly limit",
396 format: ({ snapshot }) => {
397 if (snapshot.weekPct === null) {
398 return "Weekly: \u2014";
399 }
400 const reset = until(snapshot.weekReset, snapshot.now);
401 return `Weekly: ${pct(snapshot.weekPct)}${reset !== null ? ` ${fmtDur(reset)}` : ""}`;
402 }
403});
404register({
405 type: "block-timer",
406 label: "Block",
407 format: ({ snapshot }) => {
408 const reset = until(snapshot.blockReset, snapshot.now);
409 return reset === null ? null : `Block: ${fmtDur(reset)}`;
410 }
411});
412
413// packages/core/src/widgets/custom-widgets.ts
414register({
415 type: "custom-text",
416 label: "Custom text",
417 format: ({ item }) => {
418 const text = item.type === "custom-text" ? item.text : "";
419 return text.length ? text : null;
420 }
421});
422register({
423 type: "custom-command",
424 label: "Custom command",
425 format: ({ item, commandOutputs }) => {
426 const out = (commandOutputs[item.id] ?? "").trim();
427 return out.length ? out : null;
428 }
429});
430register({ type: "flex-separator", label: "Flex separator", format: () => "" });
431
432// packages/core/src/render.ts
433function render(config, snapshot, opts) {
434 const surface = config.surfaces[opts.surface];
435 if (!surface.enabled) {
436 return { lines: [] };
437 }
438 const outputs = opts.commandOutputs ?? {};
439 const lines = surface.lines.map((items) => lineToCells(items, config, snapshot, outputs)).filter((cells) => cells.length > 0).map((cells) => ({
440 segments: composeLine(cells, config.defaults.separator, opts.width, config.defaults.glyph)
441 }));
442 return { lines };
443}
444
445// packages/core/src/ansi.ts
446var FG = {
447 black: 30,
448 red: 31,
449 green: 32,
450 yellow: 33,
451 blue: 34,
452 magenta: 35,
453 cyan: 36,
454 white: 37,
455 gray: 90,
456 brightBlack: 90,
457 brightRed: 91,
458 brightGreen: 92,
459 brightYellow: 93,
460 brightBlue: 94,
461 brightMagenta: 95,
462 brightCyan: 96,
463 brightWhite: 97,
464 brightGray: 97
465};
466var BG = {
467 bgBlack: 40,
468 bgRed: 41,
469 bgGreen: 42,
470 bgYellow: 43,
471 bgBlue: 44,
472 bgMagenta: 45,
473 bgCyan: 46,
474 bgWhite: 47,
475 bgGray: 100,
476 bgBrightBlack: 100,
477 bgBrightRed: 101,
478 bgBrightGreen: 102,
479 bgBrightYellow: 103,
480 bgBrightBlue: 104,
481 bgBrightMagenta: 105,
482 bgBrightCyan: 106,
483 bgBrightWhite: 107,
484 bgBrightGray: 107
485};
486function sgr(seg) {
487 const codes = [];
488 if (seg.fg && FG[seg.fg] !== void 0) {
489 codes.push(`\x1B[${FG[seg.fg]}m`);
490 }
491 if (seg.bg && BG[seg.bg] !== void 0) {
492 codes.push(`\x1B[${BG[seg.bg]}m`);
493 }
494 if (!codes.length) {
495 return seg.text;
496 }
497 return `${codes.join("")}${seg.text}\x1B[0m`;
498}
499function toAnsi(model) {
500 return model.lines.map((line) => line.segments.map(sgr).join("")).join("\n");
501}
502
503// packages/core/src/colors.ts
504var BASE = [
505 "black",
506 "white",
507 "red",
508 "green",
509 "yellow",
510 "blue",
511 "magenta",
512 "cyan",
513 "gray"
514];
515var cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
516var fg = BASE.flatMap((c) => [c, `bright${cap(c)}`]);
517var bg = BASE.flatMap((c) => [`bg${cap(c)}`, `bgBright${cap(c)}`]);
518var COLORS = /* @__PURE__ */ new Set([...fg, ...bg]);
519function isColor(x) {
520 return typeof x === "string" && COLORS.has(x);
521}
522
523// packages/core/src/toast.ts
524var TOAST_FIELDS = {
525 ctxPct: (s) => s.ctxPct,
526 fivePct: (s) => s.fivePct,
527 weekPct: (s) => s.weekPct,
528 ctxTokens: (s) => s.ctxTokens,
529 total: (s) => s.total,
530 cost: (s) => s.cost
531};
532var WHEN_RE = /^\s*([a-zA-Z]+)\s*(>=|<=|==|>|<)\s*(-?\d+(?:\.\d+)?)\s*$/;
533function isValidWhen(when) {
534 const m = WHEN_RE.exec(when);
535 if (m === null) {
536 return false;
537 }
538 const field = m[1];
539 return field !== void 0 && field in TOAST_FIELDS;
540}
541function evalWhen(when, snapshot) {
542 const m = WHEN_RE.exec(when);
543 if (!m) {
544 return false;
545 }
546 const field = m[1];
547 const getter = field === void 0 ? void 0 : TOAST_FIELDS[field];
548 if (!getter) {
549 return false;
550 }
551 const value = getter(snapshot);
552 if (value === null || value === void 0 || Number.isNaN(value)) {
553 return false;
554 }
555 const n = Number(m[3]);
556 switch (m[2]) {
557 case ">":
558 return value > n;
559 case ">=":
560 return value >= n;
561 case "<":
562 return value < n;
563 case "<=":
564 return value <= n;
565 case "==":
566 return value === n;
567 default:
568 return false;
569 }
570}
571
572// packages/core/src/config/defaults.ts
573var defaultConfig = {
574 version: 1,
575 theme: "default",
576 themes: structuredClone(builtinThemes),
577 defaults: { separator: "powerline", padding: 1, align: "left", glyph: "\u2592", invert: false },
578 surfaces: {
579 band: {
580 enabled: true,
581 lines: [
582 [
583 { id: "d-version", type: "version" },
584 { id: "d-model", type: "model" },
585 { id: "d-ctx", type: "context-length" },
586 { id: "d-ctxp", type: "context-percentage" }
587 ]
588 ]
589 },
590 statusline: { enabled: false, lines: [[]] },
591 pane: { enabled: false, lines: [[]] },
592 toasts: {
593 enabled: true,
594 rules: [{ when: "ctxPct>80", text: "Context over 80%", once: true }]
595 }
596 }
597};
598
599// node_modules/zod/v3/external.js
600var external_exports = {};
601__export(external_exports, {
602 BRAND: () => BRAND,
603 DIRTY: () => DIRTY,
604 EMPTY_PATH: () => EMPTY_PATH,
605 INVALID: () => INVALID,
606 NEVER: () => NEVER,
607 OK: () => OK,
608 ParseStatus: () => ParseStatus,
609 Schema: () => ZodType,
610 ZodAny: () => ZodAny,
611 ZodArray: () => ZodArray,
612 ZodBigInt: () => ZodBigInt,
613 ZodBoolean: () => ZodBoolean,
614 ZodBranded: () => ZodBranded,
615 ZodCatch: () => ZodCatch,
616 ZodDate: () => ZodDate,
617 ZodDefault: () => ZodDefault,
618 ZodDiscriminatedUnion: () => ZodDiscriminatedUnion,
619 ZodEffects: () => ZodEffects,
620 ZodEnum: () => ZodEnum,
621 ZodError: () => ZodError,
622 ZodFirstPartyTypeKind: () => ZodFirstPartyTypeKind,
623 ZodFunction: () => ZodFunction,
624 ZodIntersection: () => ZodIntersection,
625 ZodIssueCode: () => ZodIssueCode,
626 ZodLazy: () => ZodLazy,
627 ZodLiteral: () => ZodLiteral,
628 ZodMap: () => ZodMap,
629 ZodNaN: () => ZodNaN,
630 ZodNativeEnum: () => ZodNativeEnum,
631 ZodNever: () => ZodNever,
632 ZodNull: () => ZodNull,
633 ZodNullable: () => ZodNullable,
634 ZodNumber: () => ZodNumber,
635 ZodObject: () => ZodObject,
636 ZodOptional: () => ZodOptional,
637 ZodParsedType: () => ZodParsedType,
638 ZodPipeline: () => ZodPipeline,
639 ZodPromise: () => ZodPromise,
640 ZodReadonly: () => ZodReadonly,
641 ZodRecord: () => ZodRecord,
642 ZodSchema: () => ZodType,
643 ZodSet: () => ZodSet,
644 ZodString: () => ZodString,
645 ZodSymbol: () => ZodSymbol,
646 ZodTransformer: () => ZodEffects,
647 ZodTuple: () => ZodTuple,
648 ZodType: () => ZodType,
649 ZodUndefined: () => ZodUndefined,
650 ZodUnion: () => ZodUnion,
651 ZodUnknown: () => ZodUnknown,
652 ZodVoid: () => ZodVoid,
653 addIssueToContext: () => addIssueToContext,
654 any: () => anyType,
655 array: () => arrayType,
656 bigint: () => bigIntType,
657 boolean: () => booleanType,
658 coerce: () => coerce,
659 custom: () => custom,
660 date: () => dateType,
661 datetimeRegex: () => datetimeRegex,
662 defaultErrorMap: () => en_default,
663 discriminatedUnion: () => discriminatedUnionType,
664 effect: () => effectsType,
665 enum: () => enumType,
666 function: () => functionType,
667 getErrorMap: () => getErrorMap,
668 getParsedType: () => getParsedType,
669 instanceof: () => instanceOfType,
670 intersection: () => intersectionType,
671 isAborted: () => isAborted,
672 isAsync: () => isAsync,
673 isDirty: () => isDirty,
674 isValid: () => isValid,
675 late: () => late,
676 lazy: () => lazyType,
677 literal: () => literalType,
678 makeIssue: () => makeIssue,
679 map: () => mapType,
680 nan: () => nanType,
681 nativeEnum: () => nativeEnumType,
682 never: () => neverType,
683 null: () => nullType,
684 nullable: () => nullableType,
685 number: () => numberType,
686 object: () => objectType,
687 objectUtil: () => objectUtil,
688 oboolean: () => oboolean,
689 onumber: () => onumber,
690 optional: () => optionalType,
691 ostring: () => ostring,
692 pipeline: () => pipelineType,
693 preprocess: () => preprocessType,
694 promise: () => promiseType,
695 quotelessJson: () => quotelessJson,
696 record: () => recordType,
697 set: () => setType,
698 setErrorMap: () => setErrorMap,
699 strictObject: () => strictObjectType,
700 string: () => stringType,
701 symbol: () => symbolType,
702 transformer: () => effectsType,
703 tuple: () => tupleType,
704 undefined: () => undefinedType,
705 union: () => unionType,
706 unknown: () => unknownType,
707 util: () => util,
708 void: () => voidType
709});
710
711// node_modules/zod/v3/helpers/util.js
712var util;
713(function(util2) {
714 util2.assertEqual = (_) => {
715 };
716 function assertIs(_arg) {
717 }
718 util2.assertIs = assertIs;
719 function assertNever(_x) {
720 throw new Error();
721 }
722 util2.assertNever = assertNever;
723 util2.arrayToEnum = (items) => {
724 const obj = {};
725 for (const item of items) {
726 obj[item] = item;
727 }
728 return obj;
729 };
730 util2.getValidEnumValues = (obj) => {
731 const validKeys = util2.objectKeys(obj).filter((k) => typeof obj[obj[k]] !== "number");
732 const filtered = {};
733 for (const k of validKeys) {
734 filtered[k] = obj[k];
735 }
736 return util2.objectValues(filtered);
737 };
738 util2.objectValues = (obj) => {
739 return util2.objectKeys(obj).map(function(e) {
740 return obj[e];
741 });
742 };
743 util2.objectKeys = typeof Object.keys === "function" ? (obj) => Object.keys(obj) : (object) => {
744 const keys = [];
745 for (const key in object) {
746 if (Object.prototype.hasOwnProperty.call(object, key)) {
747 keys.push(key);
748 }
749 }
750 return keys;
751 };
752 util2.find = (arr, checker) => {
753 for (const item of arr) {
754 if (checker(item))
755 return item;
756 }
757 return void 0;
758 };
759 util2.isInteger = typeof Number.isInteger === "function" ? (val) => Number.isInteger(val) : (val) => typeof val === "number" && Number.isFinite(val) && Math.floor(val) === val;
760 function joinValues(array, separator = " | ") {
761 return array.map((val) => typeof val === "string" ? `'${val}'` : val).join(separator);
762 }
763 util2.joinValues = joinValues;
764 util2.jsonStringifyReplacer = (_, value) => {
765 if (typeof value === "bigint") {
766 return value.toString();
767 }
768 return value;
769 };
770})(util || (util = {}));
771var objectUtil;
772(function(objectUtil2) {
773 objectUtil2.mergeShapes = (first, second) => {
774 return {
775 ...first,
776 ...second
777 // second overwrites first
778 };
779 };
780})(objectUtil || (objectUtil = {}));
781var ZodParsedType = util.arrayToEnum([
782 "string",
783 "nan",
784 "number",
785 "integer",
786 "float",
787 "boolean",
788 "date",
789 "bigint",
790 "symbol",
791 "function",
792 "undefined",
793 "null",
794 "array",
795 "object",
796 "unknown",
797 "promise",
798 "void",
799 "never",
800 "map",
801 "set"
802]);
803var getParsedType = (data) => {
804 const t = typeof data;
805 switch (t) {
806 case "undefined":
807 return ZodParsedType.undefined;
808 case "string":
809 return ZodParsedType.string;
810 case "number":
811 return Number.isNaN(data) ? ZodParsedType.nan : ZodParsedType.number;
812 case "boolean":
813 return ZodParsedType.boolean;
814 case "function":
815 return ZodParsedType.function;
816 case "bigint":
817 return ZodParsedType.bigint;
818 case "symbol":
819 return ZodParsedType.symbol;
820 case "object":
821 if (Array.isArray(data)) {
822 return ZodParsedType.array;
823 }
824 if (data === null) {
825 return ZodParsedType.null;
826 }
827 if (data.then && typeof data.then === "function" && data.catch && typeof data.catch === "function") {
828 return ZodParsedType.promise;
829 }
830 if (typeof Map !== "undefined" && data instanceof Map) {
831 return ZodParsedType.map;
832 }
833 if (typeof Set !== "undefined" && data instanceof Set) {
834 return ZodParsedType.set;
835 }
836 if (typeof Date !== "undefined" && data instanceof Date) {
837 return ZodParsedType.date;
838 }
839 return ZodParsedType.object;
840 default:
841 return ZodParsedType.unknown;
842 }
843};
844
845// node_modules/zod/v3/ZodError.js
846var ZodIssueCode = util.arrayToEnum([
847 "invalid_type",
848 "invalid_literal",
849 "custom",
850 "invalid_union",
851 "invalid_union_discriminator",
852 "invalid_enum_value",
853 "unrecognized_keys",
854 "invalid_arguments",
855 "invalid_return_type",
856 "invalid_date",
857 "invalid_string",
858 "too_small",
859 "too_big",
860 "invalid_intersection_types",
861 "not_multiple_of",
862 "not_finite"
863]);
864var quotelessJson = (obj) => {
865 const json = JSON.stringify(obj, null, 2);
866 return json.replace(/"([^"]+)":/g, "$1:");
867};
868var ZodError = class _ZodError extends Error {
869 get errors() {
870 return this.issues;
871 }
872 constructor(issues) {
873 super();
874 this.issues = [];
875 this.addIssue = (sub) => {
876 this.issues = [...this.issues, sub];
877 };
878 this.addIssues = (subs = []) => {
879 this.issues = [...this.issues, ...subs];
880 };
881 const actualProto = new.target.prototype;
882 if (Object.setPrototypeOf) {
883 Object.setPrototypeOf(this, actualProto);
884 } else {
885 this.__proto__ = actualProto;
886 }
887 this.name = "ZodError";
888 this.issues = issues;
889 }
890 format(_mapper) {
891 const mapper = _mapper || function(issue) {
892 return issue.message;
893 };
894 const fieldErrors = { _errors: [] };
895 const processError = (error) => {
896 for (const issue of error.issues) {
897 if (issue.code === "invalid_union") {
898 issue.unionErrors.map(processError);
899 } else if (issue.code === "invalid_return_type") {
900 processError(issue.returnTypeError);
901 } else if (issue.code === "invalid_arguments") {
902 processError(issue.argumentsError);
903 } else if (issue.path.length === 0) {
904 fieldErrors._errors.push(mapper(issue));
905 } else {
906 let curr = fieldErrors;
907 let i = 0;
908 while (i < issue.path.length) {
909 const el = issue.path[i];
910 const terminal = i === issue.path.length - 1;
911 if (!terminal) {
912 curr[el] = curr[el] || { _errors: [] };
913 } else {
914 curr[el] = curr[el] || { _errors: [] };
915 curr[el]._errors.push(mapper(issue));
916 }
917 curr = curr[el];
918 i++;
919 }
920 }
921 }
922 };
923 processError(this);
924 return fieldErrors;
925 }
926 static assert(value) {
927 if (!(value instanceof _ZodError)) {
928 throw new Error(`Not a ZodError: ${value}`);
929 }
930 }
931 toString() {
932 return this.message;
933 }
934 get message() {
935 return JSON.stringify(this.issues, util.jsonStringifyReplacer, 2);
936 }
937 get isEmpty() {
938 return this.issues.length === 0;
939 }
940 flatten(mapper = (issue) => issue.message) {
941 const fieldErrors = {};
942 const formErrors = [];
943 for (const sub of this.issues) {
944 if (sub.path.length > 0) {
945 const firstEl = sub.path[0];
946 fieldErrors[firstEl] = fieldErrors[firstEl] || [];
947 fieldErrors[firstEl].push(mapper(sub));
948 } else {
949 formErrors.push(mapper(sub));
950 }
951 }
952 return { formErrors, fieldErrors };
953 }
954 get formErrors() {
955 return this.flatten();
956 }
957};
958ZodError.create = (issues) => {
959 const error = new ZodError(issues);
960 return error;
961};
962
963// node_modules/zod/v3/locales/en.js
964var errorMap = (issue, _ctx) => {
965 let message;
966 switch (issue.code) {
967 case ZodIssueCode.invalid_type:
968 if (issue.received === ZodParsedType.undefined) {
969 message = "Required";
970 } else {
971 message = `Expected ${issue.expected}, received ${issue.received}`;
972 }
973 break;
974 case ZodIssueCode.invalid_literal:
975 message = `Invalid literal value, expected ${JSON.stringify(issue.expected, util.jsonStringifyReplacer)}`;
976 break;
977 case ZodIssueCode.unrecognized_keys:
978 message = `Unrecognized key(s) in object: ${util.joinValues(issue.keys, ", ")}`;
979 break;
980 case ZodIssueCode.invalid_union:
981 message = `Invalid input`;
982 break;
983 case ZodIssueCode.invalid_union_discriminator:
984 message = `Invalid discriminator value. Expected ${util.joinValues(issue.options)}`;
985 break;
986 case ZodIssueCode.invalid_enum_value:
987 message = `Invalid enum value. Expected ${util.joinValues(issue.options)}, received '${issue.received}'`;
988 break;
989 case ZodIssueCode.invalid_arguments:
990 message = `Invalid function arguments`;
991 break;
992 case ZodIssueCode.invalid_return_type:
993 message = `Invalid function return type`;
994 break;
995 case ZodIssueCode.invalid_date:
996 message = `Invalid date`;
997 break;
998 case ZodIssueCode.invalid_string:
999 if (typeof issue.validation === "object") {
1000 if ("includes" in issue.validation) {
1001 message = `Invalid input: must include "${issue.validation.includes}"`;
1002 if (typeof issue.validation.position === "number") {
1003 message = `${message} at one or more positions greater than or equal to ${issue.validation.position}`;
1004 }
1005 } else if ("startsWith" in issue.validation) {
1006 message = `Invalid input: must start with "${issue.validation.startsWith}"`;
1007 } else if ("endsWith" in issue.validation) {
1008 message = `Invalid input: must end with "${issue.validation.endsWith}"`;
1009 } else {
1010 util.assertNever(issue.validation);
1011 }
1012 } else if (issue.validation !== "regex") {
1013 message = `Invalid ${issue.validation}`;
1014 } else {
1015 message = "Invalid";
1016 }
1017 break;
1018 case ZodIssueCode.too_small:
1019 if (issue.type === "array")
1020 message = `Array must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `more than`} ${issue.minimum} element(s)`;
1021 else if (issue.type === "string")
1022 message = `String must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `over`} ${issue.minimum} character(s)`;
1023 else if (issue.type === "number")
1024 message = `Number must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${issue.minimum}`;
1025 else if (issue.type === "bigint")
1026 message = `Number must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${issue.minimum}`;
1027 else if (issue.type === "date")
1028 message = `Date must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${new Date(Number(issue.minimum))}`;
1029 else
1030 message = "Invalid input";
1031 break;
1032 case ZodIssueCode.too_big:
1033 if (issue.type === "array")
1034 message = `Array must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `less than`} ${issue.maximum} element(s)`;
1035 else if (issue.type === "string")
1036 message = `String must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `under`} ${issue.maximum} character(s)`;
1037 else if (issue.type === "number")
1038 message = `Number must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`;
1039 else if (issue.type === "bigint")
1040 message = `BigInt must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`;
1041 else if (issue.type === "date")
1042 message = `Date must be ${issue.exact ? `exactly` : issue.inclusive ? `smaller than or equal to` : `smaller than`} ${new Date(Number(issue.maximum))}`;
1043 else
1044 message = "Invalid input";
1045 break;
1046 case ZodIssueCode.custom:
1047 message = `Invalid input`;
1048 break;
1049 case ZodIssueCode.invalid_intersection_types:
1050 message = `Intersection results could not be merged`;
1051 break;
1052 case ZodIssueCode.not_multiple_of:
1053 message = `Number must be a multiple of ${issue.multipleOf}`;
1054 break;
1055 case ZodIssueCode.not_finite:
1056 message = "Number must be finite";
1057 break;
1058 default:
1059 message = _ctx.defaultError;
1060 util.assertNever(issue);
1061 }
1062 return { message };
1063};
1064var en_default = errorMap;
1065
1066// node_modules/zod/v3/errors.js
1067var overrideErrorMap = en_default;
1068function setErrorMap(map) {
1069 overrideErrorMap = map;
1070}
1071function getErrorMap() {
1072 return overrideErrorMap;
1073}
1074
1075// node_modules/zod/v3/helpers/parseUtil.js
1076var makeIssue = (params) => {
1077 const { data, path, errorMaps, issueData } = params;
1078 const fullPath = [...path, ...issueData.path || []];
1079 const fullIssue = {
1080 ...issueData,
1081 path: fullPath
1082 };
1083 if (issueData.message !== void 0) {
1084 return {
1085 ...issueData,
1086 path: fullPath,
1087 message: issueData.message
1088 };
1089 }
1090 let errorMessage = "";
1091 const maps = errorMaps.filter((m) => !!m).slice().reverse();
1092 for (const map of maps) {
1093 errorMessage = map(fullIssue, { data, defaultError: errorMessage }).message;
1094 }
1095 return {
1096 ...issueData,
1097 path: fullPath,
1098 message: errorMessage
1099 };
1100};
1101var EMPTY_PATH = [];
1102function addIssueToContext(ctx, issueData) {
1103 const overrideMap = getErrorMap();
1104 const issue = makeIssue({
1105 issueData,
1106 data: ctx.data,
1107 path: ctx.path,
1108 errorMaps: [
1109 ctx.common.contextualErrorMap,
1110 // contextual error map is first priority
1111 ctx.schemaErrorMap,
1112 // then schema-bound map if available
1113 overrideMap,
1114 // then global override map
1115 overrideMap === en_default ? void 0 : en_default
1116 // then global default map
1117 ].filter((x) => !!x)
1118 });
1119 ctx.common.issues.push(issue);
1120}
1121var ParseStatus = class _ParseStatus {
1122 constructor() {
1123 this.value = "valid";
1124 }
1125 dirty() {
1126 if (this.value === "valid")
1127 this.value = "dirty";
1128 }
1129 abort() {
1130 if (this.value !== "aborted")
1131 this.value = "aborted";
1132 }
1133 static mergeArray(status, results) {
1134 const arrayValue = [];
1135 for (const s of results) {
1136 if (s.status === "aborted")
1137 return INVALID;
1138 if (s.status === "dirty")
1139 status.dirty();
1140 arrayValue.push(s.value);
1141 }
1142 return { status: status.value, value: arrayValue };
1143 }
1144 static async mergeObjectAsync(status, pairs) {
1145 const syncPairs = [];
1146 for (const pair of pairs) {
1147 const key = await pair.key;
1148 const value = await pair.value;
1149 syncPairs.push({
1150 key,
1151 value
1152 });
1153 }
1154 return _ParseStatus.mergeObjectSync(status, syncPairs);
1155 }
1156 static mergeObjectSync(status, pairs) {
1157 const finalObject = {};
1158 for (const pair of pairs) {
1159 const { key, value } = pair;
1160 if (key.status === "aborted")
1161 return INVALID;
1162 if (value.status === "aborted")
1163 return INVALID;
1164 if (key.status === "dirty")
1165 status.dirty();
1166 if (value.status === "dirty")
1167 status.dirty();
1168 if (key.value !== "__proto__" && (typeof value.value !== "undefined" || pair.alwaysSet)) {
1169 finalObject[key.value] = value.value;
1170 }
1171 }
1172 return { status: status.value, value: finalObject };
1173 }
1174};
1175var INVALID = Object.freeze({
1176 status: "aborted"
1177});
1178var DIRTY = (value) => ({ status: "dirty", value });
1179var OK = (value) => ({ status: "valid", value });
1180var isAborted = (x) => x.status === "aborted";
1181var isDirty = (x) => x.status === "dirty";
1182var isValid = (x) => x.status === "valid";
1183var isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise;
1184
1185// node_modules/zod/v3/helpers/errorUtil.js
1186var errorUtil;
1187(function(errorUtil2) {
1188 errorUtil2.errToObj = (message) => typeof message === "string" ? { message } : message || {};
1189 errorUtil2.toString = (message) => typeof message === "string" ? message : message == null ? void 0 : message.message;
1190})(errorUtil || (errorUtil = {}));
1191
1192// node_modules/zod/v3/types.js
1193var ParseInputLazyPath = class {
1194 constructor(parent, value, path, key) {
1195 this._cachedPath = [];
1196 this.parent = parent;
1197 this.data = value;
1198 this._path = path;
1199 this._key = key;
1200 }hooks/git-parse.ts 41 lines1export interface GitFields {
2 repo: boolean;
3 root: string;
4 branch: string;
5 worktree: string;
6 added: number;
7 modified: number;
8 deleted: number;
9}
10
11const none: GitFields = {
12 repo: false,
13 root: '',
14 branch: '',
15 worktree: '',
16 added: 0,
17 modified: 0,
18 deleted: 0,
19};
20
21/**
22 * Parse the tab-separated output of the git snapshot shell command.
23 * @param stdout - The raw stdout from the git probe command.
24 * @returns The parsed git fields, or an all-empty set when not in a repository.
25 */
26export function parseGit(stdout: string): GitFields {
27 const t = (stdout || '').trim().split('\t');
28 if (t[0] !== 'YES') {
29 return { ...none };
30 }
31 return {
32 repo: true,
33 root: t[1] ?? '',
34 branch: t[2] ?? '',
35 worktree: t[3] ?? '',
36 added: Number(t[4]) || 0,
37 modified: Number(t[5]) || 0,
38 deleted: Number(t[6]) || 0,
39 };
40}
41hooks/paint.tsx 41 lines1import type { RenderModel } from './core.js';
2import { rendererBg, rendererColor } from './core.js';
3
4/**
5 * An element factory taking one props object (with `children` among the props).
6 */
7type ElementFactory = (props: Record<string, unknown>) => unknown;
8
9// No JSX: the element constructors from `$.ui.resolve(e)` take one props object
10// with `children` among the props (what `h(Box, props, ...children)` builds), so
11// they are called directly. This also works with stub factories in tests.
12/**
13 * Paint a render model into engine elements as a column of styled rows.
14 * @param model - The surface-agnostic render model to paint.
15 * @param el - The element factories to build the tree with.
16 * @param el.Box - The `Box` element factory.
17 * @param el.Text - The `Text` element factory.
18 * @returns The root element produced by the `Box` factory.
19 */
20export function paintModel(
21 model: RenderModel,
22 el: { Box: ElementFactory; Text: ElementFactory }
23): unknown {
24 const { Box, Text } = el;
25 const lines = model.lines.map((line, li) =>
26 Box({
27 key: `l${li}`,
28 flexDirection: 'row',
29 children: line.segments.map((seg, si) =>
30 Text({
31 key: `s${li}-${si}`,
32 color: rendererColor(seg.fg),
33 backgroundColor: rendererBg(seg.bg),
34 children: seg.text,
35 })
36 ),
37 })
38 );
39 return Box({ flexDirection: 'column', children: lines });
40}
41hooks/snapshot-build.ts 81 lines1import type { Snapshot } from './core.js';
2import type { GitFields } from './git-parse.js';
3
4export type RawUsage = {
5 ctxTokens: number;
6 ctxPct: number;
7 cached: number;
8 input: number;
9 output: number;
10 startedAt: number;
11 fivePct: number | null;
12 fiveReset: string | null;
13 weekPct: number | null;
14 weekReset: string | null;
15} | null;
16
17const emptyUsage: NonNullable<RawUsage> = {
18 ctxTokens: 0,
19 ctxPct: 0,
20 cached: 0,
21 input: 0,
22 output: 0,
23 startedAt: 0,
24 fivePct: null,
25 fiveReset: null,
26 weekPct: null,
27 weekReset: null,
28};
29
30/**
31 * Assemble a full `Snapshot` from the raw session, git, and usage fields.
32 * @param raw - The gathered raw inputs.
33 * @param raw.version - The Claude Code version string.
34 * @param raw.model - The active model identifier.
35 * @param raw.effort - The active reasoning effort, or `null` when unknown.
36 * @param raw.cwd - The current working directory.
37 * @param raw.now - The current clock time in milliseconds.
38 * @param raw.git - The parsed git fields for the working directory.
39 * @param raw.usage - The raw usage figures, or `null` when unavailable.
40 * @returns The complete snapshot with usage defaults filled in.
41 */
42export function buildSnapshot(raw: {
43 version: string;
44 model: string;
45 effort: string | null;
46 cwd: string;
47 now: number;
48 git: GitFields;
49 usage: RawUsage;
50}): Snapshot {
51 const u = raw.usage ?? emptyUsage;
52 return {
53 version: raw.version,
54 model: raw.model,
55 effort: raw.effort,
56 cwd: raw.cwd,
57 repo: raw.git.repo,
58 gitRoot: raw.git.root,
59 gitBranch: raw.git.branch,
60 gitWorktree: raw.git.worktree,
61 added: raw.git.added,
62 modified: raw.git.modified,
63 deleted: raw.git.deleted,
64 ctxTokens: u.ctxTokens,
65 ctxPct: u.ctxPct,
66 cached: u.cached,
67 input: u.input,
68 output: u.output,
69 total: u.cached + u.input + u.output,
70 startedAt: u.startedAt,
71 cost: null,
72 fivePct: u.fivePct,
73 fiveReset: u.fiveReset,
74 weekPct: u.weekPct,
75 weekReset: u.weekReset,
76 blockReset: null,
77 terminalWidth: 0,
78 now: raw.now,
79 };
80}
81hooks/toasts.ts 31 lines1import type { Snapshot, ToastRule } from './core.js';
2import { evalWhen } from './core.js';
3
4// Re-exported so existing importers (register.tsx, tests) keep their import path.
5export { evalWhen };
6
7/**
8 * Select the toast rules whose conditions are currently met and not suppressed.
9 * @param rules - The configured toast rules to evaluate.
10 * @param snapshot - The current snapshot the rules are evaluated against.
11 * @param fired - A map of rule keys that have already fired once.
12 * @returns The toasts that are due, each with its display text and dedupe key.
13 */
14export function dueToasts(
15 rules: ToastRule[],
16 snapshot: Snapshot,
17 fired: Record<string, boolean>
18): { text: string; key: string }[] {
19 const out: { text: string; key: string }[] = [];
20 for (const rule of rules) {
21 if (!evalWhen(rule.when, snapshot)) {
22 continue;
23 }
24 if (rule.once && fired[rule.when]) {
25 continue;
26 }
27 out.push({ text: rule.text, key: rule.when });
28 }
29 return out;
30}
31types/index.d.ts 46 lines1// The types contract must be self-contained (no imports), so the shapes the
2// plugin keeps in state are restated here; they mirror hooks/core.d.ts.
3
4export interface CcstatusSnapshot {
5 version: string;
6 model: string;
7 effort: string | null;
8 cwd: string;
9 repo: boolean;
10 gitRoot: string;
11 gitBranch: string;
12 gitWorktree: string;
13 added: number;
14 modified: number;
15 deleted: number;
16 ctxTokens: number;
17 ctxPct: number;
18 cached: number;
19 input: number;
20 output: number;
21 total: number;
22 startedAt: number;
23 cost: number | null;
24 fivePct: number | null;
25 fiveReset: string | null;
26 weekPct: number | null;
27 weekReset: string | null;
28 blockReset: string | null;
29 terminalWidth: number;
30 now: number;
31}
32
33declare module 'claude-code' {
34 interface PluginState {
35 ccstatus: {
36 visible: boolean;
37 snapshot: CcstatusSnapshot | null;
38 config: Record<string, unknown> | null;
39 effort: string | null;
40 paneOpen: boolean;
41 configMtime: number | null;
42 toastFired: Record<string, boolean>;
43 };
44 }
45}
46