pinglet-cli 0.6.2 → 0.7.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.
@@ -0,0 +1,787 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.renderHud = renderHud;
37
+ exports.formatProjectPath = formatProjectPath;
38
+ exports.prettifyAdvisorId = prettifyAdvisorId;
39
+ exports.formatSessionDuration = formatSessionDuration;
40
+ exports.formatResetTime = formatResetTime;
41
+ exports.cacheHitRate = cacheHitRate;
42
+ exports.sessionTokenSummary = sessionTokenSummary;
43
+ exports.formatAgentModel = formatAgentModel;
44
+ const fs = __importStar(require("fs"));
45
+ const path = __importStar(require("path"));
46
+ const url_1 = require("url");
47
+ const colors_1 = require("./colors");
48
+ const stdin_1 = require("./stdin");
49
+ const transcript_1 = require("./transcript");
50
+ const memory_1 = require("./memory");
51
+ const labels_1 = require("./labels");
52
+ const width_1 = require("./width");
53
+ const ACTIVITY = new Set(["tools", "skills", "agents", "todos"]);
54
+ const SEP = " │ ";
55
+ const COMPLETED_AGENT_RETENTION_MS = 60_000;
56
+ function renderHud(ctx) {
57
+ const width = (0, width_1.terminalWidth)(ctx.config.maxWidth, ctx.config.forceMaxWidth);
58
+ const lines = ctx.config.lineLayout === "expanded" ? renderExpanded(ctx, width) : renderCompact(ctx, width);
59
+ const physical = lines.flatMap((line) => line.split("\n"));
60
+ const fitted = width === null ? physical : physical.flatMap((line) => (0, width_1.wrapLineToWidth)(line, width));
61
+ return fitted.map((line) => `${colors_1.RESET}${line}`);
62
+ }
63
+ // ---------------------------------------------------------------- layouts
64
+ function renderExpanded(ctx, width) {
65
+ const { elementOrder, display, showSeparators } = ctx.config;
66
+ const groupOf = new Map();
67
+ for (const group of display.mergeGroups) {
68
+ const set = new Set(group);
69
+ for (const element of group)
70
+ if (!groupOf.has(element))
71
+ groupOf.set(element, set);
72
+ }
73
+ const rendered = [];
74
+ const seen = new Set();
75
+ for (let index = 0; index < elementOrder.length; index++) {
76
+ const element = elementOrder[index];
77
+ if (seen.has(element))
78
+ continue;
79
+ const group = groupOf.get(element);
80
+ // 같은 merge group에 속한 연속 요소를 모아 한 줄 후보로 만든다.
81
+ const sequence = [];
82
+ if (group) {
83
+ for (let j = index; j < elementOrder.length; j++) {
84
+ const candidate = elementOrder[j];
85
+ if (seen.has(candidate) || !group.has(candidate))
86
+ break;
87
+ sequence.push(candidate);
88
+ }
89
+ }
90
+ if (sequence.length > 1) {
91
+ index += sequence.length - 1;
92
+ sequence.forEach((e) => seen.add(e));
93
+ const parts = sequence
94
+ .map((e) => ({ element: e, line: renderElement(ctx, e, width) }))
95
+ .filter((p) => !!p.line);
96
+ if (parts.length === 0)
97
+ continue;
98
+ const combined = parts.map((p) => p.line).join(SEP);
99
+ if (parts.length === 1 || width === null || (0, width_1.visualWidth)(combined) <= width) {
100
+ rendered.push({ line: combined, activity: parts.some((p) => ACTIVITY.has(p.element)) });
101
+ }
102
+ else {
103
+ parts.forEach((p) => rendered.push({ line: p.line, activity: ACTIVITY.has(p.element) }));
104
+ }
105
+ continue;
106
+ }
107
+ seen.add(element);
108
+ const line = renderElement(ctx, element, width);
109
+ if (line)
110
+ rendered.push({ line, activity: ACTIVITY.has(element) });
111
+ }
112
+ const gitFiles = renderGitFilesLine(ctx, width);
113
+ if (gitFiles)
114
+ rendered.push({ line: gitFiles, activity: false });
115
+ const lines = rendered.map((r) => r.line);
116
+ const sessionTokens = renderSessionTokensLine(ctx);
117
+ if (sessionTokens)
118
+ lines.push(sessionTokens);
119
+ const compactions = renderCompactionsLine(ctx);
120
+ if (compactions)
121
+ lines.push(compactions);
122
+ if (showSeparators) {
123
+ const firstActivity = rendered.findIndex((r) => r.activity);
124
+ if (firstActivity > 0)
125
+ lines.splice(firstActivity, 0, separator(rendered.slice(0, firstActivity).map((r) => r.line), width));
126
+ }
127
+ return lines;
128
+ }
129
+ function renderCompact(ctx, width) {
130
+ const lines = [];
131
+ const session = renderSessionLine(ctx, width);
132
+ if (session)
133
+ lines.push(session);
134
+ const activity = ["tools", "skills", "agents", "todos"]
135
+ .map((e) => renderElement(ctx, e, width))
136
+ .filter((line) => !!line);
137
+ if (ctx.config.showSeparators && activity.length > 0 && lines.length > 0)
138
+ lines.push(separator(lines, width));
139
+ lines.push(...activity);
140
+ return lines;
141
+ }
142
+ function separator(above, width) {
143
+ const base = Math.max(20, ...above.map(width_1.visualWidth));
144
+ const length = width === null ? base : Math.min(base, width);
145
+ return (0, colors_1.dim)("─".repeat(length));
146
+ }
147
+ function renderElement(ctx, element, width) {
148
+ const { display } = ctx.config;
149
+ switch (element) {
150
+ case "project":
151
+ return renderProjectLine(ctx);
152
+ case "context":
153
+ return renderContextLine(ctx, width);
154
+ case "usage":
155
+ return renderUsageLine(ctx, width);
156
+ case "promptCache":
157
+ return display.showPromptCache ? renderPromptCacheLine(ctx) : null;
158
+ case "memory":
159
+ return display.showMemoryUsage && ctx.config.lineLayout === "expanded" ? renderMemoryLine(ctx, width) : null;
160
+ case "tools":
161
+ return display.showTools ? renderToolsLine(ctx) : null;
162
+ case "skills":
163
+ return display.showSkills ? renderSkillsLine(ctx) : null;
164
+ case "agents":
165
+ return display.showAgents ? renderAgentsLine(ctx) : null;
166
+ case "todos":
167
+ return display.showTodos ? renderTodosLine(ctx) : null;
168
+ }
169
+ }
170
+ /** projectLineOrder에 적힌 세그먼트를 앞으로 당기고 나머지는 원래 순서를 유지한다. */
171
+ function orderParts(parts, order) {
172
+ if (order.length === 0)
173
+ return parts.map((p) => p.text);
174
+ const used = new Set();
175
+ const out = [];
176
+ for (const key of order) {
177
+ parts.forEach((part, i) => {
178
+ if (!used.has(i) && part.key === key) {
179
+ used.add(i);
180
+ out.push(part.text);
181
+ }
182
+ });
183
+ }
184
+ parts.forEach((part, i) => {
185
+ if (!used.has(i))
186
+ out.push(part.text);
187
+ });
188
+ return out;
189
+ }
190
+ function formatProjectPath(cwd, levels) {
191
+ const safe = (0, transcript_1.sanitizeText)(cwd, 512);
192
+ const segments = safe.split(/[/\\]/).filter(Boolean);
193
+ if (levels !== "full") {
194
+ const shown = segments.slice(-levels).join("/");
195
+ return shown || (/^[/\\]/.test(safe) ? "/" : safe);
196
+ }
197
+ if (/^[A-Za-z]:[\\/]/.test(safe))
198
+ return segments.join("/");
199
+ return `${/^[/\\]/.test(safe) ? "/" : ""}${segments.join("/")}` || safe;
200
+ }
201
+ function fileHref(filePath) {
202
+ try {
203
+ return (0, url_1.pathToFileURL)(path.resolve(filePath)).toString();
204
+ }
205
+ catch {
206
+ return null;
207
+ }
208
+ }
209
+ function currentDir(ctx) {
210
+ const cwd = ctx.payload.cwd ?? ctx.payload.workspace?.current_dir;
211
+ return typeof cwd === "string" && cwd ? cwd : undefined;
212
+ }
213
+ /** "claude-opus-4-7" → "Opus 4.7", "opus" → "Opus". 모르는 형태는 접두사만 뗀다. */
214
+ function prettifyAdvisorId(rawId) {
215
+ const id = rawId.trim();
216
+ const match = id.match(/^(?:claude-)?(opus|sonnet|haiku|fable)-(\d+)-(\d+)/i);
217
+ if (match)
218
+ return `${match[1][0].toUpperCase()}${match[1].slice(1).toLowerCase()} ${match[2]}.${match[3]}`;
219
+ const lower = id.toLowerCase();
220
+ if (["opus", "sonnet", "haiku", "fable"].includes(lower))
221
+ return lower[0].toUpperCase() + lower.slice(1);
222
+ return id.replace(/^claude-/i, "");
223
+ }
224
+ function advisorSegment(ctx) {
225
+ const { display, colors } = ctx.config;
226
+ if (!display.showAdvisor)
227
+ return null;
228
+ const override = (0, transcript_1.sanitizeText)(display.advisorOverride, 64);
229
+ const raw = override || ctx.transcript.advisorModel;
230
+ if (!raw)
231
+ return null;
232
+ const pretty = (0, transcript_1.sanitizeText)(override ? raw : prettifyAdvisorId(raw), 64);
233
+ return pretty ? `${(0, colors_1.label)(`${(0, labels_1.hudLabel)("advisor")}:`, colors)} ${pretty}` : null;
234
+ }
235
+ function effortSuffix(ctx) {
236
+ const { display } = ctx.config;
237
+ if (!display.showEffortLevel)
238
+ return "";
239
+ const info = (0, stdin_1.resolveEffortLevel)(ctx.payload.effort, ctx.transcript.ultracodeActive);
240
+ if (!info)
241
+ return "";
242
+ const isUltracode = info.level.startsWith("ultracode(");
243
+ if (display.effortFormat === "symbol" && info.symbol && !isUltracode)
244
+ return ` ${info.symbol}`;
245
+ if (display.effortFormat === "text" || !info.symbol)
246
+ return ` ${info.level}`;
247
+ return ` ${info.symbol} ${info.level}`;
248
+ }
249
+ function modelBadge(ctx) {
250
+ const { display, colors } = ctx.config;
251
+ const name = (0, stdin_1.formatModelName)((0, stdin_1.getModelName)(ctx.payload), display.modelFormat);
252
+ return (0, colors_1.model)(`[${name}${effortSuffix(ctx)}]`, colors);
253
+ }
254
+ function aheadText(ctx, ahead) {
255
+ const { gitStatus: cfg, colors } = ctx.config;
256
+ const value = `↑${ahead}`;
257
+ if (cfg.pushCriticalThreshold > 0 && ahead >= cfg.pushCriticalThreshold)
258
+ return (0, colors_1.critical)(value, colors);
259
+ if (cfg.pushWarningThreshold > 0 && ahead >= cfg.pushWarningThreshold)
260
+ return (0, colors_1.warning)(value, colors);
261
+ return (0, colors_1.gitBranch)(value, colors);
262
+ }
263
+ /** expanded 첫 줄의 git 배지: `git:(main* ↑2 ↓1 [+12 -3])` — 브랜치는 GitHub 링크. */
264
+ function gitBadgeExpanded(ctx) {
265
+ const { gitStatus: git } = ctx;
266
+ const { gitStatus: cfg, colors } = ctx.config;
267
+ if (!git || !cfg.enabled)
268
+ return "";
269
+ const branchText = git.branch + (cfg.showDirty && git.isDirty ? "*" : "");
270
+ const inner = [(0, width_1.safeHyperlink)(git.branchUrl, (0, colors_1.gitBranch)(branchText, colors))];
271
+ if (cfg.showAheadBehind && git.ahead > 0)
272
+ inner.push(aheadText(ctx, git.ahead));
273
+ if (cfg.showAheadBehind && git.behind > 0)
274
+ inner.push((0, colors_1.gitBranch)(`↓${git.behind}`, colors));
275
+ if (cfg.showFileStats && git.lineDiff) {
276
+ const diff = [];
277
+ if (git.lineDiff.added > 0)
278
+ diff.push((0, colors_1.green)(`+${git.lineDiff.added}`));
279
+ if (git.lineDiff.deleted > 0)
280
+ diff.push((0, colors_1.red)(`-${git.lineDiff.deleted}`));
281
+ if (diff.length > 0)
282
+ inner.push(`[${diff.join(" ")}]`);
283
+ }
284
+ return `${(0, colors_1.git)("git:(", colors)}${inner.join(" ")}${(0, colors_1.git)(")", colors)}`;
285
+ }
286
+ /** compact 세션 줄의 git 배지: Starship식 `git:(main* ↑2 !3 +1 ✘1 ?2)`. */
287
+ function gitBadgeCompact(ctx) {
288
+ const { gitStatus: git } = ctx;
289
+ const { gitStatus: cfg, colors } = ctx.config;
290
+ if (!git || !cfg.enabled)
291
+ return "";
292
+ const parts = [git.branch];
293
+ if (cfg.showDirty && git.isDirty)
294
+ parts.push("*");
295
+ if (cfg.showAheadBehind && git.ahead > 0)
296
+ parts.push(` ↑${git.ahead}`);
297
+ if (cfg.showAheadBehind && git.behind > 0)
298
+ parts.push(` ↓${git.behind}`);
299
+ if (cfg.showFileStats && git.fileStats) {
300
+ const { modified, added, deleted, untracked } = git.fileStats;
301
+ const stats = [];
302
+ if (modified > 0)
303
+ stats.push(`!${modified}`);
304
+ if (added > 0)
305
+ stats.push(`+${added}`);
306
+ if (deleted > 0)
307
+ stats.push(`✘${deleted}`);
308
+ if (untracked > 0)
309
+ stats.push(`?${untracked}`);
310
+ if (stats.length > 0)
311
+ parts.push(` ${stats.join(" ")}`);
312
+ }
313
+ return `${(0, colors_1.git)("git:(", colors)}${(0, colors_1.gitBranch)(parts.join(""), colors)}${(0, colors_1.git)(")", colors)}`;
314
+ }
315
+ function pushProjectParts(ctx, gitPart, push, linkPath) {
316
+ const { display, pathLevels, colors, gitStatus: cfg } = ctx.config;
317
+ const cwd = currentDir(ctx);
318
+ let projectPart = null;
319
+ if (display.showProject && cwd) {
320
+ const colored = (0, colors_1.project)(formatProjectPath(cwd, pathLevels), colors);
321
+ projectPart = linkPath ? (0, width_1.safeHyperlink)(fileHref(cwd), colored) : colored;
322
+ }
323
+ if (projectPart && gitPart) {
324
+ if (cfg.branchOverflow === "wrap") {
325
+ push(projectPart, "project");
326
+ push(gitPart, "project");
327
+ }
328
+ else {
329
+ push(`${projectPart} ${gitPart}`, "project");
330
+ }
331
+ }
332
+ else if (projectPart) {
333
+ push(projectPart, "project");
334
+ }
335
+ else if (gitPart) {
336
+ push(gitPart, "project");
337
+ }
338
+ }
339
+ function trailingSegments(ctx, push) {
340
+ const { display, colors } = ctx.config;
341
+ const advisor = advisorSegment(ctx);
342
+ if (advisor)
343
+ push(advisor, "advisor");
344
+ if (display.showSessionName && ctx.transcript.sessionName)
345
+ push((0, colors_1.label)(ctx.transcript.sessionName, colors), "sessionName");
346
+ if (display.showDuration) {
347
+ const duration = formatSessionDuration(ctx.transcript.sessionStart, ctx.now);
348
+ if (duration)
349
+ push((0, colors_1.label)(`⏱ ${duration}`, colors), "duration");
350
+ }
351
+ if (display.showCost) {
352
+ const cost = ctx.payload.cost?.total_cost_usd;
353
+ if (typeof cost === "number" && Number.isFinite(cost) && cost > 0) {
354
+ push((0, colors_1.label)(`${(0, labels_1.hudLabel)("cost")} ${(0, stdin_1.formatUsd)(cost)}`, colors), "cost");
355
+ }
356
+ }
357
+ if (display.showSpeed && ctx.speed !== null && Number.isFinite(ctx.speed)) {
358
+ push((0, colors_1.label)(`${(0, labels_1.hudLabel)("out")}: ${ctx.speed.toFixed(1)} ${(0, labels_1.hudLabel)("tokPerSec")}`, colors), "speed");
359
+ }
360
+ }
361
+ function formatSessionDuration(sessionStart, now) {
362
+ if (sessionStart === undefined)
363
+ return "";
364
+ const mins = Math.floor(Math.max(0, now - sessionStart) / 60_000);
365
+ if (mins < 1)
366
+ return "<1m";
367
+ if (mins < 60)
368
+ return `${mins}m`;
369
+ return `${Math.floor(mins / 60)}h ${mins % 60}m`;
370
+ }
371
+ function renderProjectLine(ctx) {
372
+ const { display, projectLineOrder } = ctx.config;
373
+ const parts = [];
374
+ const push = (text, key = null) => parts.push({ key, text });
375
+ if (display.showModel)
376
+ push(modelBadge(ctx), "model");
377
+ pushProjectParts(ctx, gitBadgeExpanded(ctx), push, true);
378
+ trailingSegments(ctx, push);
379
+ if (parts.length === 0)
380
+ return null;
381
+ return orderParts(parts, projectLineOrder).join(SEP);
382
+ }
383
+ /** compact 레이아웃의 한 줄 — 모델+컨텍스트 바, 프로젝트, 사용량, 부가 정보를 " | "로 잇는다. */
384
+ function renderSessionLine(ctx, width) {
385
+ const { display, colors, projectLineOrder } = ctx.config;
386
+ const parts = [];
387
+ const push = (text, key = null) => parts.push({ key, text });
388
+ const percent = (0, stdin_1.getContextPercent)(ctx.payload);
389
+ const thresholds = { warning: display.contextWarningThreshold, critical: display.contextCriticalThreshold };
390
+ const value = `${(0, colors_1.contextColor)(percent, colors, thresholds)}${formatContextValue(ctx, percent)}${colors_1.RESET}`;
391
+ const bar = (0, colors_1.contextBar)(percent, (0, width_1.adaptiveBarWidth)(width), colors, thresholds);
392
+ const head = [display.showModel ? modelBadge(ctx) : "", display.showContextBar ? bar : "", value].filter(Boolean).join(" ");
393
+ push(head, "model");
394
+ pushProjectParts(ctx, gitBadgeCompact(ctx), push, false);
395
+ usageParts(ctx, width).forEach((part) => push(part));
396
+ const sessionTokens = sessionTokenSummary(ctx.transcript.sessionTokens, `${(0, labels_1.hudLabel)("tok")}:`);
397
+ if (display.showSessionTokens && sessionTokens)
398
+ push((0, colors_1.label)(sessionTokens, colors));
399
+ if (display.showCompactions && (ctx.transcript.compactionCount ?? 0) > 0) {
400
+ push((0, colors_1.label)(`${(0, labels_1.hudLabel)("compactions")}: ${ctx.transcript.compactionCount}`, colors));
401
+ }
402
+ trailingSegments(ctx, push);
403
+ let line = orderParts(parts, projectLineOrder).join(" | ");
404
+ const breakdown = tokenBreakdown(ctx, percent);
405
+ if (breakdown)
406
+ line += breakdown;
407
+ return line;
408
+ }
409
+ // ---------------------------------------------------------------- context / usage
410
+ function formatContextValue(ctx, percent) {
411
+ const mode = ctx.config.display.contextValue;
412
+ const total = (0, stdin_1.getTotalTokens)(ctx.payload);
413
+ const size = ctx.payload.context_window?.context_window_size ?? 0;
414
+ if (mode === "tokens")
415
+ return size > 0 ? `${(0, stdin_1.formatTokens)(total)}/${(0, stdin_1.formatTokens)(size)}` : (0, stdin_1.formatTokens)(total);
416
+ if (mode === "both")
417
+ return size > 0 ? `${percent}% (${(0, stdin_1.formatTokens)(total)}/${(0, stdin_1.formatTokens)(size)})` : `${percent}%`;
418
+ if (mode === "remaining")
419
+ return `${Math.max(0, 100 - percent)}%`;
420
+ return `${percent}%`;
421
+ }
422
+ function tokenBreakdown(ctx, percent) {
423
+ const { display, colors } = ctx.config;
424
+ if (!display.showTokenBreakdown || percent < display.contextCriticalThreshold)
425
+ return "";
426
+ const usage = ctx.payload.context_window?.current_usage;
427
+ if (!usage)
428
+ return "";
429
+ const input = (0, stdin_1.formatTokens)(usage.input_tokens ?? 0);
430
+ const cache = (0, stdin_1.formatTokens)((usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0));
431
+ return (0, colors_1.label)(` (${(0, labels_1.hudLabel)("in")}: ${input}, ${(0, labels_1.hudLabel)("cache")}: ${cache})`, colors);
432
+ }
433
+ function renderContextLine(ctx, width) {
434
+ const { display, colors } = ctx.config;
435
+ const percent = (0, stdin_1.getContextPercent)(ctx.payload);
436
+ const thresholds = { warning: display.contextWarningThreshold, critical: display.contextCriticalThreshold };
437
+ const value = `${(0, colors_1.contextColor)(percent, colors, thresholds)}${formatContextValue(ctx, percent)}${colors_1.RESET}`;
438
+ const head = (0, colors_1.label)((0, labels_1.hudLabel)("context"), colors);
439
+ const line = display.showContextBar
440
+ ? `${head} ${(0, colors_1.contextBar)(percent, (0, width_1.adaptiveBarWidth)(width), colors, thresholds)} ${value}`
441
+ : `${head} ${value}`;
442
+ return line + tokenBreakdown(ctx, percent);
443
+ }
444
+ function formatRelative(diffMs) {
445
+ const mins = Math.ceil(diffMs / 60_000);
446
+ if (mins < 60)
447
+ return `${mins}m`;
448
+ const hours = Math.floor(mins / 60);
449
+ if (hours >= 24) {
450
+ const days = Math.floor(hours / 24);
451
+ const rem = hours % 24;
452
+ return rem > 0 ? `${days}d ${rem}h` : `${days}d`;
453
+ }
454
+ const rem = mins % 60;
455
+ return rem > 0 ? `${hours}h ${rem}m` : `${hours}h`;
456
+ }
457
+ function formatAbsolute(at, now) {
458
+ const time = at.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
459
+ const prefix = (0, labels_1.hudLabel)("at");
460
+ const stamp = at.toDateString() === now.toDateString()
461
+ ? time
462
+ : `${at.toLocaleDateString([], { month: "short", day: "numeric" })} ${time}`;
463
+ return prefix ? `${prefix} ${stamp}` : stamp;
464
+ }
465
+ function formatResetTime(resetAt, mode, now) {
466
+ if (!resetAt)
467
+ return "";
468
+ const diff = resetAt.getTime() - now;
469
+ if (diff <= 0)
470
+ return "";
471
+ if (mode === "relative")
472
+ return formatRelative(diff);
473
+ const absolute = formatAbsolute(resetAt, new Date(now));
474
+ return mode === "absolute" ? absolute : `${formatRelative(diff)}, ${absolute}`;
475
+ }
476
+ function usagePercentText(ctx, percent) {
477
+ const { display, colors } = ctx.config;
478
+ if (percent === null)
479
+ return (0, colors_1.label)("--", colors);
480
+ const shown = display.usageValue === "remaining" ? Math.max(0, 100 - percent) : percent;
481
+ return `${(0, colors_1.quotaColor)(percent, colors)}${shown}%${colors_1.RESET}`;
482
+ }
483
+ function usageWindow(ctx, width, o) {
484
+ const { display, colors } = ctx.config;
485
+ const value = usagePercentText(ctx, o.percent);
486
+ const reset = formatResetTime(o.resetAt, display.timeFormat, ctx.now);
487
+ const resetsKey = display.timeFormat === "absolute" ? "resets" : "resetsIn";
488
+ const styled = (0, colors_1.label)(o.label, colors);
489
+ if (display.usageCompact) {
490
+ return reset ? `${(0, colors_1.label)(`${o.label}:`, colors)} ${value} ${(0, colors_1.label)(`(${reset})`, colors)}` : `${(0, colors_1.label)(`${o.label}:`, colors)} ${value}`;
491
+ }
492
+ const suffix = reset ? (display.showResetLabel ? ` (${(0, labels_1.hudLabel)(resetsKey)} ${reset})` : ` (${reset})`) : "";
493
+ if (display.usageBarEnabled) {
494
+ const body = `${(0, colors_1.quotaBar)(o.percent ?? 0, (0, width_1.adaptiveBarWidth)(width), colors)} ${value}${suffix}`;
495
+ return o.forceLabel ? `${styled} ${body}` : body;
496
+ }
497
+ return `${styled} ${value}${suffix}`;
498
+ }
499
+ /** usage 창들을 부분 문자열 배열로 — expanded는 한 줄로 잇고, compact 세션 줄은 개별 세그먼트로 쓴다. */
500
+ function usageParts(ctx, width) {
501
+ const { display, colors } = ctx.config;
502
+ if (!display.showUsage)
503
+ return [];
504
+ const usage = (0, stdin_1.getUsage)(ctx.payload);
505
+ if (!usage)
506
+ return [];
507
+ const usageHead = (0, colors_1.label)((0, labels_1.hudLabel)("usage"), colors);
508
+ const scoped = display.showModelScopedUsage ? usage.scopedWindows : [];
509
+ const scopedParts = scoped.map((w) => usageWindow(ctx, width, { label: w.label, percent: w.percent, resetAt: w.resetAt, durationLabel: "7d", forceLabel: true }));
510
+ if (usage.fiveHour === 100 || usage.sevenDay === 100) {
511
+ const resetAt = usage.fiveHour === 100 ? usage.fiveHourResetAt : usage.sevenDayResetAt;
512
+ const reset = formatResetTime(resetAt, display.timeFormat, ctx.now);
513
+ const resetsKey = display.timeFormat === "absolute" ? "resets" : "resetsIn";
514
+ const suffix = reset ? (display.showResetLabel && !display.usageCompact ? ` (${(0, labels_1.hudLabel)(resetsKey)} ${reset})` : ` (${reset})`) : "";
515
+ return [`${usageHead} ${(0, colors_1.critical)(`⚠ ${(0, labels_1.hudLabel)("limitReached")}${suffix}`, colors)}`, ...scopedParts];
516
+ }
517
+ const effective = Math.max(usage.fiveHour ?? 0, usage.sevenDay ?? 0, ...scoped.map((w) => w.percent ?? 0));
518
+ if (effective < display.usageThreshold)
519
+ return [];
520
+ const parts = [];
521
+ if (usage.fiveHour === null && usage.sevenDay !== null) {
522
+ parts.push(`${usageHead} ${usageWindow(ctx, width, { label: (0, labels_1.hudLabel)("weekly"), percent: usage.sevenDay, resetAt: usage.sevenDayResetAt, durationLabel: "7d", forceLabel: true })}`);
523
+ }
524
+ else if (usage.fiveHour !== null) {
525
+ parts.push(`${usageHead} ${usageWindow(ctx, width, { label: "5h", percent: usage.fiveHour, resetAt: usage.fiveHourResetAt, durationLabel: "5h", forceLabel: false })}`);
526
+ if (usage.sevenDay !== null && usage.sevenDay >= display.sevenDayThreshold) {
527
+ parts.push(usageWindow(ctx, width, { label: (0, labels_1.hudLabel)("weekly"), percent: usage.sevenDay, resetAt: usage.sevenDayResetAt, durationLabel: "7d", forceLabel: true }));
528
+ }
529
+ }
530
+ if (parts.length === 0 && scopedParts.length > 0) {
531
+ parts.push(`${usageHead} ${scopedParts[0]}`, ...scopedParts.slice(1));
532
+ }
533
+ else {
534
+ parts.push(...scopedParts);
535
+ }
536
+ return parts;
537
+ }
538
+ function renderUsageLine(ctx, width) {
539
+ const parts = usageParts(ctx, width);
540
+ return parts.length > 0 ? parts.join(" | ") : null;
541
+ }
542
+ /**
543
+ * 마지막 요청의 캐시 히트율 = cache_read / (input + cache_creation + cache_read).
544
+ * 캐시가 만료돼 새로 쓴 턴에서는 뚝 떨어지므로 세션 누적치보다 캐시 상태를 빨리 드러낸다.
545
+ */
546
+ function cacheHitRate(usage) {
547
+ if (!usage)
548
+ return null;
549
+ const total = usage.inputTokens + usage.cacheCreationTokens + usage.cacheReadTokens;
550
+ if (total <= 0)
551
+ return null;
552
+ return Math.round((usage.cacheReadTokens / total) * 100);
553
+ }
554
+ function cacheHitSegment(ctx) {
555
+ const { display, colors } = ctx.config;
556
+ if (!display.showCacheHitRate)
557
+ return "";
558
+ const rate = cacheHitRate(ctx.transcript.lastRequestUsage);
559
+ if (rate === null)
560
+ return "";
561
+ const value = `${rate}%`;
562
+ const colored = rate >= 80 ? (0, colors_1.green)(value) : rate >= 50 ? (0, colors_1.warning)(value, colors) : (0, colors_1.critical)(value, colors);
563
+ return ` ${(0, colors_1.label)("·", colors)} ${(0, colors_1.label)((0, labels_1.hudLabel)("hit"), colors)} ${colored}`;
564
+ }
565
+ /**
566
+ * prompt cache 만료 시각. 카운트다운이 아닌 시각인 이유: statusline은 Claude가 활동할 때만
567
+ * 다시 그려져서, 턴 사이(캐시가 실제로 빠지는 동안)에는 남은 시간이 멈춘 채 보이기 때문이다.
568
+ * 만료 전 TTL/5(최소 60초)부터 경고색, 지나면 "expired". 뒤에 마지막 요청의 히트율이 붙는다.
569
+ */
570
+ function renderPromptCacheLine(ctx) {
571
+ const { display, colors } = ctx.config;
572
+ const anchor = ctx.transcript.promptCacheAnchorAt;
573
+ if (anchor === undefined || !Number.isFinite(anchor))
574
+ return null;
575
+ const ttl = ctx.transcript.promptCacheTtlSeconds ?? display.promptCacheTtlSeconds;
576
+ const expiresAt = anchor + ttl * 1000;
577
+ const remaining = expiresAt - ctx.now;
578
+ const warnMs = Math.min(ttl, Math.max(60, Math.floor(ttl / 5))) * 1000;
579
+ const head = (0, colors_1.label)((0, labels_1.hudLabel)("promptCache"), colors);
580
+ const hit = cacheHitSegment(ctx);
581
+ if (remaining <= 0)
582
+ return `${head} ${(0, colors_1.label)(`⏱ ${(0, labels_1.hudLabel)("expired")}`, colors)}${hit}`;
583
+ const value = `⏱ ${(0, labels_1.hudLabel)("until")} ${formatAbsolute(new Date(expiresAt), new Date(ctx.now)).replace(/^at /, "")}`;
584
+ const thresholds = { warning: display.contextWarningThreshold, critical: display.contextCriticalThreshold };
585
+ const colored = remaining <= warnMs ? (0, colors_1.warning)(value, colors) : `${(0, colors_1.contextColor)(0, colors, thresholds)}${value}${colors_1.RESET}`;
586
+ return `${head} ${colored}${hit}`;
587
+ }
588
+ function renderMemoryLine(ctx, width) {
589
+ const { colors } = ctx.config;
590
+ const memory = ctx.memory;
591
+ if (!memory)
592
+ return null;
593
+ const percent = `${(0, colors_1.quotaColor)(memory.usedPercent, colors)}${memory.usedPercent}%${colors_1.RESET}`;
594
+ return `${(0, colors_1.label)((0, labels_1.hudLabel)("approxRam"), colors)} ${(0, colors_1.quotaBar)(memory.usedPercent, (0, width_1.adaptiveBarWidth)(width), colors)} ${(0, memory_1.formatBytes)(memory.usedBytes)} / ${(0, memory_1.formatBytes)(memory.totalBytes)} (${percent})`;
595
+ }
596
+ // ---------------------------------------------------------------- session totals
597
+ function sessionTokenSummary(tokens, prefix) {
598
+ if (!tokens)
599
+ return null;
600
+ const total = tokens.inputTokens + tokens.outputTokens + tokens.cacheCreationTokens + tokens.cacheReadTokens;
601
+ if (total === 0)
602
+ return null;
603
+ const parts = [`${(0, labels_1.hudLabel)("in")}: ${(0, stdin_1.formatTokens)(tokens.inputTokens)}`, `${(0, labels_1.hudLabel)("out")}: ${(0, stdin_1.formatTokens)(tokens.outputTokens)}`];
604
+ if (tokens.cacheCreationTokens > 0 || tokens.cacheReadTokens > 0) {
605
+ parts.push(`${(0, labels_1.hudLabel)("cache")}: ${(0, stdin_1.formatTokens)(tokens.cacheCreationTokens + tokens.cacheReadTokens)}`);
606
+ }
607
+ return `${prefix} ${(0, stdin_1.formatTokens)(total)} (${parts.join(", ")})`;
608
+ }
609
+ function renderSessionTokensLine(ctx) {
610
+ const { display, colors } = ctx.config;
611
+ if (!display.showSessionTokens)
612
+ return null;
613
+ const summary = sessionTokenSummary(ctx.transcript.sessionTokens, (0, labels_1.hudLabel)("tokens"));
614
+ return summary ? (0, colors_1.label)(summary, colors) : null;
615
+ }
616
+ function renderCompactionsLine(ctx) {
617
+ const { display, colors } = ctx.config;
618
+ const count = ctx.transcript.compactionCount ?? 0;
619
+ if (!display.showCompactions || count === 0)
620
+ return null;
621
+ return (0, colors_1.label)(`${(0, labels_1.hudLabel)("compactions")}: ${count}`, colors);
622
+ }
623
+ // ---------------------------------------------------------------- activity lines
624
+ function shortenToolName(name, maxLen) {
625
+ const display = /^mcp__.+__.+$/.test(name) ? (name.split("__").pop() ?? name) : name;
626
+ if (maxLen === 0 || display.length <= maxLen)
627
+ return display;
628
+ return `${display.slice(0, Math.max(0, maxLen - 1))}…`;
629
+ }
630
+ function truncatePath(target, maxLen = 20) {
631
+ const normalized = target.replace(/\\/g, "/");
632
+ if (normalized.length <= maxLen)
633
+ return normalized;
634
+ const base = path.posix.basename(normalized) || normalized;
635
+ return base.length >= maxLen ? `${base.slice(0, maxLen - 3)}...` : `.../${base}`;
636
+ }
637
+ function renderToolsLine(ctx) {
638
+ const { display, colors } = ctx.config;
639
+ // Skills 줄이 켜져 있으면 Skill 호출은 그쪽에서 보여주므로 도구 줄에서는 뺀다.
640
+ const tools = display.showSkills ? ctx.transcript.tools.filter((t) => t.name !== "Skill") : ctx.transcript.tools;
641
+ if (tools.length === 0)
642
+ return null;
643
+ const parts = [];
644
+ for (const tool of tools.filter((t) => t.status === "running").slice(-2)) {
645
+ const target = tool.target ? (0, colors_1.label)(`: ${truncatePath(tool.target)}`, colors) : "";
646
+ parts.push(`${(0, colors_1.yellow)("◐")} ${(0, colors_1.cyan)(shortenToolName(tool.name, display.toolNameMaxLength))}${target}`);
647
+ }
648
+ const counts = new Map();
649
+ for (const tool of tools) {
650
+ if (tool.status === "running")
651
+ continue;
652
+ counts.set(tool.name, (counts.get(tool.name) ?? 0) + 1);
653
+ }
654
+ const sorted = Array.from(counts.entries()).sort((a, b) => b[1] - a[1]);
655
+ const visible = display.toolsMaxVisible === 0 ? sorted : sorted.slice(0, display.toolsMaxVisible);
656
+ for (const [name, count] of visible) {
657
+ parts.push(`${(0, colors_1.green)("✓")} ${shortenToolName(name, display.toolNameMaxLength)} ${(0, colors_1.label)(`×${count}`, colors)}`);
658
+ }
659
+ const hidden = sorted.length - visible.length;
660
+ if (hidden > 0)
661
+ parts.push((0, colors_1.label)(`+${hidden} ${(0, labels_1.hudLabel)("more")}`, colors));
662
+ return parts.length > 0 ? parts.join(" | ") : null;
663
+ }
664
+ function renderSkillsLine(ctx) {
665
+ const { colors } = ctx.config;
666
+ const skills = ctx.transcript.skills;
667
+ if (skills.length === 0)
668
+ return null;
669
+ const visible = skills.slice(0, 4).map((name) => (0, colors_1.cyan)(name));
670
+ const hidden = skills.length - visible.length;
671
+ if (hidden > 0)
672
+ visible.push((0, colors_1.label)(`+${hidden} ${(0, labels_1.hudLabel)("more")}`, colors));
673
+ return `${(0, colors_1.green)("✓")} ${(0, labels_1.hudLabel)("skills")} ${(0, colors_1.label)(`(${skills.length})`, colors)}: ${visible.join(", ")}`;
674
+ }
675
+ /** "claude-opus-4-8[1m]" → "opus-4.8" 처럼 statusline에 맞게 줄인다. 모르는 형태는 그대로. */
676
+ function formatAgentModel(model) {
677
+ if (!model)
678
+ return undefined;
679
+ const candidate = model.replace(/\[[^\]]*\]$/, "");
680
+ const current = candidate.match(/^claude-(opus|sonnet|haiku|fable)-(\d+)(?:-(\d+))?(?:-\d{8})?$/i);
681
+ if (current)
682
+ return `${current[1].toLowerCase()}-${current[2]}${current[3] ? `.${current[3]}` : ""}`;
683
+ const legacy = candidate.match(/^claude-(\d+)(?:-(\d+))?-(opus|sonnet|haiku)(?:-\d{8})?$/i);
684
+ if (legacy)
685
+ return `${legacy[3].toLowerCase()}-${legacy[1]}${legacy[2] ? `.${legacy[2]}` : ""}`;
686
+ return model;
687
+ }
688
+ function formatElapsed(agent, now) {
689
+ const ms = Math.max(0, (agent.endTime ?? now) - agent.startTime);
690
+ if (ms < 1000)
691
+ return "<1s";
692
+ if (ms < 60_000)
693
+ return `${Math.round(ms / 1000)}s`;
694
+ const totalSecs = Math.floor(ms / 1000);
695
+ const mins = Math.floor(totalSecs / 60);
696
+ if (mins < 60)
697
+ return `${mins}m ${totalSecs % 60}s`;
698
+ return `${Math.floor(mins / 60)}h ${mins % 60}m`;
699
+ }
700
+ function renderAgentsLine(ctx) {
701
+ const { colors } = ctx.config;
702
+ const { agents } = ctx.transcript;
703
+ const running = agents.filter((a) => a.status === "running").slice(-3);
704
+ const completedSlots = Math.min(2, 3 - running.length);
705
+ const completed = agents
706
+ .filter((a) => a.status === "completed" && a.endTime !== undefined && a.endTime <= ctx.now && ctx.now - a.endTime <= COMPLETED_AGENT_RETENTION_MS)
707
+ .slice(-completedSlots);
708
+ const shown = [...running, ...(completedSlots > 0 ? completed : [])];
709
+ if (shown.length === 0)
710
+ return null;
711
+ return shown
712
+ .map((agent) => {
713
+ const icon = agent.status === "running" ? (0, colors_1.yellow)("◐") : (0, colors_1.green)("✓");
714
+ const model = formatAgentModel(agent.model);
715
+ const modelPart = model ? ` ${(0, colors_1.label)(`[${model}]`, colors)}` : "";
716
+ const desc = agent.description ? (0, colors_1.label)(`: ${agent.description}`, colors) : "";
717
+ return `${icon} ${(0, colors_1.magenta)(agent.type)}${modelPart}${desc} ${(0, colors_1.label)(`(${formatElapsed(agent, ctx.now)})`, colors)}`;
718
+ })
719
+ .join("\n");
720
+ }
721
+ function renderTodosLine(ctx) {
722
+ const { colors } = ctx.config;
723
+ const { todos } = ctx.transcript;
724
+ if (todos.length === 0)
725
+ return null;
726
+ const completed = todos.filter((t) => t.status === "completed").length;
727
+ const inProgress = todos.find((t) => t.status === "in_progress");
728
+ const progress = (0, colors_1.label)(`(${completed}/${todos.length})`, colors);
729
+ if (!inProgress) {
730
+ return completed === todos.length ? `${(0, colors_1.green)("✓")} ${(0, labels_1.hudLabel)("allTodosComplete")} ${progress}` : null;
731
+ }
732
+ const content = inProgress.content.length > 50 ? `${inProgress.content.slice(0, 47)}...` : inProgress.content;
733
+ return `${(0, colors_1.yellow)("▸")} ${content} ${progress}`;
734
+ }
735
+ // ---------------------------------------------------------------- git files line
736
+ function resolveWithinCwd(cwd, candidate) {
737
+ const resolved = path.resolve(cwd, candidate);
738
+ const relative = path.relative(path.resolve(cwd), resolved);
739
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)) ? resolved : null;
740
+ }
741
+ /** 마지막 줄: 최근 수정순 변경 파일 6개 (`~name(+a -d) +name -name +N more ?N`). 60칸 미만이면 숨긴다. */
742
+ function renderGitFilesLine(ctx, width) {
743
+ const { gitStatus: cfg } = ctx.config;
744
+ const stats = ctx.gitStatus?.fileStats;
745
+ if (!cfg.enabled || !cfg.showFileStats || !stats)
746
+ return null;
747
+ if (stats.trackedFiles.length === 0 && stats.untracked === 0)
748
+ return null;
749
+ if (width !== null && width < 60)
750
+ return null;
751
+ const cwd = currentDir(ctx);
752
+ const mtime = (file) => {
753
+ try {
754
+ const resolved = cwd ? resolveWithinCwd(cwd, file) : null;
755
+ return resolved ? fs.statSync(resolved).mtimeMs : 0;
756
+ }
757
+ catch {
758
+ return 0;
759
+ }
760
+ };
761
+ const sorted = [...stats.trackedFiles].sort((a, b) => mtime(b.fullPath) - mtime(a.fullPath));
762
+ const shown = sorted.slice(0, 6);
763
+ const parts = [];
764
+ for (const file of shown) {
765
+ const name = (0, transcript_1.sanitizeText)(file.basename, 64);
766
+ const prefix = file.type === "added" ? (0, colors_1.green)("+") : file.type === "deleted" ? (0, colors_1.red)("-") : (0, colors_1.yellow)("~");
767
+ const colored = file.type === "added" ? (0, colors_1.green)(name) : file.type === "deleted" ? (0, colors_1.red)(name) : (0, colors_1.yellow)(name);
768
+ const resolved = cwd ? resolveWithinCwd(cwd, file.fullPath) : null;
769
+ let entry = `${prefix}${resolved ? (0, width_1.safeHyperlink)(fileHref(resolved), colored) : colored}`;
770
+ if (file.lineDiff) {
771
+ const diff = [];
772
+ if (file.lineDiff.added > 0)
773
+ diff.push((0, colors_1.green)(`+${file.lineDiff.added}`));
774
+ if (file.lineDiff.deleted > 0)
775
+ diff.push((0, colors_1.red)(`-${file.lineDiff.deleted}`));
776
+ if (diff.length > 0)
777
+ entry += (0, colors_1.dim)(`(${diff.join(" ")})`);
778
+ }
779
+ parts.push(entry);
780
+ }
781
+ const overflow = sorted.length - shown.length;
782
+ if (overflow > 0)
783
+ parts.push((0, colors_1.dim)(`+${overflow} ${(0, labels_1.hudLabel)("more")}`));
784
+ if (stats.untracked > 0)
785
+ parts.push((0, colors_1.dim)(`?${stats.untracked}`));
786
+ return parts.join(" ");
787
+ }