@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.
Files changed (210) hide show
  1. package/.claude-plugin/marketplace.json +20 -0
  2. package/.claude-plugin/plugin.json +20 -0
  3. package/LICENSE +21 -0
  4. package/README.md +379 -0
  5. package/commands/configure.md +361 -0
  6. package/commands/export.md +43 -0
  7. package/commands/health.md +61 -0
  8. package/commands/setup.md +287 -0
  9. package/commands/theme.md +31 -0
  10. package/dist/alert.d.ts +31 -0
  11. package/dist/alert.d.ts.map +1 -0
  12. package/dist/alert.js +53 -0
  13. package/dist/alert.js.map +1 -0
  14. package/dist/burn-rate.d.ts +4 -0
  15. package/dist/burn-rate.d.ts.map +1 -0
  16. package/dist/burn-rate.js +36 -0
  17. package/dist/burn-rate.js.map +1 -0
  18. package/dist/cache.d.ts +6 -0
  19. package/dist/cache.d.ts.map +1 -0
  20. package/dist/cache.js +47 -0
  21. package/dist/cache.js.map +1 -0
  22. package/dist/claude-config-dir.d.ts +4 -0
  23. package/dist/claude-config-dir.d.ts.map +1 -0
  24. package/dist/claude-config-dir.js +24 -0
  25. package/dist/claude-config-dir.js.map +1 -0
  26. package/dist/config-io.d.ts +6 -0
  27. package/dist/config-io.d.ts.map +1 -0
  28. package/dist/config-io.js +27 -0
  29. package/dist/config-io.js.map +1 -0
  30. package/dist/config-reader.d.ts +8 -0
  31. package/dist/config-reader.d.ts.map +1 -0
  32. package/dist/config-reader.js +204 -0
  33. package/dist/config-reader.js.map +1 -0
  34. package/dist/config.d.ts +94 -0
  35. package/dist/config.d.ts.map +1 -0
  36. package/dist/config.js +358 -0
  37. package/dist/config.js.map +1 -0
  38. package/dist/constants.d.ts +11 -0
  39. package/dist/constants.d.ts.map +1 -0
  40. package/dist/constants.js +11 -0
  41. package/dist/constants.js.map +1 -0
  42. package/dist/cost-tracker.d.ts +9 -0
  43. package/dist/cost-tracker.d.ts.map +1 -0
  44. package/dist/cost-tracker.js +46 -0
  45. package/dist/cost-tracker.js.map +1 -0
  46. package/dist/debug.d.ts +6 -0
  47. package/dist/debug.d.ts.map +1 -0
  48. package/dist/debug.js +15 -0
  49. package/dist/debug.js.map +1 -0
  50. package/dist/extra-cmd.d.ts +20 -0
  51. package/dist/extra-cmd.d.ts.map +1 -0
  52. package/dist/extra-cmd.js +112 -0
  53. package/dist/extra-cmd.js.map +1 -0
  54. package/dist/git.d.ts +16 -0
  55. package/dist/git.d.ts.map +1 -0
  56. package/dist/git.js +94 -0
  57. package/dist/git.js.map +1 -0
  58. package/dist/health-check.d.ts +12 -0
  59. package/dist/health-check.d.ts.map +1 -0
  60. package/dist/health-check.js +37 -0
  61. package/dist/health-check.js.map +1 -0
  62. package/dist/index.d.ts +24 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +198 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/providers/agent-teams-provider.d.ts +10 -0
  67. package/dist/providers/agent-teams-provider.d.ts.map +1 -0
  68. package/dist/providers/agent-teams-provider.js +57 -0
  69. package/dist/providers/agent-teams-provider.js.map +1 -0
  70. package/dist/providers/agw-provider.d.ts +10 -0
  71. package/dist/providers/agw-provider.d.ts.map +1 -0
  72. package/dist/providers/agw-provider.js +49 -0
  73. package/dist/providers/agw-provider.js.map +1 -0
  74. package/dist/providers/index.d.ts +14 -0
  75. package/dist/providers/index.d.ts.map +1 -0
  76. package/dist/providers/index.js +25 -0
  77. package/dist/providers/index.js.map +1 -0
  78. package/dist/render/agents-line.d.ts +3 -0
  79. package/dist/render/agents-line.d.ts.map +1 -0
  80. package/dist/render/agents-line.js +40 -0
  81. package/dist/render/agents-line.js.map +1 -0
  82. package/dist/render/alert-line.d.ts +3 -0
  83. package/dist/render/alert-line.d.ts.map +1 -0
  84. package/dist/render/alert-line.js +11 -0
  85. package/dist/render/alert-line.js.map +1 -0
  86. package/dist/render/colors.d.ts +39 -0
  87. package/dist/render/colors.d.ts.map +1 -0
  88. package/dist/render/colors.js +109 -0
  89. package/dist/render/colors.js.map +1 -0
  90. package/dist/render/framework-line.d.ts +3 -0
  91. package/dist/render/framework-line.d.ts.map +1 -0
  92. package/dist/render/framework-line.js +32 -0
  93. package/dist/render/framework-line.js.map +1 -0
  94. package/dist/render/index.d.ts +3 -0
  95. package/dist/render/index.d.ts.map +1 -0
  96. package/dist/render/index.js +435 -0
  97. package/dist/render/index.js.map +1 -0
  98. package/dist/render/lines/environment.d.ts +3 -0
  99. package/dist/render/lines/environment.d.ts.map +1 -0
  100. package/dist/render/lines/environment.js +30 -0
  101. package/dist/render/lines/environment.js.map +1 -0
  102. package/dist/render/lines/identity.d.ts +3 -0
  103. package/dist/render/lines/identity.d.ts.map +1 -0
  104. package/dist/render/lines/identity.js +93 -0
  105. package/dist/render/lines/identity.js.map +1 -0
  106. package/dist/render/lines/index.d.ts +5 -0
  107. package/dist/render/lines/index.d.ts.map +1 -0
  108. package/dist/render/lines/index.js +5 -0
  109. package/dist/render/lines/index.js.map +1 -0
  110. package/dist/render/lines/project.d.ts +3 -0
  111. package/dist/render/lines/project.d.ts.map +1 -0
  112. package/dist/render/lines/project.js +100 -0
  113. package/dist/render/lines/project.js.map +1 -0
  114. package/dist/render/lines/usage.d.ts +3 -0
  115. package/dist/render/lines/usage.d.ts.map +1 -0
  116. package/dist/render/lines/usage.js +65 -0
  117. package/dist/render/lines/usage.js.map +1 -0
  118. package/dist/render/session-line.d.ts +7 -0
  119. package/dist/render/session-line.d.ts.map +1 -0
  120. package/dist/render/session-line.js +227 -0
  121. package/dist/render/session-line.js.map +1 -0
  122. package/dist/render/todos-line.d.ts +3 -0
  123. package/dist/render/todos-line.d.ts.map +1 -0
  124. package/dist/render/todos-line.js +29 -0
  125. package/dist/render/todos-line.js.map +1 -0
  126. package/dist/render/tools-line.d.ts +3 -0
  127. package/dist/render/tools-line.d.ts.map +1 -0
  128. package/dist/render/tools-line.js +45 -0
  129. package/dist/render/tools-line.js.map +1 -0
  130. package/dist/session-history.d.ts +15 -0
  131. package/dist/session-history.d.ts.map +1 -0
  132. package/dist/session-history.js +46 -0
  133. package/dist/session-history.js.map +1 -0
  134. package/dist/session-stats.d.ts +11 -0
  135. package/dist/session-stats.d.ts.map +1 -0
  136. package/dist/session-stats.js +48 -0
  137. package/dist/session-stats.js.map +1 -0
  138. package/dist/speed-tracker.d.ts +7 -0
  139. package/dist/speed-tracker.d.ts.map +1 -0
  140. package/dist/speed-tracker.js +34 -0
  141. package/dist/speed-tracker.js.map +1 -0
  142. package/dist/stdin.d.ts +9 -0
  143. package/dist/stdin.d.ts.map +1 -0
  144. package/dist/stdin.js +142 -0
  145. package/dist/stdin.js.map +1 -0
  146. package/dist/themes.d.ts +10 -0
  147. package/dist/themes.d.ts.map +1 -0
  148. package/dist/themes.js +81 -0
  149. package/dist/themes.js.map +1 -0
  150. package/dist/transcript.d.ts +3 -0
  151. package/dist/transcript.d.ts.map +1 -0
  152. package/dist/transcript.js +221 -0
  153. package/dist/transcript.js.map +1 -0
  154. package/dist/types.d.ts +124 -0
  155. package/dist/types.d.ts.map +1 -0
  156. package/dist/types.js +5 -0
  157. package/dist/types.js.map +1 -0
  158. package/dist/usage-api.d.ts +62 -0
  159. package/dist/usage-api.d.ts.map +1 -0
  160. package/dist/usage-api.js +908 -0
  161. package/dist/usage-api.js.map +1 -0
  162. package/dist/utils/format.d.ts +9 -0
  163. package/dist/utils/format.d.ts.map +1 -0
  164. package/dist/utils/format.js +75 -0
  165. package/dist/utils/format.js.map +1 -0
  166. package/dist/utils/terminal.d.ts +5 -0
  167. package/dist/utils/terminal.d.ts.map +1 -0
  168. package/dist/utils/terminal.js +42 -0
  169. package/dist/utils/terminal.js.map +1 -0
  170. package/package.json +36 -0
  171. package/src/alert.ts +75 -0
  172. package/src/burn-rate.ts +45 -0
  173. package/src/cache.ts +57 -0
  174. package/src/claude-config-dir.ts +27 -0
  175. package/src/config-io.ts +26 -0
  176. package/src/config-reader.ts +236 -0
  177. package/src/config.ts +496 -0
  178. package/src/constants.ts +10 -0
  179. package/src/cost-tracker.ts +53 -0
  180. package/src/debug.ts +16 -0
  181. package/src/extra-cmd.ts +125 -0
  182. package/src/git.ts +126 -0
  183. package/src/health-check.ts +50 -0
  184. package/src/index.ts +234 -0
  185. package/src/providers/agent-teams-provider.ts +56 -0
  186. package/src/providers/agw-provider.ts +47 -0
  187. package/src/providers/index.ts +27 -0
  188. package/src/render/agents-line.ts +51 -0
  189. package/src/render/alert-line.ts +11 -0
  190. package/src/render/colors.ts +145 -0
  191. package/src/render/framework-line.ts +34 -0
  192. package/src/render/index.ts +512 -0
  193. package/src/render/lines/environment.ts +41 -0
  194. package/src/render/lines/identity.ts +109 -0
  195. package/src/render/lines/index.ts +4 -0
  196. package/src/render/lines/project.ts +113 -0
  197. package/src/render/lines/usage.ts +79 -0
  198. package/src/render/session-line.ts +253 -0
  199. package/src/render/todos-line.ts +35 -0
  200. package/src/render/tools-line.ts +58 -0
  201. package/src/session-history.ts +62 -0
  202. package/src/session-stats.ts +65 -0
  203. package/src/speed-tracker.ts +51 -0
  204. package/src/stdin.ts +169 -0
  205. package/src/themes.ts +90 -0
  206. package/src/transcript.ts +268 -0
  207. package/src/types.ts +146 -0
  208. package/src/usage-api.ts +1090 -0
  209. package/src/utils/format.ts +79 -0
  210. 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,4 @@
1
+ export { renderIdentityLine } from './identity.js';
2
+ export { renderProjectLine } from './project.js';
3
+ export { renderEnvironmentLine } from './environment.js';
4
+ export { renderUsageLine } from './usage.js';
@@ -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
+