@bastra-recall/statusline 0.9.2 → 1.0.1
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/dist/.build-revision +2 -0
- package/dist/browser.d.ts +100 -102
- package/dist/browser.js +3 -3
- package/dist/index.mjs +14 -14
- package/package.json +4 -4
- package/src/config/defaults.ts +5 -1
- package/src/powerline-layout.ts +174 -0
- package/src/powerline-segments.ts +374 -0
- package/src/powerline-theme.ts +323 -0
- package/src/powerline.ts +53 -835
- package/src/segments/git.ts +3 -3
- package/src/segments/pricing.ts +78 -1
- package/src/segments/renderer-bars.ts +83 -0
- package/src/segments/renderer-format.ts +89 -0
- package/src/segments/renderer-types.ts +171 -0
- package/src/segments/renderer.ts +80 -328
- package/src/segments/session.ts +29 -5
- package/src/segments/today.ts +11 -13
- package/src/tui/sections-bars.ts +215 -0
- package/src/tui/sections-parts.ts +604 -0
- package/src/tui/sections-shared.ts +23 -0
- package/src/tui/sections.ts +67 -819
- package/src/utils/budget.ts +1 -1
- package/src/utils/claude.ts +55 -16
- package/src/utils/formatters.ts +12 -5
- package/src/utils/terminal.ts +5 -5
package/src/segments/renderer.ts
CHANGED
|
@@ -6,7 +6,6 @@ import type { BlockInfo } from "./block";
|
|
|
6
6
|
import type { CacheTimerInfo } from "./cacheTimer";
|
|
7
7
|
import type {
|
|
8
8
|
UsageInfo,
|
|
9
|
-
TokenBreakdown,
|
|
10
9
|
GitInfo,
|
|
11
10
|
ContextInfo,
|
|
12
11
|
MetricsInfo,
|
|
@@ -17,10 +16,6 @@ import type { BastraInfo } from "./bastra";
|
|
|
17
16
|
import {
|
|
18
17
|
formatModelName,
|
|
19
18
|
abbreviateFishStyle,
|
|
20
|
-
formatCost,
|
|
21
|
-
formatTokens,
|
|
22
|
-
formatTokenCount,
|
|
23
|
-
formatTokenBreakdown,
|
|
24
19
|
formatTimeSince,
|
|
25
20
|
formatDuration,
|
|
26
21
|
formatLongTimeRemaining,
|
|
@@ -29,196 +24,67 @@ import {
|
|
|
29
24
|
collapseHome,
|
|
30
25
|
minutesUntilReset,
|
|
31
26
|
} from "../utils/formatters";
|
|
32
|
-
import { resolveBudgetDisplay } from "../utils/budget";
|
|
33
|
-
import type { BudgetItemConfig } from "../config/loader";
|
|
34
|
-
import { shouldShowIcon } from "../utils/icon-visibility";
|
|
35
|
-
|
|
36
|
-
export interface SegmentConfig {
|
|
37
|
-
enabled: boolean;
|
|
38
|
-
showIcon?: boolean;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface DirectorySegmentConfig extends SegmentConfig {
|
|
42
|
-
showBasename?: boolean;
|
|
43
|
-
style?: "full" | "fish" | "basename";
|
|
44
|
-
}
|
|
45
27
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
costSource?: "calculated" | "official";
|
|
61
|
-
/** Show the trailing "tokens" unit on token counts. Only affects `type: "tokens"` and `type: "both"` (default: true). Inert in the `tui` display style, which never renders the suffix. */
|
|
62
|
-
showUnits?: boolean;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface TmuxSegmentConfig extends SegmentConfig {}
|
|
66
|
-
|
|
67
|
-
export type BarDisplayStyle =
|
|
68
|
-
| "text"
|
|
69
|
-
| "ball"
|
|
70
|
-
| "bar"
|
|
71
|
-
| "blocks"
|
|
72
|
-
| "blocks-line"
|
|
73
|
-
| "capped"
|
|
74
|
-
| "dots"
|
|
75
|
-
| "filled"
|
|
76
|
-
| "geometric"
|
|
77
|
-
| "line"
|
|
78
|
-
| "squares";
|
|
79
|
-
|
|
80
|
-
export interface ContextSegmentConfig extends SegmentConfig {
|
|
81
|
-
showPercentageOnly?: boolean;
|
|
82
|
-
displayStyle?: BarDisplayStyle;
|
|
83
|
-
autocompactBuffer?: number;
|
|
84
|
-
percentageMode?: "remaining" | "used";
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export interface MetricsSegmentConfig extends SegmentConfig {
|
|
88
|
-
showResponseTime?: boolean;
|
|
89
|
-
showLastResponseTime?: boolean;
|
|
90
|
-
showDuration?: boolean;
|
|
91
|
-
showMessageCount?: boolean;
|
|
92
|
-
showLinesAdded?: boolean;
|
|
93
|
-
showLinesRemoved?: boolean;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export interface BlockSegmentConfig extends SegmentConfig {
|
|
97
|
-
type: "cost" | "tokens" | "both" | "time" | "weighted";
|
|
98
|
-
burnType?: "cost" | "tokens" | "both" | "none";
|
|
99
|
-
displayStyle?: BarDisplayStyle;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export interface TodaySegmentConfig extends SegmentConfig {
|
|
103
|
-
type: "cost" | "tokens" | "both" | "breakdown";
|
|
104
|
-
/** Show the trailing "tokens" unit on token counts. Only affects `type: "tokens"` and `type: "both"` (default: true). Inert in the `tui` display style, which never renders the suffix. */
|
|
105
|
-
showUnits?: boolean;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export interface VersionSegmentConfig extends SegmentConfig {}
|
|
109
|
-
|
|
110
|
-
export interface SessionIdSegmentConfig extends SegmentConfig {
|
|
111
|
-
showIdLabel?: boolean;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export interface EnvSegmentConfig extends SegmentConfig {
|
|
115
|
-
variable: string;
|
|
116
|
-
prefix?: string;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export interface WeeklySegmentConfig extends SegmentConfig {
|
|
120
|
-
displayStyle?: BarDisplayStyle;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export interface AgentSegmentConfig extends SegmentConfig {
|
|
124
|
-
showLabel?: boolean;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export interface ThinkingSegmentConfig extends SegmentConfig {
|
|
128
|
-
showEnabled?: boolean;
|
|
129
|
-
showEffort?: boolean;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export interface CacheTimerSegmentConfig extends SegmentConfig {
|
|
133
|
-
displayMode?: "elapsed" | "remaining";
|
|
134
|
-
ttlSeconds?: number;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export interface BastraSegmentConfig extends SegmentConfig {}
|
|
138
|
-
|
|
139
|
-
export type AnySegmentConfig =
|
|
140
|
-
| SegmentConfig
|
|
141
|
-
| DirectorySegmentConfig
|
|
142
|
-
| GitSegmentConfig
|
|
143
|
-
| UsageSegmentConfig
|
|
144
|
-
| TmuxSegmentConfig
|
|
145
|
-
| ContextSegmentConfig
|
|
146
|
-
| MetricsSegmentConfig
|
|
147
|
-
| BlockSegmentConfig
|
|
148
|
-
| TodaySegmentConfig
|
|
149
|
-
| VersionSegmentConfig
|
|
150
|
-
| SessionIdSegmentConfig
|
|
151
|
-
| EnvSegmentConfig
|
|
152
|
-
| WeeklySegmentConfig
|
|
153
|
-
| AgentSegmentConfig
|
|
154
|
-
| ThinkingSegmentConfig
|
|
155
|
-
| CacheTimerSegmentConfig
|
|
156
|
-
| BastraSegmentConfig;
|
|
157
|
-
|
|
158
|
-
export interface PowerlineSymbols {
|
|
159
|
-
right: string;
|
|
160
|
-
left: string;
|
|
161
|
-
branch: string;
|
|
162
|
-
model: string;
|
|
163
|
-
git_clean: string;
|
|
164
|
-
git_dirty: string;
|
|
165
|
-
git_conflicts: string;
|
|
166
|
-
git_ahead: string;
|
|
167
|
-
git_behind: string;
|
|
168
|
-
git_worktree: string;
|
|
169
|
-
git_tag: string;
|
|
170
|
-
git_sha: string;
|
|
171
|
-
git_upstream: string;
|
|
172
|
-
git_stash: string;
|
|
173
|
-
git_time: string;
|
|
174
|
-
session_cost: string;
|
|
175
|
-
block_cost: string;
|
|
176
|
-
today_cost: string;
|
|
177
|
-
context_time: string;
|
|
178
|
-
metrics_response: string;
|
|
179
|
-
metrics_last_response: string;
|
|
180
|
-
metrics_duration: string;
|
|
181
|
-
metrics_messages: string;
|
|
182
|
-
metrics_lines_added: string;
|
|
183
|
-
metrics_lines_removed: string;
|
|
184
|
-
metrics_burn: string;
|
|
185
|
-
version: string;
|
|
186
|
-
bar_filled: string;
|
|
187
|
-
bar_empty: string;
|
|
188
|
-
env: string;
|
|
189
|
-
session_id: string;
|
|
190
|
-
weekly_cost: string;
|
|
191
|
-
agent: string;
|
|
192
|
-
thinking: string;
|
|
193
|
-
cache_timer: string;
|
|
194
|
-
bastra: string;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export interface SegmentData {
|
|
198
|
-
text: string;
|
|
199
|
-
bgColor: string;
|
|
200
|
-
fgColor: string;
|
|
201
|
-
bold?: boolean;
|
|
202
|
-
}
|
|
28
|
+
// #1039: the segment config types, the bar drawing and the shared text
|
|
29
|
+
// formatting live in their own modules; this class keeps one render method per
|
|
30
|
+
// segment and delegates to them. The types are re-exported so every import
|
|
31
|
+
// from "./renderer" keeps working.
|
|
32
|
+
import {
|
|
33
|
+
plural,
|
|
34
|
+
getDisplayDirectoryName,
|
|
35
|
+
formatUsageWithBudget,
|
|
36
|
+
} from "./renderer-format";
|
|
37
|
+
import {
|
|
38
|
+
buildBar,
|
|
39
|
+
resolveBarStyleDef,
|
|
40
|
+
formatPercentageWithBar,
|
|
41
|
+
} from "./renderer-bars";
|
|
203
42
|
|
|
204
|
-
|
|
205
|
-
filled: string;
|
|
206
|
-
empty: string;
|
|
207
|
-
cap?: string;
|
|
208
|
-
marker?: string;
|
|
209
|
-
}
|
|
43
|
+
import { shouldShowIcon } from "../utils/icon-visibility";
|
|
210
44
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
45
|
+
import type {
|
|
46
|
+
SegmentConfig,
|
|
47
|
+
DirectorySegmentConfig,
|
|
48
|
+
GitSegmentConfig,
|
|
49
|
+
UsageSegmentConfig,
|
|
50
|
+
ContextSegmentConfig,
|
|
51
|
+
MetricsSegmentConfig,
|
|
52
|
+
BlockSegmentConfig,
|
|
53
|
+
TodaySegmentConfig,
|
|
54
|
+
VersionSegmentConfig,
|
|
55
|
+
SessionIdSegmentConfig,
|
|
56
|
+
EnvSegmentConfig,
|
|
57
|
+
WeeklySegmentConfig,
|
|
58
|
+
AgentSegmentConfig,
|
|
59
|
+
ThinkingSegmentConfig,
|
|
60
|
+
CacheTimerSegmentConfig,
|
|
61
|
+
BastraSegmentConfig,
|
|
62
|
+
PowerlineSymbols,
|
|
63
|
+
SegmentData,
|
|
64
|
+
} from "./renderer-types";
|
|
65
|
+
export type {
|
|
66
|
+
SegmentConfig,
|
|
67
|
+
DirectorySegmentConfig,
|
|
68
|
+
GitSegmentConfig,
|
|
69
|
+
UsageSegmentConfig,
|
|
70
|
+
TmuxSegmentConfig,
|
|
71
|
+
BarDisplayStyle,
|
|
72
|
+
ContextSegmentConfig,
|
|
73
|
+
MetricsSegmentConfig,
|
|
74
|
+
BlockSegmentConfig,
|
|
75
|
+
TodaySegmentConfig,
|
|
76
|
+
VersionSegmentConfig,
|
|
77
|
+
SessionIdSegmentConfig,
|
|
78
|
+
EnvSegmentConfig,
|
|
79
|
+
WeeklySegmentConfig,
|
|
80
|
+
AgentSegmentConfig,
|
|
81
|
+
ThinkingSegmentConfig,
|
|
82
|
+
CacheTimerSegmentConfig,
|
|
83
|
+
BastraSegmentConfig,
|
|
84
|
+
AnySegmentConfig,
|
|
85
|
+
PowerlineSymbols,
|
|
86
|
+
SegmentData,
|
|
87
|
+
} from "./renderer-types";
|
|
222
88
|
|
|
223
89
|
export class SegmentRenderer {
|
|
224
90
|
constructor(
|
|
@@ -261,7 +127,7 @@ export class SegmentRenderer {
|
|
|
261
127
|
? collapseHome(projectDir)
|
|
262
128
|
: projectDir;
|
|
263
129
|
|
|
264
|
-
let dirName =
|
|
130
|
+
let dirName = getDisplayDirectoryName(displayDir, displayProjectDir);
|
|
265
131
|
|
|
266
132
|
if (style === "fish") {
|
|
267
133
|
dirName = abbreviateFishStyle(dirName);
|
|
@@ -361,6 +227,8 @@ export class SegmentRenderer {
|
|
|
361
227
|
gitStatusIcon = this.symbols.git_conflicts;
|
|
362
228
|
} else if (gitInfo.status === "dirty") {
|
|
363
229
|
gitStatusIcon = this.symbols.git_dirty;
|
|
230
|
+
} else if (gitInfo.status === "unknown") {
|
|
231
|
+
gitStatusIcon = "?";
|
|
364
232
|
}
|
|
365
233
|
parts.push(gitStatusIcon);
|
|
366
234
|
|
|
@@ -397,11 +265,12 @@ export class SegmentRenderer {
|
|
|
397
265
|
|
|
398
266
|
const getCost = () => {
|
|
399
267
|
if (costSource === "calculated") return usageInfo.session.calculatedCost;
|
|
400
|
-
if (costSource === "official")
|
|
268
|
+
if (costSource === "official")
|
|
269
|
+
return usageInfo.session.officialCost ?? usageInfo.session.calculatedCost;
|
|
401
270
|
return usageInfo.session.cost;
|
|
402
271
|
};
|
|
403
272
|
|
|
404
|
-
const formattedUsage =
|
|
273
|
+
const formattedUsage = formatUsageWithBudget(
|
|
405
274
|
getCost(),
|
|
406
275
|
usageInfo.session.tokens,
|
|
407
276
|
usageInfo.session.tokenBreakdown,
|
|
@@ -467,12 +336,15 @@ export class SegmentRenderer {
|
|
|
467
336
|
const defaultMode = style === "text" ? "remaining" : "used";
|
|
468
337
|
const mode = config?.percentageMode ?? defaultMode;
|
|
469
338
|
|
|
470
|
-
const barStyleDef = this.
|
|
339
|
+
const barStyleDef = resolveBarStyleDef(this.symbols, style);
|
|
471
340
|
|
|
472
341
|
const emptyPct = mode === "remaining" ? "100%" : "0%";
|
|
473
342
|
if (!contextInfo) {
|
|
474
343
|
if (barStyleDef) {
|
|
475
|
-
const emptyBar =
|
|
344
|
+
const emptyBar =
|
|
345
|
+
mode === "remaining"
|
|
346
|
+
? buildBar(barStyleDef, barLength, 0, barLength)
|
|
347
|
+
: barStyleDef.empty.repeat(barLength);
|
|
476
348
|
return {
|
|
477
349
|
text: `${emptyBar} ${emptyPct}`,
|
|
478
350
|
bgColor: colors.contextBg,
|
|
@@ -505,12 +377,12 @@ export class SegmentRenderer {
|
|
|
505
377
|
? contextInfo.contextLeftPercentage
|
|
506
378
|
: contextInfo.usablePercentage;
|
|
507
379
|
const filledCount = Math.round(
|
|
508
|
-
(
|
|
380
|
+
(pct / 100) * barLength,
|
|
509
381
|
);
|
|
510
382
|
const emptyCount = barLength - filledCount;
|
|
511
383
|
|
|
512
384
|
if (barStyleDef) {
|
|
513
|
-
const bar =
|
|
385
|
+
const bar = buildBar(
|
|
514
386
|
barStyleDef,
|
|
515
387
|
filledCount,
|
|
516
388
|
emptyCount,
|
|
@@ -532,61 +404,6 @@ export class SegmentRenderer {
|
|
|
532
404
|
return { text, bgColor, fgColor, bold };
|
|
533
405
|
}
|
|
534
406
|
|
|
535
|
-
private buildBar(
|
|
536
|
-
s: BarStyleDef,
|
|
537
|
-
filledCount: number,
|
|
538
|
-
emptyCount: number,
|
|
539
|
-
barLength: number,
|
|
540
|
-
): string {
|
|
541
|
-
if (s.marker) {
|
|
542
|
-
const pos = Math.min(filledCount, barLength - 1);
|
|
543
|
-
return (
|
|
544
|
-
s.filled.repeat(pos) + s.marker + s.empty.repeat(barLength - pos - 1)
|
|
545
|
-
);
|
|
546
|
-
}
|
|
547
|
-
if (s.cap) {
|
|
548
|
-
if (filledCount === 0) {
|
|
549
|
-
return s.cap + s.empty.repeat(barLength - 1);
|
|
550
|
-
}
|
|
551
|
-
if (filledCount >= barLength) {
|
|
552
|
-
return s.filled.repeat(barLength);
|
|
553
|
-
}
|
|
554
|
-
return (
|
|
555
|
-
s.filled.repeat(filledCount - 1) + s.cap + s.empty.repeat(emptyCount)
|
|
556
|
-
);
|
|
557
|
-
}
|
|
558
|
-
return s.filled.repeat(filledCount) + s.empty.repeat(emptyCount);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
private resolveBarStyleDef(style: string): BarStyleDef | null {
|
|
562
|
-
return style === "bar"
|
|
563
|
-
? { filled: this.symbols.bar_filled, empty: this.symbols.bar_empty }
|
|
564
|
-
: (BAR_STYLES[style] ?? null);
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
private formatPercentageWithBar(
|
|
568
|
-
pct: number,
|
|
569
|
-
displayStyle?: BarDisplayStyle,
|
|
570
|
-
timeStr?: string | null,
|
|
571
|
-
): string {
|
|
572
|
-
const style = displayStyle ?? "text";
|
|
573
|
-
const barStyleDef = this.resolveBarStyleDef(style);
|
|
574
|
-
const barLength = 10;
|
|
575
|
-
|
|
576
|
-
if (barStyleDef) {
|
|
577
|
-
const filledCount = Math.round((pct / 100) * barLength);
|
|
578
|
-
const emptyCount = barLength - filledCount;
|
|
579
|
-
const bar = this.buildBar(
|
|
580
|
-
barStyleDef,
|
|
581
|
-
filledCount,
|
|
582
|
-
emptyCount,
|
|
583
|
-
barLength,
|
|
584
|
-
);
|
|
585
|
-
return timeStr ? `${bar} ${pct}% (${timeStr})` : `${bar} ${pct}%`;
|
|
586
|
-
}
|
|
587
|
-
return timeStr ? `${pct}% (${timeStr})` : `${pct}%`;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
407
|
renderMetrics(
|
|
591
408
|
metricsInfo: MetricsInfo | null,
|
|
592
409
|
colors: PowerlineColors,
|
|
@@ -697,7 +514,7 @@ export class SegmentRenderer {
|
|
|
697
514
|
}
|
|
698
515
|
|
|
699
516
|
return {
|
|
700
|
-
text: `${this.leadingIcon(this.symbols.block_cost, config)}${this.
|
|
517
|
+
text: `${this.leadingIcon(this.symbols.block_cost, config)}${formatPercentageWithBar(this.symbols, pct, config?.displayStyle, timeStr)}`,
|
|
701
518
|
bgColor,
|
|
702
519
|
fgColor,
|
|
703
520
|
bold,
|
|
@@ -731,7 +548,7 @@ export class SegmentRenderer {
|
|
|
731
548
|
}
|
|
732
549
|
|
|
733
550
|
return {
|
|
734
|
-
text: `${this.leadingIcon(this.symbols.weekly_cost, config)}${this.
|
|
551
|
+
text: `${this.leadingIcon(this.symbols.weekly_cost, config)}${formatPercentageWithBar(this.symbols, pct, config?.displayStyle, timeStr)}`,
|
|
735
552
|
bgColor,
|
|
736
553
|
fgColor,
|
|
737
554
|
bold,
|
|
@@ -749,7 +566,7 @@ export class SegmentRenderer {
|
|
|
749
566
|
: configOrType;
|
|
750
567
|
const type = config?.type ?? "cost";
|
|
751
568
|
const todayBudget = this.config.budget?.today;
|
|
752
|
-
const formattedUsage =
|
|
569
|
+
const formattedUsage = formatUsageWithBudget(
|
|
753
570
|
todayInfo.cost,
|
|
754
571
|
todayInfo.tokens,
|
|
755
572
|
todayInfo.tokenBreakdown,
|
|
@@ -769,72 +586,6 @@ export class SegmentRenderer {
|
|
|
769
586
|
};
|
|
770
587
|
}
|
|
771
588
|
|
|
772
|
-
private getDisplayDirectoryName(
|
|
773
|
-
currentDir: string,
|
|
774
|
-
projectDir?: string,
|
|
775
|
-
): string {
|
|
776
|
-
if (currentDir.startsWith("~")) {
|
|
777
|
-
return currentDir;
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
if (projectDir && projectDir !== currentDir) {
|
|
781
|
-
if (currentDir.startsWith(projectDir)) {
|
|
782
|
-
const relativePath = currentDir.slice(projectDir.length + 1);
|
|
783
|
-
return relativePath || projectDir.split(/[\\/]/).pop() || "project";
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
return currentDir;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
private formatUsageDisplay(
|
|
791
|
-
cost: number | null,
|
|
792
|
-
tokens: number | null,
|
|
793
|
-
tokenBreakdown: TokenBreakdown | null,
|
|
794
|
-
type: string,
|
|
795
|
-
showUnits: boolean,
|
|
796
|
-
): string {
|
|
797
|
-
const tokenStr = showUnits
|
|
798
|
-
? formatTokens(tokens)
|
|
799
|
-
: formatTokenCount(tokens);
|
|
800
|
-
switch (type) {
|
|
801
|
-
case "cost":
|
|
802
|
-
return formatCost(cost);
|
|
803
|
-
case "tokens":
|
|
804
|
-
return tokenStr;
|
|
805
|
-
case "both":
|
|
806
|
-
return `${formatCost(cost)} (${tokenStr})`;
|
|
807
|
-
case "breakdown":
|
|
808
|
-
return formatTokenBreakdown(tokenBreakdown);
|
|
809
|
-
default:
|
|
810
|
-
return formatCost(cost);
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
private formatUsageWithBudget(
|
|
815
|
-
cost: number | null,
|
|
816
|
-
tokens: number | null,
|
|
817
|
-
tokenBreakdown: TokenBreakdown | null,
|
|
818
|
-
type: string,
|
|
819
|
-
budget: BudgetItemConfig | undefined,
|
|
820
|
-
showUnits: boolean,
|
|
821
|
-
): string | null {
|
|
822
|
-
const state = resolveBudgetDisplay(cost, tokens, budget);
|
|
823
|
-
if (state.suppressAll) return null;
|
|
824
|
-
if (!state.showBase) return state.percentText;
|
|
825
|
-
|
|
826
|
-
const baseDisplay = this.formatUsageDisplay(
|
|
827
|
-
cost,
|
|
828
|
-
tokens,
|
|
829
|
-
tokenBreakdown,
|
|
830
|
-
type,
|
|
831
|
-
showUnits,
|
|
832
|
-
);
|
|
833
|
-
return state.percentText
|
|
834
|
-
? `${baseDisplay} ${state.percentText}`
|
|
835
|
-
: baseDisplay;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
589
|
renderVersion(
|
|
839
590
|
hookData: ClaudeHookData,
|
|
840
591
|
colors: PowerlineColors,
|
|
@@ -939,11 +690,12 @@ export class SegmentRenderer {
|
|
|
939
690
|
}
|
|
940
691
|
|
|
941
692
|
const text = `${iconPrefix}${formatCacheTimerElapsed(e)}`;
|
|
942
|
-
|
|
693
|
+
const ttl = config?.ttlSeconds ?? info.detectedTtlSeconds ?? 300;
|
|
694
|
+
if (e >= ttl) {
|
|
943
695
|
bgColor = colors.contextCriticalBg;
|
|
944
696
|
fgColor = colors.contextCriticalFg;
|
|
945
697
|
bold = colors.contextCriticalBold;
|
|
946
|
-
} else if (e >=
|
|
698
|
+
} else if (e >= ttl * 0.6) {
|
|
947
699
|
bgColor = colors.contextWarningBg;
|
|
948
700
|
fgColor = colors.contextWarningFg;
|
|
949
701
|
bold = colors.contextWarningBold;
|
|
@@ -973,7 +725,7 @@ export class SegmentRenderer {
|
|
|
973
725
|
// Prefer the human-readable banter phrase; fall back to the raw stage
|
|
974
726
|
// name (banter off, or older feed without current_message).
|
|
975
727
|
const label = info.currentMessage ?? info.currentStage;
|
|
976
|
-
text = `${icon}bastra · ${info.recallCount
|
|
728
|
+
text = `${icon}bastra · ${plural(info.recallCount, "call", "calls")} · ${plural(info.totalHits, "hit", "hits")} · ${live}ms · ${label}`;
|
|
977
729
|
} else {
|
|
978
730
|
// Between/after recalls in this turn — done snapshot. The running-state
|
|
979
731
|
// phrase is too brief (~120ms) for the ≥1s statusline refresh to catch,
|
|
@@ -987,9 +739,9 @@ export class SegmentRenderer {
|
|
|
987
739
|
const tail = showPhrase ? ` · ${info.lastPhrase}` : "";
|
|
988
740
|
// hits come from recalls only; ms from any tool call. Show each only
|
|
989
741
|
// when present, so load_memory-only turns read "N calls · Xms" (no hits).
|
|
990
|
-
const hits = info.totalHits > 0 ? ` · ${info.totalHits
|
|
742
|
+
const hits = info.totalHits > 0 ? ` · ${plural(info.totalHits, "hit", "hits")}` : "";
|
|
991
743
|
const ms = info.totalMs > 0 ? ` · ${info.totalMs}ms` : "";
|
|
992
|
-
text = `${icon}✓ bastra · ${info.recallCount
|
|
744
|
+
text = `${icon}✓ bastra · ${plural(info.recallCount, "call", "calls")}${hits}${ms}${tail}`;
|
|
993
745
|
}
|
|
994
746
|
|
|
995
747
|
return {
|
package/src/segments/session.ts
CHANGED
|
@@ -4,6 +4,9 @@ import {
|
|
|
4
4
|
findTranscriptFile,
|
|
5
5
|
findAgentTranscripts,
|
|
6
6
|
parseJsonlFile,
|
|
7
|
+
readJsonlFile,
|
|
8
|
+
canReadProjects,
|
|
9
|
+
createUniqueHash,
|
|
7
10
|
type ParsedEntry,
|
|
8
11
|
type ClaudeHookData,
|
|
9
12
|
} from "../utils/claude";
|
|
@@ -68,12 +71,14 @@ export class SessionProvider {
|
|
|
68
71
|
const transcriptPath = await findTranscriptFile(sessionId);
|
|
69
72
|
if (!transcriptPath) {
|
|
70
73
|
debug(`No transcript found for session: ${sessionId}`);
|
|
71
|
-
|
|
74
|
+
// A session that has not written its transcript yet has spent nothing;
|
|
75
|
+
// without a readable projects directory the usage is unknown.
|
|
76
|
+
return (await canReadProjects()) ? { totalCost: 0, entries: [] } : null;
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
debug(`Found transcript at: ${transcriptPath}`);
|
|
75
80
|
|
|
76
|
-
const parsedEntries = await
|
|
81
|
+
const parsedEntries = await readJsonlFile(transcriptPath);
|
|
77
82
|
const projectPath = dirname(transcriptPath);
|
|
78
83
|
const agentTranscripts = await findAgentTranscripts(
|
|
79
84
|
sessionId,
|
|
@@ -91,10 +96,25 @@ export class SessionProvider {
|
|
|
91
96
|
return { totalCost: 0, entries: [] };
|
|
92
97
|
}
|
|
93
98
|
|
|
99
|
+
const seenHashes = new Set<string>();
|
|
100
|
+
const dedupedEntries = parsedEntries.filter((entry) => {
|
|
101
|
+
const hash = createUniqueHash(entry);
|
|
102
|
+
if (!hash) return true;
|
|
103
|
+
if (seenHashes.has(hash)) return false;
|
|
104
|
+
seenHashes.add(hash);
|
|
105
|
+
return true;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (dedupedEntries.length !== parsedEntries.length) {
|
|
109
|
+
debug(
|
|
110
|
+
`Session usage: dropped ${parsedEntries.length - dedupedEntries.length} duplicate entries (retries/reconnects logged twice)`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
94
114
|
const entries: SessionUsageEntry[] = [];
|
|
95
115
|
let totalCost = 0;
|
|
96
116
|
|
|
97
|
-
for (const entry of
|
|
117
|
+
for (const entry of dedupedEntries) {
|
|
98
118
|
if (entry.message?.usage) {
|
|
99
119
|
const sessionEntry = convertToSessionEntry(entry);
|
|
100
120
|
|
|
@@ -142,7 +162,7 @@ export class SessionProvider {
|
|
|
142
162
|
): Promise<SessionInfo> {
|
|
143
163
|
const sessionUsage = await this.getSessionUsage(sessionId);
|
|
144
164
|
|
|
145
|
-
if (!sessionUsage
|
|
165
|
+
if (!sessionUsage) {
|
|
146
166
|
return {
|
|
147
167
|
cost: null,
|
|
148
168
|
calculatedCost: null,
|
|
@@ -161,7 +181,11 @@ export class SessionProvider {
|
|
|
161
181
|
|
|
162
182
|
const calculatedCost = sessionUsage.totalCost;
|
|
163
183
|
const hookDataCost = hookData?.cost?.total_cost_usd ?? null;
|
|
164
|
-
|
|
184
|
+
// Prefer Claude Code's own authoritative total_cost_usd (billed, not guessed
|
|
185
|
+
// from a client-side price list) whenever the hook provides it; the local
|
|
186
|
+
// per-token recompute is a fallback for older Claude Code versions that
|
|
187
|
+
// don't send `cost` at all.
|
|
188
|
+
const cost = hookDataCost ?? calculatedCost;
|
|
165
189
|
|
|
166
190
|
return {
|
|
167
191
|
cost,
|
package/src/segments/today.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { TokenBreakdown } from "./session";
|
|
|
4
4
|
import { debug } from "../utils/logger";
|
|
5
5
|
import { PricingService } from "./pricing";
|
|
6
6
|
import { CacheManager } from "../utils/cache";
|
|
7
|
-
import { loadEntriesFromProjects } from "../utils/claude";
|
|
7
|
+
import { loadEntriesFromProjects, canReadProjects } from "../utils/claude";
|
|
8
8
|
|
|
9
9
|
export interface TodayUsageEntry {
|
|
10
10
|
timestamp: Date;
|
|
@@ -69,7 +69,12 @@ export class TodayProvider {
|
|
|
69
69
|
"today",
|
|
70
70
|
latestMtime,
|
|
71
71
|
)) as TodayUsageEntry[] | null;
|
|
72
|
-
|
|
72
|
+
// The disk cache is keyed on transcript mtime only, so after midnight it
|
|
73
|
+
// still holds yesterday's entries until a transcript is touched.
|
|
74
|
+
if (
|
|
75
|
+
sharedCached &&
|
|
76
|
+
sharedCached.every((e) => formatDate(e.timestamp) === todayDateString)
|
|
77
|
+
) {
|
|
73
78
|
debug("Using shared today usage cache");
|
|
74
79
|
return sharedCached;
|
|
75
80
|
}
|
|
@@ -124,20 +129,13 @@ export class TodayProvider {
|
|
|
124
129
|
return todayEntries;
|
|
125
130
|
}
|
|
126
131
|
|
|
127
|
-
private async getTodayEntries(): Promise<TodayUsageEntry[]> {
|
|
128
|
-
try {
|
|
129
|
-
return await this.loadTodayEntries();
|
|
130
|
-
} catch (error) {
|
|
131
|
-
debug("Error loading today's entries:", error);
|
|
132
|
-
return [];
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
132
|
async getTodayInfo(): Promise<TodayInfo> {
|
|
137
133
|
try {
|
|
138
|
-
const entries = await this.
|
|
134
|
+
const entries = await this.loadTodayEntries();
|
|
139
135
|
|
|
140
|
-
|
|
136
|
+
// No entries is a measured zero only while the projects directory can
|
|
137
|
+
// be read; otherwise today's usage is unknown.
|
|
138
|
+
if (entries.length === 0 && !(await canReadProjects())) {
|
|
141
139
|
return {
|
|
142
140
|
cost: null,
|
|
143
141
|
tokens: null,
|