-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.js
More file actions
139 lines (126 loc) · 7.32 KB
/
Copy pathindex.js
File metadata and controls
139 lines (126 loc) · 7.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
#!/usr/bin/env node
// ccvitals — operational HUD statusline for Claude Code (see version.js)
// Line 1: [alerts] Model [effort][fast] | Dir | Branch (uncommitted, sync) | Task · vX.Y.Z
// Line 2: Context bar | Cache (+TTL) | Rate limits | Cost | Session | Compact | Tools | Turns | Todo progress
// Env: STATUSLINE_DISABLE=1 skip; STATUSLINE_STDIN_TIMEOUT=ms; DEBUG_STATUSLINE=1 log errors
'use strict';
const path = require('path');
const os = require('os');
const { C, getErrors, safeSlice, logError, sweepOrphans } = require('./utils');
const { collectGit } = require('./git');
const { parseTranscript } = require('./transcript');
const {
buildContextBar, buildCostStr, buildRateLimitsStr,
buildAgentLines, buildEffortStr, buildLine1, buildLine2, computeSessionDur,
buildCacheStr, buildTodoStr,
} = require('./display');
const { readSettings, trackMonthlyCost, trackRateLimitSnapshot, readActiveTime, readCompactCount, writeBridgeFile, lookupTask } = require('./io');
const { checkClaudeUpdate, formatUpdateStr, isNewer } = require('./update');
const { readAccount, buildAccountStr } = require('./account');
if (process.env.STATUSLINE_DISABLE === '1') process.exit(0);
let input = '';
const STDIN_TIMEOUT = Math.max(500, Math.min(30000, Number(process.env.STATUSLINE_STDIN_TIMEOUT) || 5000));
const timeout = setTimeout(() => process.exit(0), STDIN_TIMEOUT);
process.stdin.setEncoding('utf8');
process.stdin.on('error', () => process.exit(0));
process.stdin.on('data', chunk => input += chunk);
process.stdin.on('end', () => {
clearTimeout(timeout);
try {
main(JSON.parse(input)).catch(e => {
try { process.stdout.write(`\x1b[31m! statusline crash: ${(e?.message || String(e)).substring(0, 80)}\x1b[0m\n`); } catch {}
});
} catch (e) {
try { process.stdout.write(`\x1b[31m! stdin parse: ${(e?.message || String(e)).substring(0, 80)}\x1b[0m\n`); } catch {}
}
});
async function main(data) {
const tickStart = Date.now();
const timings = {};
const model = data.model?.display_name || data.model?.id || '?';
const cwd = data.cwd || process.cwd();
const sessionId = data.session_id || '';
const transcriptPath = data.transcript_path || '';
const dir = data.worktree?.name || cwd;
// Directories brought in by /add-dir or --add-dir. Only cwd shows in the dir chip,
// so without this the extra workspace roots are invisible on the line.
const addedDirs = data.workspace?.added_dirs;
const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
const settings = readSettings();
// Priority: transcript /effort (typed, picker, or inherited across /clear) > stdin payload > persisted settings.
// CC 2.1.112+ stopped shipping effortLevel via stdin; transcript is authoritative for session-only modes.
// settings.effortLevel is the persisted DEFAULT — it can legitimately diverge from the live session effort.
let effort = data.effortLevel || settings.effort;
const { thresholds } = settings;
// Prefer live signal from CC stdin; fall back to settings.json for backwards compat.
const fastMode = (data.fastMode ?? data.model?.fast ?? settings.fastMode) === true;
const t0 = Date.now();
const updateResult = await checkClaudeUpdate(path.join(claudeDir, 'cache', 'claude-update-check.json'));
timings.claudeUpdate = Date.now() - t0;
const t1 = Date.now();
const { branch, gitStatus } = await collectGit(cwd, thresholds.push);
timings.git = Date.now() - t1;
const t2 = Date.now();
const t = parseTranscript(transcriptPath, sessionId, claudeDir);
timings.transcript = Date.now() - t2;
if (t.lastEffort) effort = t.lastEffort;
sweepOrphans(claudeDir, settings.aggWindowDays);
const activeMs = readActiveTime(transcriptPath, sessionId, claudeDir);
const sessionDur = computeSessionDur(t.firstTimestamp, transcriptPath, activeMs);
const { ctx, pct, pctEstimated, remainingPct } = buildContextBar(data, t.lastUsage, {
lastCompactBoundaryTs: t.lastCompactBoundaryTs,
lastApiTimestamp: t.lastApiTimestamp,
compactSummaryTokens: t.compactSummaryTokens,
sessionBaselineTokens: t.sessionBaselineTokens,
});
writeBridgeFile(sessionId, pct, pctEstimated, remainingPct);
const cost = data.cost?.total_cost_usd;
const cost30d = trackMonthlyCost(cost, sessionId, claudeDir, settings.aggWindowDays);
const costStr = buildCostStr(cost, cost30d, thresholds);
const rlAgg = trackRateLimitSnapshot(data.rate_limits, sessionId, claudeDir);
const rlStr = buildRateLimitsStr(data.rate_limits, thresholds, t.lastApiTimestamp, rlAgg);
const taskStr = lookupTask(sessionId, claudeDir);
const effortStr = buildEffortStr(effort);
const fastStr = fastMode ? `${C.yellow}\u26A1${C.reset}` : '';
const accountStr = buildAccountStr(readAccount());
// CC version is captured per session from transcript `version` field — refreshes naturally on new sessions.
// Green = up-to-date, bright red = update available, soft gray = undetermined (first run / network err).
// "Outdated" compares npm latest against the RUNNING session's version — a replaced
// binary on disk doesn't help until this session restarts. Falls back to the
// PATH-CLI comparison (worker's `claude --version`) when the session doesn't report one.
const ccVer = data.version || t.ccVersion;
const outdated = updateResult.latest && ccVer
? isNewer(updateResult.latest, ccVer)
: updateResult.update_available;
const updateStr = formatUpdateStr(updateResult, outdated);
let ccVerStr = '';
if (ccVer) {
const undetermined = !outdated && updateResult.error && updateResult.error !== 'no-cli';
const color = outdated ? '\x1b[91m' : undetermined ? C.soft : C.green;
ccVerStr = ` ${color}(${ccVer})${C.reset}`;
}
const toolStr = t.toolCallCount > 0 ? ` ${C.gray}| ${t.toolCallCount} tools${C.reset}` : '';
const turnStr = t.turnCount > 0 ? ` ${C.gray}| ${t.turnCount} turns${C.reset}` : '';
// Prefer MAX(JSONL-derived, PreCompact-hook count) — protects the counter
// against schema drift on either side; whichever observes more wins.
const compactCount = Math.max(t.compactCount, readCompactCount(transcriptPath, sessionId, claudeDir));
const compactStr = compactCount > 0 ? ` ${C.gray}| ${C.orange}${compactCount}x compact${C.reset}` : '';
const toolUsedStr = t.lastToolUsed ? ` ${C.gray}| ${C.soft}${t.lastToolUsed}${C.reset}` : '';
const cacheStr = buildCacheStr(t.lastUsage, t.lastApiTimestamp);
const todoStr = buildTodoStr(t.lastTodos);
const errs = getErrors();
const errStr = errs.length
? ` ${C.red}!${errs.length > 2 ? errs.length + ':' : ''}${errs.slice(-2).join(',')}${C.reset}` : '';
process.stdout.write(buildLine1({ updateStr, errStr, model, effortStr, ccVerStr, fastStr, accountStr, dir, cwd, addedDirs, branch, gitStatus, taskStr }) + '\n');
process.stdout.write(buildLine2({ ctx, rlStr, costStr, sessionDur, cacheStr, compactStr, toolStr, turnStr, toolUsedStr, todoStr }) + '\n');
const agentLines = buildAgentLines(t.agentMap, effort, model);
if (agentLines) process.stdout.write(agentLines + '\n');
if (t.lastUserMessage) {
const display = t.lastUserMessage.length > 100 ? safeSlice(t.lastUserMessage, 97) + '...' : t.lastUserMessage;
process.stdout.write(`\ud83d\udcac ${display}\n`);
}
const elapsed = Date.now() - tickStart;
if (elapsed > 500) {
logError('slow', new Error(`${elapsed}ms (${Object.entries(timings).map(([k, v]) => `${k}=${v}ms`).join(',')})`));
}
}