@sooneocean/claude-hud 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +20 -0
- package/.claude-plugin/plugin.json +20 -0
- package/LICENSE +21 -0
- package/README.md +379 -0
- package/commands/configure.md +361 -0
- package/commands/export.md +43 -0
- package/commands/health.md +61 -0
- package/commands/setup.md +287 -0
- package/commands/theme.md +31 -0
- package/dist/alert.d.ts +31 -0
- package/dist/alert.d.ts.map +1 -0
- package/dist/alert.js +53 -0
- package/dist/alert.js.map +1 -0
- package/dist/burn-rate.d.ts +4 -0
- package/dist/burn-rate.d.ts.map +1 -0
- package/dist/burn-rate.js +36 -0
- package/dist/burn-rate.js.map +1 -0
- package/dist/cache.d.ts +6 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +47 -0
- package/dist/cache.js.map +1 -0
- package/dist/claude-config-dir.d.ts +4 -0
- package/dist/claude-config-dir.d.ts.map +1 -0
- package/dist/claude-config-dir.js +24 -0
- package/dist/claude-config-dir.js.map +1 -0
- package/dist/config-io.d.ts +6 -0
- package/dist/config-io.d.ts.map +1 -0
- package/dist/config-io.js +27 -0
- package/dist/config-io.js.map +1 -0
- package/dist/config-reader.d.ts +8 -0
- package/dist/config-reader.d.ts.map +1 -0
- package/dist/config-reader.js +204 -0
- package/dist/config-reader.js.map +1 -0
- package/dist/config.d.ts +94 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +358 -0
- package/dist/config.js.map +1 -0
- package/dist/constants.d.ts +11 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +11 -0
- package/dist/constants.js.map +1 -0
- package/dist/cost-tracker.d.ts +9 -0
- package/dist/cost-tracker.d.ts.map +1 -0
- package/dist/cost-tracker.js +46 -0
- package/dist/cost-tracker.js.map +1 -0
- package/dist/debug.d.ts +6 -0
- package/dist/debug.d.ts.map +1 -0
- package/dist/debug.js +15 -0
- package/dist/debug.js.map +1 -0
- package/dist/extra-cmd.d.ts +20 -0
- package/dist/extra-cmd.d.ts.map +1 -0
- package/dist/extra-cmd.js +112 -0
- package/dist/extra-cmd.js.map +1 -0
- package/dist/git.d.ts +16 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +94 -0
- package/dist/git.js.map +1 -0
- package/dist/health-check.d.ts +12 -0
- package/dist/health-check.d.ts.map +1 -0
- package/dist/health-check.js +37 -0
- package/dist/health-check.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +198 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/agent-teams-provider.d.ts +10 -0
- package/dist/providers/agent-teams-provider.d.ts.map +1 -0
- package/dist/providers/agent-teams-provider.js +57 -0
- package/dist/providers/agent-teams-provider.js.map +1 -0
- package/dist/providers/agw-provider.d.ts +10 -0
- package/dist/providers/agw-provider.d.ts.map +1 -0
- package/dist/providers/agw-provider.js +49 -0
- package/dist/providers/agw-provider.js.map +1 -0
- package/dist/providers/index.d.ts +14 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +25 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/render/agents-line.d.ts +3 -0
- package/dist/render/agents-line.d.ts.map +1 -0
- package/dist/render/agents-line.js +40 -0
- package/dist/render/agents-line.js.map +1 -0
- package/dist/render/alert-line.d.ts +3 -0
- package/dist/render/alert-line.d.ts.map +1 -0
- package/dist/render/alert-line.js +11 -0
- package/dist/render/alert-line.js.map +1 -0
- package/dist/render/colors.d.ts +39 -0
- package/dist/render/colors.d.ts.map +1 -0
- package/dist/render/colors.js +109 -0
- package/dist/render/colors.js.map +1 -0
- package/dist/render/framework-line.d.ts +3 -0
- package/dist/render/framework-line.d.ts.map +1 -0
- package/dist/render/framework-line.js +32 -0
- package/dist/render/framework-line.js.map +1 -0
- package/dist/render/index.d.ts +3 -0
- package/dist/render/index.d.ts.map +1 -0
- package/dist/render/index.js +435 -0
- package/dist/render/index.js.map +1 -0
- package/dist/render/lines/environment.d.ts +3 -0
- package/dist/render/lines/environment.d.ts.map +1 -0
- package/dist/render/lines/environment.js +30 -0
- package/dist/render/lines/environment.js.map +1 -0
- package/dist/render/lines/identity.d.ts +3 -0
- package/dist/render/lines/identity.d.ts.map +1 -0
- package/dist/render/lines/identity.js +93 -0
- package/dist/render/lines/identity.js.map +1 -0
- package/dist/render/lines/index.d.ts +5 -0
- package/dist/render/lines/index.d.ts.map +1 -0
- package/dist/render/lines/index.js +5 -0
- package/dist/render/lines/index.js.map +1 -0
- package/dist/render/lines/project.d.ts +3 -0
- package/dist/render/lines/project.d.ts.map +1 -0
- package/dist/render/lines/project.js +100 -0
- package/dist/render/lines/project.js.map +1 -0
- package/dist/render/lines/usage.d.ts +3 -0
- package/dist/render/lines/usage.d.ts.map +1 -0
- package/dist/render/lines/usage.js +65 -0
- package/dist/render/lines/usage.js.map +1 -0
- package/dist/render/session-line.d.ts +7 -0
- package/dist/render/session-line.d.ts.map +1 -0
- package/dist/render/session-line.js +227 -0
- package/dist/render/session-line.js.map +1 -0
- package/dist/render/todos-line.d.ts +3 -0
- package/dist/render/todos-line.d.ts.map +1 -0
- package/dist/render/todos-line.js +29 -0
- package/dist/render/todos-line.js.map +1 -0
- package/dist/render/tools-line.d.ts +3 -0
- package/dist/render/tools-line.d.ts.map +1 -0
- package/dist/render/tools-line.js +45 -0
- package/dist/render/tools-line.js.map +1 -0
- package/dist/session-history.d.ts +15 -0
- package/dist/session-history.d.ts.map +1 -0
- package/dist/session-history.js +46 -0
- package/dist/session-history.js.map +1 -0
- package/dist/session-stats.d.ts +11 -0
- package/dist/session-stats.d.ts.map +1 -0
- package/dist/session-stats.js +48 -0
- package/dist/session-stats.js.map +1 -0
- package/dist/speed-tracker.d.ts +7 -0
- package/dist/speed-tracker.d.ts.map +1 -0
- package/dist/speed-tracker.js +34 -0
- package/dist/speed-tracker.js.map +1 -0
- package/dist/stdin.d.ts +9 -0
- package/dist/stdin.d.ts.map +1 -0
- package/dist/stdin.js +142 -0
- package/dist/stdin.js.map +1 -0
- package/dist/themes.d.ts +10 -0
- package/dist/themes.d.ts.map +1 -0
- package/dist/themes.js +81 -0
- package/dist/themes.js.map +1 -0
- package/dist/transcript.d.ts +3 -0
- package/dist/transcript.d.ts.map +1 -0
- package/dist/transcript.js +221 -0
- package/dist/transcript.js.map +1 -0
- package/dist/types.d.ts +124 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/usage-api.d.ts +62 -0
- package/dist/usage-api.d.ts.map +1 -0
- package/dist/usage-api.js +908 -0
- package/dist/usage-api.js.map +1 -0
- package/dist/utils/format.d.ts +9 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +75 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/terminal.d.ts +5 -0
- package/dist/utils/terminal.d.ts.map +1 -0
- package/dist/utils/terminal.js +42 -0
- package/dist/utils/terminal.js.map +1 -0
- package/package.json +36 -0
- package/src/alert.ts +75 -0
- package/src/burn-rate.ts +45 -0
- package/src/cache.ts +57 -0
- package/src/claude-config-dir.ts +27 -0
- package/src/config-io.ts +26 -0
- package/src/config-reader.ts +236 -0
- package/src/config.ts +496 -0
- package/src/constants.ts +10 -0
- package/src/cost-tracker.ts +53 -0
- package/src/debug.ts +16 -0
- package/src/extra-cmd.ts +125 -0
- package/src/git.ts +126 -0
- package/src/health-check.ts +50 -0
- package/src/index.ts +234 -0
- package/src/providers/agent-teams-provider.ts +56 -0
- package/src/providers/agw-provider.ts +47 -0
- package/src/providers/index.ts +27 -0
- package/src/render/agents-line.ts +51 -0
- package/src/render/alert-line.ts +11 -0
- package/src/render/colors.ts +145 -0
- package/src/render/framework-line.ts +34 -0
- package/src/render/index.ts +512 -0
- package/src/render/lines/environment.ts +41 -0
- package/src/render/lines/identity.ts +109 -0
- package/src/render/lines/index.ts +4 -0
- package/src/render/lines/project.ts +113 -0
- package/src/render/lines/usage.ts +79 -0
- package/src/render/session-line.ts +253 -0
- package/src/render/todos-line.ts +35 -0
- package/src/render/tools-line.ts +58 -0
- package/src/session-history.ts +62 -0
- package/src/session-stats.ts +65 -0
- package/src/speed-tracker.ts +51 -0
- package/src/stdin.ts +169 -0
- package/src/themes.ts +90 -0
- package/src/transcript.ts +268 -0
- package/src/types.ts +146 -0
- package/src/usage-api.ts +1090 -0
- package/src/utils/format.ts +79 -0
- package/src/utils/terminal.ts +46 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { RenderContext } from '../../types.js';
|
|
2
|
+
import { dim } from '../colors.js';
|
|
3
|
+
|
|
4
|
+
export function renderEnvironmentLine(ctx: RenderContext): string | null {
|
|
5
|
+
const display = ctx.config?.display;
|
|
6
|
+
|
|
7
|
+
if (display?.showConfigCounts === false) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const totalCounts = ctx.claudeMdCount + ctx.rulesCount + ctx.mcpCount + ctx.hooksCount;
|
|
12
|
+
const threshold = display?.environmentThreshold ?? 0;
|
|
13
|
+
|
|
14
|
+
if (totalCounts === 0 || totalCounts < threshold) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const parts: string[] = [];
|
|
19
|
+
|
|
20
|
+
if (ctx.claudeMdCount > 0) {
|
|
21
|
+
parts.push(`${ctx.claudeMdCount} CLAUDE.md`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (ctx.rulesCount > 0) {
|
|
25
|
+
parts.push(`${ctx.rulesCount} rules`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (ctx.mcpCount > 0) {
|
|
29
|
+
parts.push(`${ctx.mcpCount} MCPs`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (ctx.hooksCount > 0) {
|
|
33
|
+
parts.push(`${ctx.hooksCount} hooks`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (parts.length === 0) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return dim(parts.join(' | '));
|
|
41
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { RenderContext } from '../../types.js';
|
|
2
|
+
import { getContextPercent, getBufferedPercent } from '../../stdin.js';
|
|
3
|
+
import { coloredBar, dim, getContextColor, RESET, red, warning } from '../colors.js';
|
|
4
|
+
import { getAdaptiveBarWidth, isNarrowTerminal, isVeryNarrowTerminal } from '../../utils/terminal.js';
|
|
5
|
+
import { formatCost } from '../../cost-tracker.js';
|
|
6
|
+
import { formatTokens, formatContextValue } from '../../utils/format.js';
|
|
7
|
+
|
|
8
|
+
const DEBUG = process.env.DEBUG?.includes('claude-hud') || process.env.DEBUG === '*';
|
|
9
|
+
|
|
10
|
+
const SPARK_CHARS = '▁▂▃▄▅▆▇█';
|
|
11
|
+
|
|
12
|
+
function renderSparkline(values: number[]): string {
|
|
13
|
+
if (values.length < 2) return '';
|
|
14
|
+
const min = Math.min(...values);
|
|
15
|
+
const max = Math.max(...values);
|
|
16
|
+
const range = max - min || 1;
|
|
17
|
+
return values.map(v => {
|
|
18
|
+
const idx = Math.round(((v - min) / range) * (SPARK_CHARS.length - 1));
|
|
19
|
+
return SPARK_CHARS[idx];
|
|
20
|
+
}).join('');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function ordinal(n: number): string {
|
|
24
|
+
if (n === 1) return '1st';
|
|
25
|
+
if (n === 2) return '2nd';
|
|
26
|
+
if (n === 3) return '3rd';
|
|
27
|
+
return `${n}th`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function renderIdentityLine(ctx: RenderContext): string {
|
|
31
|
+
const rawPercent = getContextPercent(ctx.stdin);
|
|
32
|
+
const bufferedPercent = getBufferedPercent(ctx.stdin);
|
|
33
|
+
const autocompactMode = ctx.config?.display?.autocompactBuffer ?? 'enabled';
|
|
34
|
+
const percent = autocompactMode === 'disabled' ? rawPercent : bufferedPercent;
|
|
35
|
+
const colors = ctx.config?.colors;
|
|
36
|
+
|
|
37
|
+
if (DEBUG && autocompactMode === 'disabled') {
|
|
38
|
+
console.error(`[claude-hud:context] autocompactBuffer=disabled, showing raw ${rawPercent}% (buffered would be ${bufferedPercent}%)`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const display = ctx.config?.display;
|
|
42
|
+
const contextValueMode = display?.contextValue ?? 'percent';
|
|
43
|
+
const contextValue = formatContextValue(ctx, percent, contextValueMode);
|
|
44
|
+
|
|
45
|
+
const alertThresholds = ctx.config.alerts?.context
|
|
46
|
+
? { warningThreshold: ctx.config.alerts.context.warningThreshold, criticalThreshold: ctx.config.alerts.context.criticalThreshold }
|
|
47
|
+
: undefined;
|
|
48
|
+
|
|
49
|
+
const contextValueDisplay = `${getContextColor(percent, colors, alertThresholds)}${contextValue}${RESET}`;
|
|
50
|
+
|
|
51
|
+
const barStyle = display?.barStyle ?? 'classic';
|
|
52
|
+
let line = display?.showContextBar !== false
|
|
53
|
+
? `${dim('Context')} ${coloredBar(percent, getAdaptiveBarWidth(), colors, barStyle, alertThresholds)} ${contextValueDisplay}`
|
|
54
|
+
: `${dim('Context')} ${contextValueDisplay}`;
|
|
55
|
+
|
|
56
|
+
// Progressive content reduction based on terminal width
|
|
57
|
+
const tw = ctx.terminalWidth;
|
|
58
|
+
const isNarrow = isNarrowTerminal(tw);
|
|
59
|
+
const isVeryNarrow = isVeryNarrowTerminal(tw);
|
|
60
|
+
|
|
61
|
+
if (!isVeryNarrow && display?.showTokenBreakdown !== false && percent >= 85) {
|
|
62
|
+
const usage = ctx.stdin.context_window?.current_usage;
|
|
63
|
+
if (usage) {
|
|
64
|
+
const input = formatTokens(usage.input_tokens ?? 0);
|
|
65
|
+
const cache = formatTokens((usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0));
|
|
66
|
+
line += dim(` (in: ${input}, cache: ${cache})`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (ctx.costEstimate && ctx.config.display.showCost) {
|
|
71
|
+
if (!isNarrow) {
|
|
72
|
+
line += ` ${dim('│')} ${dim(formatCost(ctx.costEstimate.sessionCost))}`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!isNarrow && ctx.burnRate && ctx.config.display.showBurnRate) {
|
|
77
|
+
const formatted = ctx.burnRate.tokensPerMinute >= 1000
|
|
78
|
+
? `${(ctx.burnRate.tokensPerMinute / 1000).toFixed(1)}k`
|
|
79
|
+
: `${ctx.burnRate.tokensPerMinute}`;
|
|
80
|
+
line += ` ${dim('│')} ${dim(`${formatted} tok/m`)}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!isNarrow && ctx.burnRate && ctx.burnRate.tokensPerMinute > 0 && percent >= (alertThresholds?.warningThreshold ?? 70)) {
|
|
84
|
+
const remaining = ctx.stdin.context_window?.context_window_size
|
|
85
|
+
? ctx.stdin.context_window.context_window_size - (ctx.stdin.context_window?.current_usage?.input_tokens ?? 0)
|
|
86
|
+
: 0;
|
|
87
|
+
if (remaining > 0) {
|
|
88
|
+
const minsLeft = Math.round(remaining / ctx.burnRate.tokensPerMinute);
|
|
89
|
+
const timeStr = minsLeft >= 60 ? `${Math.floor(minsLeft / 60)}h ${minsLeft % 60}m` : `${minsLeft}m`;
|
|
90
|
+
line += dim(` ~${timeStr}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!isVeryNarrow && ctx.sessionStats.autocompactCount > 0) {
|
|
95
|
+
line += dim(` (${ordinal(ctx.sessionStats.autocompactCount)} compact)`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!isNarrow && ctx.sparkline && ctx.sparkline.length >= 3) {
|
|
99
|
+
line += ` ${dim(renderSparkline(ctx.sparkline))}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!isNarrow && ctx.apiLatency !== null && ctx.apiLatency !== undefined && ctx.apiLatency > 0) {
|
|
103
|
+
const latencyColor = ctx.apiLatency > 5000 ? red : ctx.apiLatency > 2000 ? warning : dim;
|
|
104
|
+
line += ` ${latencyColor(`${ctx.apiLatency}ms`)}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return line;
|
|
108
|
+
}
|
|
109
|
+
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { RenderContext } from '../../types.js';
|
|
2
|
+
import { getModelName, getProviderLabel } from '../../stdin.js';
|
|
3
|
+
import { getOutputSpeed } from '../../speed-tracker.js';
|
|
4
|
+
import { cyan, dim, magenta, yellow, red, claudeOrange, colorize, green } from '../colors.js';
|
|
5
|
+
import { isNarrowTerminal } from '../../utils/terminal.js';
|
|
6
|
+
|
|
7
|
+
export function renderProjectLine(ctx: RenderContext): string | null {
|
|
8
|
+
const display = ctx.config?.display;
|
|
9
|
+
const parts: string[] = [];
|
|
10
|
+
|
|
11
|
+
if (display?.activityIndicator) {
|
|
12
|
+
const hasRunning = ctx.transcript.tools.some(t => t.status === 'running');
|
|
13
|
+
const indicator = hasRunning ? colorize('◉', '\x1b[31m') : green('◉');
|
|
14
|
+
parts.unshift(indicator);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (display?.showModel !== false) {
|
|
18
|
+
const model = getModelName(ctx.stdin);
|
|
19
|
+
const providerLabel = getProviderLabel(ctx.stdin);
|
|
20
|
+
const showUsage = display?.showUsage !== false;
|
|
21
|
+
const planName = showUsage ? ctx.usageData?.planName : undefined;
|
|
22
|
+
const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
|
|
23
|
+
const billingLabel = showUsage ? (planName ?? (hasApiKey ? red('API') : undefined)) : undefined;
|
|
24
|
+
const planDisplay = providerLabel ?? billingLabel;
|
|
25
|
+
const modelDisplay = planDisplay ? `${model} | ${planDisplay}` : model;
|
|
26
|
+
parts.push(cyan(`[${modelDisplay}]`));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let projectPart: string | null = null;
|
|
30
|
+
if (display?.showProject !== false && ctx.stdin.cwd) {
|
|
31
|
+
const segments = ctx.stdin.cwd.split(/[/\\]/).filter(Boolean);
|
|
32
|
+
const pathLevels = ctx.config?.pathLevels ?? 1;
|
|
33
|
+
const projectPath = segments.length > 0 ? segments.slice(-pathLevels).join('/') : '/';
|
|
34
|
+
projectPart = yellow(projectPath);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let gitPart = '';
|
|
38
|
+
const gitConfig = ctx.config?.gitStatus;
|
|
39
|
+
const showGit = gitConfig?.enabled ?? true;
|
|
40
|
+
|
|
41
|
+
if (showGit && ctx.gitStatus) {
|
|
42
|
+
const gitParts: string[] = [ctx.gitStatus.branch];
|
|
43
|
+
|
|
44
|
+
if ((gitConfig?.showDirty ?? true) && ctx.gitStatus.isDirty) {
|
|
45
|
+
gitParts.push('*');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (gitConfig?.showAheadBehind) {
|
|
49
|
+
if (ctx.gitStatus.ahead > 0) {
|
|
50
|
+
gitParts.push(` ↑${ctx.gitStatus.ahead}`);
|
|
51
|
+
}
|
|
52
|
+
if (ctx.gitStatus.behind > 0) {
|
|
53
|
+
gitParts.push(` ↓${ctx.gitStatus.behind}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (gitConfig?.showFileStats && ctx.gitStatus.fileStats) {
|
|
58
|
+
const { modified, added, deleted, untracked } = ctx.gitStatus.fileStats;
|
|
59
|
+
const statParts: string[] = [];
|
|
60
|
+
if (modified > 0) statParts.push(`!${modified}`);
|
|
61
|
+
if (added > 0) statParts.push(`+${added}`);
|
|
62
|
+
if (deleted > 0) statParts.push(`✘${deleted}`);
|
|
63
|
+
if (untracked > 0) statParts.push(`?${untracked}`);
|
|
64
|
+
if (statParts.length > 0) {
|
|
65
|
+
gitParts.push(` ${statParts.join(' ')}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
gitPart = `${magenta('git:(')}${cyan(gitParts.join(''))}${magenta(')')}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (projectPart && gitPart) {
|
|
73
|
+
parts.push(`${projectPart} ${gitPart}`);
|
|
74
|
+
} else if (projectPart) {
|
|
75
|
+
parts.push(projectPart);
|
|
76
|
+
} else if (gitPart) {
|
|
77
|
+
parts.push(gitPart);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Progressive content reduction based on terminal width
|
|
81
|
+
const tw = ctx.terminalWidth;
|
|
82
|
+
const isNarrow = isNarrowTerminal(tw);
|
|
83
|
+
|
|
84
|
+
if (!isNarrow && display?.showSessionName && ctx.transcript.sessionName) {
|
|
85
|
+
parts.push(dim(ctx.transcript.sessionName));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!isNarrow && ctx.extraLabel) {
|
|
89
|
+
parts.push(dim(ctx.extraLabel));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (display?.showSpeed) {
|
|
93
|
+
const speed = getOutputSpeed(ctx.stdin);
|
|
94
|
+
if (speed !== null) {
|
|
95
|
+
parts.push(dim(`out: ${speed.toFixed(1)} tok/s`));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!isNarrow && display?.showDuration !== false && ctx.sessionDuration) {
|
|
100
|
+
parts.push(dim(`⏱️ ${ctx.sessionDuration}`));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const customLine = display?.customLine;
|
|
104
|
+
if (customLine) {
|
|
105
|
+
parts.push(claudeOrange(customLine));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (parts.length === 0) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return parts.join(' \u2502 ');
|
|
113
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { RenderContext } from '../../types.js';
|
|
2
|
+
import { isLimitReached } from '../../types.js';
|
|
3
|
+
import { getProviderLabel } from '../../stdin.js';
|
|
4
|
+
import { critical, warning, dim, quotaBar } from '../colors.js';
|
|
5
|
+
import { getAdaptiveBarWidth } from '../../utils/terminal.js';
|
|
6
|
+
import { formatResetTime, formatUsageError, formatUsagePercent } from '../../utils/format.js';
|
|
7
|
+
|
|
8
|
+
export function renderUsageLine(ctx: RenderContext): string | null {
|
|
9
|
+
const display = ctx.config?.display;
|
|
10
|
+
const colors = ctx.config?.colors;
|
|
11
|
+
|
|
12
|
+
if (display?.showUsage === false) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!ctx.usageData?.planName) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (getProviderLabel(ctx.stdin)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const label = dim('Usage');
|
|
25
|
+
|
|
26
|
+
if (ctx.usageData.apiUnavailable) {
|
|
27
|
+
const errorHint = formatUsageError(ctx.usageData.apiError);
|
|
28
|
+
return `${label} ${warning(`⚠${errorHint}`, colors)}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (isLimitReached(ctx.usageData)) {
|
|
32
|
+
const resetTime = ctx.usageData.fiveHour === 100
|
|
33
|
+
? formatResetTime(ctx.usageData.fiveHourResetAt)
|
|
34
|
+
: formatResetTime(ctx.usageData.sevenDayResetAt);
|
|
35
|
+
return `${label} ${critical(`⚠ Limit reached${resetTime ? ` (resets ${resetTime})` : ''}`, colors)}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const threshold = display?.usageThreshold ?? 0;
|
|
39
|
+
const fiveHour = ctx.usageData.fiveHour;
|
|
40
|
+
const sevenDay = ctx.usageData.sevenDay;
|
|
41
|
+
|
|
42
|
+
const effectiveUsage = Math.max(fiveHour ?? 0, sevenDay ?? 0);
|
|
43
|
+
if (effectiveUsage < threshold) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const fiveHourDisplay = formatUsagePercent(ctx.usageData.fiveHour, colors);
|
|
48
|
+
const fiveHourReset = formatResetTime(ctx.usageData.fiveHourResetAt);
|
|
49
|
+
|
|
50
|
+
const usageBarEnabled = display?.usageBarEnabled ?? true;
|
|
51
|
+
const barStyle = display?.barStyle ?? 'classic';
|
|
52
|
+
const fiveHourPart = usageBarEnabled
|
|
53
|
+
? (fiveHourReset
|
|
54
|
+
? `${quotaBar(fiveHour ?? 0, getAdaptiveBarWidth(), colors, barStyle)} ${fiveHourDisplay} (resets in ${fiveHourReset})`
|
|
55
|
+
: `${quotaBar(fiveHour ?? 0, getAdaptiveBarWidth(), colors, barStyle)} ${fiveHourDisplay}`)
|
|
56
|
+
: (fiveHourReset
|
|
57
|
+
? `5h: ${fiveHourDisplay} (resets in ${fiveHourReset})`
|
|
58
|
+
: `5h: ${fiveHourDisplay}`);
|
|
59
|
+
|
|
60
|
+
const sevenDayThreshold = display?.sevenDayThreshold ?? 80;
|
|
61
|
+
const syncingSuffix = ctx.usageData.apiError === 'rate-limited'
|
|
62
|
+
? ` ${dim('(syncing...)')}`
|
|
63
|
+
: '';
|
|
64
|
+
if (sevenDay !== null && sevenDay >= sevenDayThreshold) {
|
|
65
|
+
const sevenDayDisplay = formatUsagePercent(sevenDay, colors);
|
|
66
|
+
const sevenDayReset = formatResetTime(ctx.usageData.sevenDayResetAt);
|
|
67
|
+
const sevenDayPart = usageBarEnabled
|
|
68
|
+
? (sevenDayReset
|
|
69
|
+
? `${quotaBar(sevenDay, getAdaptiveBarWidth(), colors, barStyle)} ${sevenDayDisplay} (resets in ${sevenDayReset})`
|
|
70
|
+
: `${quotaBar(sevenDay, getAdaptiveBarWidth(), colors, barStyle)} ${sevenDayDisplay}`)
|
|
71
|
+
: (sevenDayReset
|
|
72
|
+
? `7d: ${sevenDayDisplay} (resets in ${sevenDayReset})`
|
|
73
|
+
: `7d: ${sevenDayDisplay}`);
|
|
74
|
+
return `${label} ${fiveHourPart} ${dim('│')} ${sevenDayPart}${syncingSuffix}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return `${label} ${fiveHourPart}${syncingSuffix}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import type { RenderContext } from '../types.js';
|
|
2
|
+
import { isLimitReached } from '../types.js';
|
|
3
|
+
import { getContextPercent, getBufferedPercent, getModelName, getProviderLabel } from '../stdin.js';
|
|
4
|
+
import { getOutputSpeed } from '../speed-tracker.js';
|
|
5
|
+
import { coloredBar, colorize, critical, cyan, dim, green, magenta, red, warning, yellow, getContextColor, quotaBar, claudeOrange, RESET } from './colors.js';
|
|
6
|
+
import { getAdaptiveBarWidth } from '../utils/terminal.js';
|
|
7
|
+
import { formatTokens, formatContextValue, formatResetTime, formatUsageError, formatUsagePercent } from '../utils/format.js';
|
|
8
|
+
|
|
9
|
+
const DEBUG = process.env.DEBUG?.includes('claude-hud') || process.env.DEBUG === '*';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Renders the full session line (model + context bar + project + git + counts + usage + duration).
|
|
13
|
+
* Used for compact layout mode.
|
|
14
|
+
*/
|
|
15
|
+
export function renderSessionLine(ctx: RenderContext): string {
|
|
16
|
+
const model = getModelName(ctx.stdin);
|
|
17
|
+
|
|
18
|
+
const rawPercent = getContextPercent(ctx.stdin);
|
|
19
|
+
const bufferedPercent = getBufferedPercent(ctx.stdin);
|
|
20
|
+
const autocompactMode = ctx.config?.display?.autocompactBuffer ?? 'enabled';
|
|
21
|
+
const percent = autocompactMode === 'disabled' ? rawPercent : bufferedPercent;
|
|
22
|
+
|
|
23
|
+
if (DEBUG && autocompactMode === 'disabled') {
|
|
24
|
+
console.error(`[claude-hud:context] autocompactBuffer=disabled, showing raw ${rawPercent}% (buffered would be ${bufferedPercent}%)`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const colors = ctx.config?.colors;
|
|
28
|
+
const barWidth = getAdaptiveBarWidth();
|
|
29
|
+
const barStyle = ctx.config?.display?.barStyle ?? 'classic';
|
|
30
|
+
const bar = coloredBar(percent, barWidth, colors, barStyle);
|
|
31
|
+
|
|
32
|
+
const parts: string[] = [];
|
|
33
|
+
const display = ctx.config?.display;
|
|
34
|
+
|
|
35
|
+
const contextValueMode = display?.contextValue ?? 'percent';
|
|
36
|
+
const contextValue = formatContextValue(ctx, percent, contextValueMode);
|
|
37
|
+
const contextValueDisplay = `${getContextColor(percent, colors)}${contextValue}${RESET}`;
|
|
38
|
+
|
|
39
|
+
// Model and context bar (FIRST)
|
|
40
|
+
// Plan name only shows if showUsage is enabled (respects hybrid toggle)
|
|
41
|
+
const providerLabel = getProviderLabel(ctx.stdin);
|
|
42
|
+
const showUsage = display?.showUsage !== false;
|
|
43
|
+
const planName = showUsage ? ctx.usageData?.planName : undefined;
|
|
44
|
+
const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
|
|
45
|
+
const billingLabel = showUsage ? (planName ?? (hasApiKey ? red('API') : undefined)) : undefined;
|
|
46
|
+
const planDisplay = providerLabel ?? billingLabel;
|
|
47
|
+
const modelDisplay = planDisplay ? `${model} | ${planDisplay}` : model;
|
|
48
|
+
|
|
49
|
+
if (display?.showModel !== false && display?.showContextBar !== false) {
|
|
50
|
+
parts.push(`${cyan(`[${modelDisplay}]`)} ${bar} ${contextValueDisplay}`);
|
|
51
|
+
} else if (display?.showModel !== false) {
|
|
52
|
+
parts.push(`${cyan(`[${modelDisplay}]`)} ${contextValueDisplay}`);
|
|
53
|
+
} else if (display?.showContextBar !== false) {
|
|
54
|
+
parts.push(`${bar} ${contextValueDisplay}`);
|
|
55
|
+
} else {
|
|
56
|
+
parts.push(contextValueDisplay);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (display?.activityIndicator) {
|
|
60
|
+
const hasRunning = ctx.transcript.tools.some(t => t.status === 'running');
|
|
61
|
+
const indicator = hasRunning ? colorize('◉', '\x1b[31m') : green('◉');
|
|
62
|
+
parts.unshift(indicator);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (ctx.burnRate && display?.showBurnRate) {
|
|
66
|
+
const formatted = ctx.burnRate.tokensPerMinute >= 1000
|
|
67
|
+
? `${(ctx.burnRate.tokensPerMinute / 1000).toFixed(1)}k`
|
|
68
|
+
: `${ctx.burnRate.tokensPerMinute}`;
|
|
69
|
+
parts.push(dim(`${formatted} tok/m`));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Project path + git status (SECOND)
|
|
73
|
+
let projectPart: string | null = null;
|
|
74
|
+
if (display?.showProject !== false && ctx.stdin.cwd) {
|
|
75
|
+
// Split by both Unix (/) and Windows (\) separators for cross-platform support
|
|
76
|
+
const segments = ctx.stdin.cwd.split(/[/\\]/).filter(Boolean);
|
|
77
|
+
const pathLevels = ctx.config?.pathLevels ?? 1;
|
|
78
|
+
// Always join with forward slash for consistent display
|
|
79
|
+
// Handle root path (/) which results in empty segments
|
|
80
|
+
const projectPath = segments.length > 0 ? segments.slice(-pathLevels).join('/') : '/';
|
|
81
|
+
projectPart = yellow(projectPath);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let gitPart = '';
|
|
85
|
+
const gitConfig = ctx.config?.gitStatus;
|
|
86
|
+
const showGit = gitConfig?.enabled ?? true;
|
|
87
|
+
|
|
88
|
+
if (showGit && ctx.gitStatus) {
|
|
89
|
+
const gitParts: string[] = [ctx.gitStatus.branch];
|
|
90
|
+
|
|
91
|
+
// Show dirty indicator
|
|
92
|
+
if ((gitConfig?.showDirty ?? true) && ctx.gitStatus.isDirty) {
|
|
93
|
+
gitParts.push('*');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Show ahead/behind (with space separator for readability)
|
|
97
|
+
if (gitConfig?.showAheadBehind) {
|
|
98
|
+
if (ctx.gitStatus.ahead > 0) {
|
|
99
|
+
gitParts.push(` ↑${ctx.gitStatus.ahead}`);
|
|
100
|
+
}
|
|
101
|
+
if (ctx.gitStatus.behind > 0) {
|
|
102
|
+
gitParts.push(` ↓${ctx.gitStatus.behind}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Show file stats in Starship-compatible format (!modified +added ✘deleted ?untracked)
|
|
107
|
+
if (gitConfig?.showFileStats && ctx.gitStatus.fileStats) {
|
|
108
|
+
const { modified, added, deleted, untracked } = ctx.gitStatus.fileStats;
|
|
109
|
+
const statParts: string[] = [];
|
|
110
|
+
if (modified > 0) statParts.push(`!${modified}`);
|
|
111
|
+
if (added > 0) statParts.push(`+${added}`);
|
|
112
|
+
if (deleted > 0) statParts.push(`✘${deleted}`);
|
|
113
|
+
if (untracked > 0) statParts.push(`?${untracked}`);
|
|
114
|
+
if (statParts.length > 0) {
|
|
115
|
+
gitParts.push(` ${statParts.join(' ')}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
gitPart = `${magenta('git:(')}${cyan(gitParts.join(''))}${magenta(')')}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (projectPart && gitPart) {
|
|
123
|
+
parts.push(`${projectPart} ${gitPart}`);
|
|
124
|
+
} else if (projectPart) {
|
|
125
|
+
parts.push(projectPart);
|
|
126
|
+
} else if (gitPart) {
|
|
127
|
+
parts.push(gitPart);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Session name (custom title from /rename, or auto-generated slug)
|
|
131
|
+
if (display?.showSessionName && ctx.transcript.sessionName) {
|
|
132
|
+
parts.push(dim(ctx.transcript.sessionName));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Config counts (respects environmentThreshold)
|
|
136
|
+
if (display?.showConfigCounts !== false) {
|
|
137
|
+
const totalCounts = ctx.claudeMdCount + ctx.rulesCount + ctx.mcpCount + ctx.hooksCount;
|
|
138
|
+
const envThreshold = display?.environmentThreshold ?? 0;
|
|
139
|
+
|
|
140
|
+
if (totalCounts > 0 && totalCounts >= envThreshold) {
|
|
141
|
+
if (ctx.claudeMdCount > 0) {
|
|
142
|
+
parts.push(dim(`${ctx.claudeMdCount} CLAUDE.md`));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (ctx.rulesCount > 0) {
|
|
146
|
+
parts.push(dim(`${ctx.rulesCount} rules`));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (ctx.mcpCount > 0) {
|
|
150
|
+
parts.push(dim(`${ctx.mcpCount} MCPs`));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (ctx.hooksCount > 0) {
|
|
154
|
+
parts.push(dim(`${ctx.hooksCount} hooks`));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Usage limits display (shown when enabled in config, respects usageThreshold)
|
|
160
|
+
if (display?.showUsage !== false && ctx.usageData?.planName && !providerLabel) {
|
|
161
|
+
if (ctx.usageData.apiUnavailable) {
|
|
162
|
+
const errorHint = formatUsageError(ctx.usageData.apiError);
|
|
163
|
+
parts.push(warning(`usage: ⚠${errorHint}`, colors));
|
|
164
|
+
} else if (isLimitReached(ctx.usageData)) {
|
|
165
|
+
const resetTime = ctx.usageData.fiveHour === 100
|
|
166
|
+
? formatResetTime(ctx.usageData.fiveHourResetAt)
|
|
167
|
+
: formatResetTime(ctx.usageData.sevenDayResetAt);
|
|
168
|
+
parts.push(critical(`⚠ Limit reached${resetTime ? ` (resets ${resetTime})` : ''}`, colors));
|
|
169
|
+
} else {
|
|
170
|
+
const usageThreshold = display?.usageThreshold ?? 0;
|
|
171
|
+
const fiveHour = ctx.usageData.fiveHour;
|
|
172
|
+
const sevenDay = ctx.usageData.sevenDay;
|
|
173
|
+
const effectiveUsage = Math.max(fiveHour ?? 0, sevenDay ?? 0);
|
|
174
|
+
|
|
175
|
+
if (effectiveUsage >= usageThreshold) {
|
|
176
|
+
const syncingSuffix = ctx.usageData.apiError === 'rate-limited'
|
|
177
|
+
? ` ${dim('(syncing...)')}`
|
|
178
|
+
: '';
|
|
179
|
+
const fiveHourDisplay = formatUsagePercent(fiveHour, colors);
|
|
180
|
+
const fiveHourReset = formatResetTime(ctx.usageData.fiveHourResetAt);
|
|
181
|
+
|
|
182
|
+
const usageBarEnabled = display?.usageBarEnabled ?? true;
|
|
183
|
+
const fiveHourPart = usageBarEnabled
|
|
184
|
+
? (fiveHourReset
|
|
185
|
+
? `${quotaBar(fiveHour ?? 0, barWidth, colors, barStyle)} ${fiveHourDisplay} (${fiveHourReset} / 5h)`
|
|
186
|
+
: `${quotaBar(fiveHour ?? 0, barWidth, colors, barStyle)} ${fiveHourDisplay}`)
|
|
187
|
+
: (fiveHourReset
|
|
188
|
+
? `5h: ${fiveHourDisplay} (${fiveHourReset})`
|
|
189
|
+
: `5h: ${fiveHourDisplay}`);
|
|
190
|
+
|
|
191
|
+
const sevenDayThreshold = display?.sevenDayThreshold ?? 80;
|
|
192
|
+
if (sevenDay !== null && sevenDay >= sevenDayThreshold) {
|
|
193
|
+
const sevenDayDisplay = formatUsagePercent(sevenDay, colors);
|
|
194
|
+
const sevenDayReset = formatResetTime(ctx.usageData.sevenDayResetAt);
|
|
195
|
+
const sevenDayPart = usageBarEnabled
|
|
196
|
+
? (sevenDayReset
|
|
197
|
+
? `${quotaBar(sevenDay, barWidth, colors, barStyle)} ${sevenDayDisplay} (${sevenDayReset} / 7d)`
|
|
198
|
+
: `${quotaBar(sevenDay, barWidth, colors, barStyle)} ${sevenDayDisplay}`)
|
|
199
|
+
: (sevenDayReset
|
|
200
|
+
? `7d: ${sevenDayDisplay} (${sevenDayReset})`
|
|
201
|
+
: `7d: ${sevenDayDisplay}`);
|
|
202
|
+
parts.push(`${fiveHourPart} ${dim('│')} ${sevenDayPart}${syncingSuffix}`);
|
|
203
|
+
} else {
|
|
204
|
+
parts.push(`${fiveHourPart}${syncingSuffix}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Session duration
|
|
211
|
+
if (display?.showSpeed) {
|
|
212
|
+
const speed = getOutputSpeed(ctx.stdin);
|
|
213
|
+
if (speed !== null) {
|
|
214
|
+
parts.push(dim(`out: ${speed.toFixed(1)} tok/s`));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (display?.showDuration !== false && ctx.sessionDuration) {
|
|
219
|
+
parts.push(dim(`⏱️ ${ctx.sessionDuration}`));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (ctx.extraLabel) {
|
|
223
|
+
parts.push(dim(ctx.extraLabel));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Custom line (static user-defined text)
|
|
227
|
+
const customLine = display?.customLine;
|
|
228
|
+
if (customLine) {
|
|
229
|
+
parts.push(claudeOrange(customLine));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (ctx.alerts && ctx.alerts.length > 0) {
|
|
233
|
+
const criticalAlert = ctx.alerts.find(a => a.type.includes('critical'));
|
|
234
|
+
if (criticalAlert) {
|
|
235
|
+
parts.push(red(`⚠ ${criticalAlert.message}`));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let line = parts.join(' | ');
|
|
240
|
+
|
|
241
|
+
// Token breakdown at high context
|
|
242
|
+
if (display?.showTokenBreakdown !== false && percent >= 85) {
|
|
243
|
+
const usage = ctx.stdin.context_window?.current_usage;
|
|
244
|
+
if (usage) {
|
|
245
|
+
const input = formatTokens(usage.input_tokens ?? 0);
|
|
246
|
+
const cache = formatTokens((usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0));
|
|
247
|
+
line += dim(` (in: ${input}, cache: ${cache})`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return line;
|
|
252
|
+
}
|
|
253
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { RenderContext } from '../types.js';
|
|
2
|
+
import { yellow, green, dim, claudeOrange } from './colors.js';
|
|
3
|
+
import { truncateString } from '../utils/format.js';
|
|
4
|
+
|
|
5
|
+
export function renderTodosLine(ctx: RenderContext): string | null {
|
|
6
|
+
const { todos } = ctx.transcript;
|
|
7
|
+
|
|
8
|
+
if (!todos || todos.length === 0) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const inProgress = todos.find((t) => t.status === 'in_progress');
|
|
13
|
+
const completed = todos.filter((t) => t.status === 'completed').length;
|
|
14
|
+
const total = todos.length;
|
|
15
|
+
|
|
16
|
+
if (!inProgress) {
|
|
17
|
+
if (completed === total && total > 0) {
|
|
18
|
+
return `${green('✓')} All todos complete ${dim(`(${completed}/${total})`)}`;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const content = truncateString(inProgress.content, 50);
|
|
24
|
+
const progress = dim(`(${completed}/${total})`);
|
|
25
|
+
|
|
26
|
+
const miniBar = ctx.transcript.todos.slice(0, 10).map(todo => {
|
|
27
|
+
if (todo.status === 'completed') return green('▪');
|
|
28
|
+
if (todo.status === 'in_progress') return claudeOrange('▪');
|
|
29
|
+
return dim('▪');
|
|
30
|
+
}).join('');
|
|
31
|
+
const suffix = ctx.transcript.todos.length > 10 ? dim('…') : '';
|
|
32
|
+
|
|
33
|
+
return `${yellow('▸')} ${content} ${progress} │ ${miniBar}${suffix}`;
|
|
34
|
+
}
|
|
35
|
+
|