@bastra-recall/statusline 1.0.0 → 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.
@@ -3,280 +3,77 @@ import type { PowerlineColors } from "../themes";
3
3
  import type {
4
4
  TuiData,
5
5
  SymbolSet,
6
- BoxChars,
7
6
  RenderCtx,
8
7
  SegmentTemplate,
9
8
  JustifyValue,
10
- TuiTitleConfig,
11
9
  } from "./types";
12
10
  import { visibleLength } from "../utils/terminal";
13
11
 
14
12
  import {
15
- formatCost,
16
- formatTokenCount,
17
- collapseHome,
18
13
  formatDuration,
19
14
  formatModelName,
20
15
  formatResponseTime,
21
- formatTimeRemaining,
22
- formatLongTimeRemaining,
23
- minutesUntilReset,
24
16
  abbreviateFishStyle,
25
- formatCacheTimerElapsed,
26
17
  } from "../utils/formatters";
27
- import { resolveBudgetDisplay } from "../utils/budget";
28
- import { colorize, truncateAnsi } from "./primitives";
29
- import { getEffortLevel, getThinkingEnabled } from "../utils/claude";
18
+ import { colorize } from "./primitives";
30
19
  import { resolveIconVisibility } from "../utils/icon-visibility";
31
-
32
- export function resolveTitleToken(
33
- template: string,
34
- data: TuiData,
35
- resolvedData?: Record<string, string>,
36
- ): string {
37
- const rawName = data.hookData.model?.display_name || "Claude";
38
- const modelName = formatModelName(rawName).toLowerCase();
39
-
40
- return template.replace(/\{([^}]+)\}/g, (_match, token: string) => {
41
- if (resolvedData) {
42
- const value = resolvedData[token];
43
- if (value !== undefined) return value;
44
- }
45
- if (token === "model") return modelName;
46
- return "";
47
- });
48
- }
49
-
50
- export function buildTitleBar(
51
- data: TuiData,
52
- box: BoxChars,
53
- innerWidth: number,
54
- titleConfig?: TuiTitleConfig,
55
- resolvedData?: Record<string, string>,
56
- ): string {
57
- const leftTemplate = titleConfig?.left ?? "{model}";
58
- const rightTemplate = titleConfig?.right;
59
- const leftResolved = resolveTitleToken(leftTemplate, data, resolvedData);
60
- const leftText = leftResolved ? ` ${leftResolved} ` : "";
61
- const leftLen = visibleLength(leftText);
62
-
63
- if (!rightTemplate) {
64
- const simpleFill = innerWidth - leftLen;
65
- return (
66
- box.topLeft +
67
- leftText +
68
- box.horizontal.repeat(Math.max(0, simpleFill)) +
69
- box.topRight
70
- );
71
- }
72
-
73
- const rightResolved = resolveTitleToken(rightTemplate, data, resolvedData);
74
- const rightText = rightResolved ? ` ${rightResolved} ` : "";
75
- const rightLen = visibleLength(rightText);
76
-
77
- // Truncate if combined text exceeds innerWidth
78
- let finalLeft = leftText;
79
- let finalLeftLen = leftLen;
80
- let finalRight = rightText;
81
- let finalRightLen = rightLen;
82
-
83
- if (finalLeftLen + finalRightLen > innerWidth) {
84
- const maxLeft = Math.max(0, innerWidth - finalRightLen);
85
- if (finalLeftLen > maxLeft) {
86
- finalLeft = truncateAnsi(finalLeft, maxLeft);
87
- finalLeftLen = visibleLength(finalLeft);
88
- }
89
- if (finalLeftLen + finalRightLen > innerWidth) {
90
- const maxRight = Math.max(0, innerWidth - finalLeftLen);
91
- finalRight = truncateAnsi(finalRight, maxRight);
92
- finalRightLen = visibleLength(finalRight);
93
- }
94
- }
95
-
96
- const fillCount = innerWidth - finalLeftLen - finalRightLen;
97
-
98
- if (fillCount < 2) {
99
- const simpleFill = innerWidth - finalLeftLen;
100
- return (
101
- box.topLeft +
102
- finalLeft +
103
- box.horizontal.repeat(Math.max(0, simpleFill)) +
104
- box.topRight
105
- );
106
- }
107
-
108
- return (
109
- box.topLeft +
110
- finalLeft +
111
- box.horizontal.repeat(fillCount) +
112
- finalRight +
113
- box.topRight
114
- );
115
- }
116
-
117
- function resolveThresholdStyle(
118
- pct: number,
119
- defaultFg: string,
120
- defaultBold: boolean,
121
- colors: PowerlineColors,
122
- warningAt = 60,
123
- criticalAt = 80,
124
- ): { fg: string; bold: boolean } {
125
- if (pct >= criticalAt)
126
- return { fg: colors.contextCriticalFg, bold: colors.contextCriticalBold };
127
- if (pct >= warningAt)
128
- return { fg: colors.contextWarningFg, bold: colors.contextWarningBold };
129
- return { fg: defaultFg, bold: defaultBold };
130
- }
131
-
132
- function buildBarString(
133
- pct: number,
134
- barWidth: number,
135
- sym: SymbolSet,
136
- reset: string,
137
- fgColor: string,
138
- bold = false,
139
- ): string {
140
- barWidth = Math.max(5, barWidth);
141
- const filledCount = Math.max(
142
- 0,
143
- Math.min(barWidth, Math.round((pct / 100) * barWidth)),
144
- );
145
- const emptyCount = barWidth - filledCount;
146
- const bar =
147
- sym.bar_filled.repeat(filledCount) + sym.bar_empty.repeat(emptyCount);
148
- return colorize(bar, fgColor, reset, bold);
149
- }
150
-
151
- export function formatContextParts(
152
- data: TuiData,
153
- sym: SymbolSet,
154
- iconVisible = true,
155
- ): Record<string, string> {
156
- if (!data.contextInfo)
157
- return { icon: "", label: "context", bar: "", pct: "", tokens: "" };
158
-
159
- const usedPct = data.contextInfo.usablePercentage;
160
- const tokenStr = formatTokenCount(data.contextInfo.totalTokens);
161
- const maxStr = formatTokenCount(data.contextInfo.maxTokens);
162
-
163
- return {
164
- icon: iconVisible ? sym.context_time : "",
165
- label: "context",
166
- bar: " ",
167
- pct: `${usedPct}%`,
168
- tokens: `${tokenStr}/${maxStr}`,
169
- };
170
- }
171
-
172
- export function buildContextBar(
173
- data: TuiData,
174
- barWidth: number,
175
- sym: SymbolSet,
176
- reset: string,
177
- colors: PowerlineColors,
178
- partFg?: Record<string, string>,
179
- ): string {
180
- if (!data.contextInfo) return "";
181
- const usedPct = data.contextInfo.usablePercentage;
182
- const defaultFg =
183
- partFg?.["context.bar"] ?? partFg?.["context"] ?? colors.contextFg;
184
- const { fg, bold } = resolveThresholdStyle(
185
- usedPct,
186
- defaultFg,
187
- colors.contextBold,
188
- colors,
189
- );
190
- return buildBarString(usedPct, barWidth, sym, reset, fg, bold);
191
- }
192
-
193
- export function buildBlockBar(
194
- data: TuiData,
195
- barWidth: number,
196
- sym: SymbolSet,
197
- reset: string,
198
- colors: PowerlineColors,
199
- config: PowerlineConfig,
200
- partFg?: Record<string, string>,
201
- ): string {
202
- if (!data.blockInfo) return "";
203
-
204
- const pct = data.blockInfo.nativeUtilization;
205
- const warningThreshold = config.budget?.block?.warningThreshold ?? 80;
206
- const defaultFg =
207
- partFg?.["block.bar"] ?? partFg?.["block"] ?? colors.blockFg;
208
- const { fg, bold } = resolveThresholdStyle(
209
- pct,
210
- defaultFg,
211
- colors.blockBold,
212
- colors,
213
- 50,
214
- warningThreshold,
215
- );
216
- return buildBarString(pct, barWidth, sym, reset, fg, bold);
217
- }
218
-
219
- export function buildWeeklyBar(
220
- data: TuiData,
221
- barWidth: number,
222
- sym: SymbolSet,
223
- reset: string,
224
- colors: PowerlineColors,
225
- partFg?: Record<string, string>,
226
- ): string {
227
- const sevenDay = data.hookData.rate_limits?.seven_day;
228
- if (!sevenDay) return "";
229
-
230
- const pct = sevenDay.used_percentage;
231
- const defaultFg =
232
- partFg?.["weekly.bar"] ?? partFg?.["weekly"] ?? colors.weeklyFg;
233
- const { fg, bold } = resolveThresholdStyle(
234
- pct,
235
- defaultFg,
236
- colors.weeklyBold,
237
- colors,
238
- );
239
- return buildBarString(pct, barWidth, sym, reset, fg, bold);
240
- }
241
-
242
- export function buildContextLine(
243
- data: TuiData,
244
- contentWidth: number,
245
- sym: SymbolSet,
246
- reset: string,
247
- colors: PowerlineColors,
248
- ): string | null {
249
- if (!data.contextInfo) {
250
- return null;
251
- }
252
-
253
- const usedPct = data.contextInfo.usablePercentage;
254
- const tokenStr = formatTokenCount(data.contextInfo.totalTokens);
255
- const maxStr = formatTokenCount(data.contextInfo.maxTokens);
256
- const suffix = ` ${usedPct}% ${tokenStr}/${maxStr}`;
257
- const barLen = Math.max(5, contentWidth - suffix.length);
258
- const filledCount = Math.max(
259
- 0,
260
- Math.min(barLen, Math.round((usedPct / 100) * barLen)),
261
- );
262
- const emptyCount = barLen - filledCount;
263
- const bar =
264
- sym.bar_filled.repeat(filledCount) + sym.bar_empty.repeat(emptyCount);
265
-
266
- const { fg, bold } = resolveThresholdStyle(
267
- usedPct,
268
- colors.contextFg,
269
- colors.contextBold,
270
- colors,
271
- );
272
-
273
- return colorize(`${bar}${suffix}`, fg, reset, bold);
274
- }
275
-
276
- function getDirectoryDisplay(hookData: TuiData["hookData"]): string {
277
- const currentDir = hookData.workspace?.current_dir || hookData.cwd || "/";
278
- return collapseHome(currentDir);
279
- }
20
+ import { resolveThresholdStyle, getDirectoryDisplay } from "./sections-shared";
21
+ import { buildContextLine } from "./sections-bars";
22
+ import {
23
+ formatContextParts,
24
+ formatBlockParts,
25
+ formatBlockSegment,
26
+ formatWeeklyParts,
27
+ formatWeeklySegment,
28
+ formatSessionParts,
29
+ formatSessionSegment,
30
+ formatTodayParts,
31
+ formatTodaySegment,
32
+ formatMetricsParts,
33
+ formatMetricsSegment,
34
+ formatActivityParts,
35
+ formatActivitySegment,
36
+ formatGitParts,
37
+ formatGitSegment,
38
+ formatDirParts,
39
+ formatDirValue,
40
+ formatVersionParts,
41
+ formatVersionSegment,
42
+ formatAgentParts,
43
+ formatAgentSegment,
44
+ formatThinkingParts,
45
+ formatThinkingSegment,
46
+ formatCacheTimerParts,
47
+ formatCacheTimerSegment,
48
+ cacheTimerTtl,
49
+ cacheTimerStyle,
50
+ formatTmuxParts,
51
+ formatTmuxSegment,
52
+ formatEnvParts,
53
+ formatEnvSegment,
54
+ } from "./sections-parts";
55
+
56
+ // #680: bar renderers live in sections-bars.ts, part formatters in
57
+ // sections-parts.ts; this module keeps the public surface and re-exports them.
58
+ export {
59
+ resolveTitleToken,
60
+ buildTitleBar,
61
+ buildContextBar,
62
+ buildBlockBar,
63
+ buildWeeklyBar,
64
+ buildContextLine,
65
+ } from "./sections-bars";
66
+ export {
67
+ formatContextParts,
68
+ formatBlockParts,
69
+ formatBlockSegment,
70
+ formatWeeklyParts,
71
+ formatWeeklySegment,
72
+ formatSessionParts,
73
+ formatSessionSegment,
74
+ formatTodayParts,
75
+ formatTodaySegment,
76
+ } from "./sections-parts";
280
77
 
281
78
  export function collectMetricSegments(
282
79
  data: TuiData,
@@ -450,6 +247,7 @@ export function collectFooterParts(
450
247
  const { fg, bold } = cacheTimerStyle(
451
248
  data.cacheTimerInfo.elapsedSeconds,
452
249
  colors,
250
+ cacheTimerTtl(config, data),
453
251
  );
454
252
  parts.push(colorize(cacheTimerText, fg, reset, bold));
455
253
  }
@@ -547,565 +345,11 @@ function formatBastraText(data: TuiData): string | null {
547
345
  info.currentRecallStartedAt !== null
548
346
  ? info.totalMs + Math.max(0, Date.now() - info.currentRecallStartedAt)
549
347
  : info.totalMs;
550
- return `bastra · ${info.recallCount} calls · ${info.totalHits} hits · ${live}ms · ${info.currentStage}`;
348
+ return `bastra · ${info.recallCount} ${info.recallCount === 1 ? "call" : "calls"} · ${info.totalHits} ${info.totalHits === 1 ? "hit" : "hits"} · ${live}ms · ${info.currentStage}`;
551
349
  }
552
- const hits = info.totalHits > 0 ? ` · ${info.totalHits} hits` : "";
350
+ const hits = info.totalHits > 0 ? ` · ${info.totalHits} ${info.totalHits === 1 ? "hit" : "hits"}` : "";
553
351
  const ms = info.totalMs > 0 ? ` · ${info.totalMs}ms` : "";
554
- return `✓ bastra · ${info.recallCount} calls${hits}${ms}`;
555
- }
556
-
557
- export function formatBlockParts(
558
- blockInfo: TuiData["blockInfo"] & {},
559
- sym: SymbolSet,
560
- _config: PowerlineConfig,
561
- iconVisible = true,
562
- ): Record<string, string> {
563
- const value = `${Math.round(blockInfo.nativeUtilization)}%`;
564
- const time = formatTimeRemaining(blockInfo.timeRemaining);
565
-
566
- return {
567
- icon: iconVisible ? sym.block_cost : "",
568
- label: "block",
569
- value,
570
- time,
571
- budget: "",
572
- bar: " ",
573
- };
574
- }
575
-
576
- export function formatBlockSegment(
577
- blockInfo: TuiData["blockInfo"] & {},
578
- sym: SymbolSet,
579
- config: PowerlineConfig,
580
- iconVisible = true,
581
- ): string {
582
- const parts = formatBlockParts(blockInfo, sym, config, iconVisible);
583
- let text = parts.icon ? `${parts.icon} ${parts.value}` : (parts.value ?? "");
584
- if (parts.time) text += ` · ${parts.time}`;
585
- if (parts.budget) text += parts.budget;
586
- return text;
587
- }
588
-
589
- export function formatWeeklyParts(
590
- sevenDay: { used_percentage: number; resets_at: number },
591
- sym: SymbolSet,
592
- iconVisible = true,
593
- ): Record<string, string> {
594
- const pct = `${Math.round(sevenDay.used_percentage)}%`;
595
- const time = formatLongTimeRemaining(minutesUntilReset(sevenDay.resets_at));
596
- return {
597
- icon: iconVisible ? sym.weekly_cost : "",
598
- label: "weekly",
599
- pct,
600
- time,
601
- bar: " ",
602
- };
603
- }
604
-
605
- export function formatWeeklySegment(
606
- sevenDay: { used_percentage: number; resets_at: number },
607
- sym: SymbolSet,
608
- iconVisible = true,
609
- ): string {
610
- const parts = formatWeeklyParts(sevenDay, sym, iconVisible);
611
- let text = parts.icon ? `${parts.icon} ${parts.pct}` : (parts.pct ?? "");
612
- if (parts.time) text += ` · ${parts.time}`;
613
- return text;
614
- }
615
-
616
- export function formatSessionParts(
617
- usageInfo: TuiData["usageInfo"] & {},
618
- sym: SymbolSet,
619
- config: PowerlineConfig,
620
- iconVisible = true,
621
- ): Record<string, string> {
622
- const state = resolveBudgetDisplay(
623
- usageInfo.session.cost,
624
- usageInfo.session.tokens,
625
- config.budget?.session,
626
- );
627
-
628
- if (state.suppressAll) {
629
- return { icon: "", label: "", cost: "", tokens: "", budget: "" };
630
- }
631
-
632
- const sessionTokens = usageInfo.session.tokens;
633
- const tokenStr =
634
- state.showBase && sessionTokens !== null && sessionTokens > 0
635
- ? formatTokenCount(sessionTokens)
636
- : "";
637
-
638
- return {
639
- icon: iconVisible ? sym.session_cost : "",
640
- label: state.percentageOnly ? "" : "session",
641
- cost: state.showBase ? formatCost(usageInfo.session.cost) : "",
642
- tokens: tokenStr,
643
- budget: state.percentText ? ` ${state.percentText}` : "",
644
- };
645
- }
646
-
647
- export function formatSessionSegment(
648
- usageInfo: TuiData["usageInfo"] & {},
649
- sym: SymbolSet,
650
- config: PowerlineConfig,
651
- iconVisible = true,
652
- ): string {
653
- const state = resolveBudgetDisplay(
654
- usageInfo.session.cost,
655
- usageInfo.session.tokens,
656
- config.budget?.session,
657
- );
658
- if (state.suppressAll) return "";
659
-
660
- const icon = iconVisible ? sym.session_cost : "";
661
-
662
- if (!state.showBase) {
663
- return icon ? `${icon} ${state.percentText}` : state.percentText;
664
- }
665
-
666
- const costStr = formatCost(usageInfo.session.cost);
667
- const sessionTokens = usageInfo.session.tokens;
668
- let text = icon ? `${icon} ${costStr}` : costStr;
669
- if (sessionTokens !== null && sessionTokens > 0) {
670
- text += ` · ${formatTokenCount(sessionTokens)}`;
671
- }
672
- if (state.percentText) text += ` ${state.percentText}`;
673
- return text;
674
- }
675
-
676
- export function formatTodayParts(
677
- todayInfo: TuiData["todayInfo"] & {},
678
- sym: SymbolSet,
679
- config: PowerlineConfig,
680
- iconVisible = true,
681
- ): Record<string, string> {
682
- const state = resolveBudgetDisplay(
683
- todayInfo.cost,
684
- todayInfo.tokens,
685
- config.budget?.today,
686
- );
687
-
688
- if (state.suppressAll) {
689
- return { icon: "", label: "", cost: "", budget: "" };
690
- }
691
-
692
- return {
693
- icon: iconVisible ? sym.today_cost : "",
694
- cost: state.showBase ? formatCost(todayInfo.cost) : "",
695
- label: state.percentageOnly ? "" : "today",
696
- budget: state.percentText ? ` ${state.percentText}` : "",
697
- };
698
- }
699
-
700
- export function formatTodaySegment(
701
- todayInfo: TuiData["todayInfo"] & {},
702
- sym: SymbolSet,
703
- config: PowerlineConfig,
704
- iconVisible = true,
705
- ): string {
706
- const state = resolveBudgetDisplay(
707
- todayInfo.cost,
708
- todayInfo.tokens,
709
- config.budget?.today,
710
- );
711
- if (state.suppressAll) return "";
712
-
713
- const icon = iconVisible ? sym.today_cost : "";
714
-
715
- if (!state.showBase) {
716
- return icon ? `${icon} ${state.percentText}` : state.percentText;
717
- }
718
-
719
- const costStr = formatCost(todayInfo.cost);
720
- let text = icon ? `${icon} ${costStr} today` : `${costStr} today`;
721
- if (state.percentText) text += ` ${state.percentText}`;
722
- return text;
723
- }
724
-
725
- function formatMetricsParts(
726
- data: TuiData,
727
- sym: SymbolSet,
728
- ): Record<string, string> {
729
- const empty = {
730
- response: "",
731
- responseIcon: "",
732
- responseVal: "",
733
- lastResponse: "",
734
- lastResponseIcon: "",
735
- lastResponseVal: "",
736
- added: "",
737
- addedIcon: "",
738
- addedVal: "",
739
- removed: "",
740
- removedIcon: "",
741
- removedVal: "",
742
- };
743
- if (!data.metricsInfo) return empty;
744
-
745
- const hasResponse =
746
- data.metricsInfo.responseTime !== null &&
747
- !isNaN(data.metricsInfo.responseTime) &&
748
- data.metricsInfo.responseTime > 0;
749
- const responseValStr = hasResponse
750
- ? formatResponseTime(data.metricsInfo.responseTime!)
751
- : "";
752
-
753
- const hasLast =
754
- data.metricsInfo.lastResponseTime !== null &&
755
- !isNaN(data.metricsInfo.lastResponseTime) &&
756
- data.metricsInfo.lastResponseTime > 0;
757
- const lastValStr = hasLast
758
- ? formatResponseTime(data.metricsInfo.lastResponseTime!)
759
- : "";
760
-
761
- const hasAdded =
762
- data.metricsInfo.linesAdded !== null && data.metricsInfo.linesAdded > 0;
763
- const addedValStr = hasAdded ? `${data.metricsInfo.linesAdded}` : "";
764
-
765
- const hasRemoved =
766
- data.metricsInfo.linesRemoved !== null && data.metricsInfo.linesRemoved > 0;
767
- const removedValStr = hasRemoved ? `${data.metricsInfo.linesRemoved}` : "";
768
-
769
- return {
770
- response: hasResponse ? `${sym.metrics_response} ${responseValStr}` : "",
771
- responseIcon: hasResponse ? sym.metrics_response : "",
772
- responseVal: responseValStr,
773
- lastResponse: hasLast
774
- ? `${sym.metrics_last_response} ${lastValStr}`
775
- : `${sym.metrics_last_response} --`,
776
- lastResponseIcon: sym.metrics_last_response,
777
- lastResponseVal: hasLast ? lastValStr : "--",
778
- added: hasAdded ? `${sym.metrics_lines_added}${addedValStr}` : "",
779
- addedIcon: hasAdded ? sym.metrics_lines_added : "",
780
- addedVal: addedValStr,
781
- removed: hasRemoved ? `${sym.metrics_lines_removed}${removedValStr}` : "",
782
- removedIcon: hasRemoved ? sym.metrics_lines_removed : "",
783
- removedVal: removedValStr,
784
- };
785
- }
786
-
787
- function formatMetricsSegment(data: TuiData, sym: SymbolSet): string {
788
- const parts = formatMetricsParts(data, sym);
789
- const filled = [
790
- parts.response,
791
- parts.lastResponse,
792
- parts.added,
793
- parts.removed,
794
- ].filter(Boolean);
795
- return filled.length > 0 ? filled.join(" · ") : "";
796
- }
797
-
798
- function formatActivityParts(
799
- data: TuiData,
800
- sym: SymbolSet,
801
- ): Record<string, string> {
802
- const empty = {
803
- icon: "",
804
- duration: "",
805
- durationIcon: "",
806
- durationVal: "",
807
- messages: "",
808
- messagesIcon: "",
809
- messagesVal: "",
810
- };
811
- if (!data.metricsInfo) return empty;
812
-
813
- const hasDuration =
814
- data.metricsInfo.sessionDuration !== null &&
815
- data.metricsInfo.sessionDuration > 0;
816
- const durationValStr = hasDuration
817
- ? formatDuration(data.metricsInfo.sessionDuration!)
818
- : "";
819
-
820
- const hasMessages =
821
- data.metricsInfo.messageCount !== null && data.metricsInfo.messageCount > 0;
822
- const messagesValStr = hasMessages ? `${data.metricsInfo.messageCount}` : "";
823
-
824
- return {
825
- icon: sym.activity,
826
- duration: hasDuration ? `${sym.metrics_duration} ${durationValStr}` : "",
827
- durationIcon: hasDuration ? sym.metrics_duration : "",
828
- durationVal: durationValStr,
829
- messages: hasMessages ? `${sym.metrics_messages} ${messagesValStr}` : "",
830
- messagesIcon: hasMessages ? sym.metrics_messages : "",
831
- messagesVal: messagesValStr,
832
- };
833
- }
834
-
835
- function formatActivitySegment(data: TuiData, sym: SymbolSet): string {
836
- const parts = formatActivityParts(data, sym);
837
- const filled = [parts.duration, parts.messages].filter(Boolean);
838
- return filled.length > 0 ? filled.join(" · ") : "";
839
- }
840
-
841
- function formatGitParts(
842
- data: TuiData,
843
- sym: SymbolSet,
844
- iconVisible = true,
845
- ): Record<string, string> {
846
- if (!data.gitInfo)
847
- return {
848
- icon: "",
849
- headVal: "",
850
- branch: "",
851
- status: "",
852
- ahead: "",
853
- behind: "",
854
- working: "",
855
- head: "",
856
- };
857
-
858
- let statusIcon: string;
859
- if (data.gitInfo.status === "conflicts") {
860
- statusIcon = sym.git_conflicts;
861
- } else if (data.gitInfo.status === "dirty") {
862
- statusIcon = sym.git_dirty;
863
- } else {
864
- statusIcon = sym.git_clean;
865
- }
866
-
867
- const ahead =
868
- data.gitInfo.ahead > 0 ? `${sym.git_ahead}${data.gitInfo.ahead}` : "";
869
- const behind =
870
- data.gitInfo.behind > 0 ? `${sym.git_behind}${data.gitInfo.behind}` : "";
871
-
872
- const counts: string[] = [];
873
- if (data.gitInfo.staged && data.gitInfo.staged > 0)
874
- counts.push(`+${data.gitInfo.staged}`);
875
- if (data.gitInfo.unstaged && data.gitInfo.unstaged > 0)
876
- counts.push(`~${data.gitInfo.unstaged}`);
877
- if (data.gitInfo.untracked && data.gitInfo.untracked > 0)
878
- counts.push(`?${data.gitInfo.untracked}`);
879
- const working = counts.length > 0 ? `(${counts.join(" ")})` : "";
880
-
881
- const headParts: string[] = [];
882
- if (iconVisible) headParts.push(sym.branch);
883
- headParts.push(data.gitInfo.branch, statusIcon);
884
- if (ahead) headParts.push(ahead);
885
- if (behind) headParts.push(behind);
886
-
887
- const infoParts = [data.gitInfo.branch, statusIcon];
888
- if (ahead) infoParts.push(ahead);
889
- if (behind) infoParts.push(behind);
890
-
891
- return {
892
- icon: iconVisible ? sym.branch : "",
893
- headVal: infoParts.join(" "),
894
- branch: data.gitInfo.branch,
895
- status: statusIcon,
896
- ahead,
897
- behind,
898
- working,
899
- head: headParts.join(" "),
900
- };
901
- }
902
-
903
- function formatGitSegment(
904
- data: TuiData,
905
- sym: SymbolSet,
906
- iconVisible = true,
907
- ): string {
908
- const parts = formatGitParts(data, sym, iconVisible);
909
- if (!parts.branch) return "";
910
- let text = parts.icon
911
- ? `${parts.icon} ${parts.branch} ${parts.status}`
912
- : `${parts.branch} ${parts.status}`;
913
- if (parts.ahead) text += ` ${parts.ahead}`;
914
- if (parts.behind) text += `${parts.behind}`;
915
- if (parts.working) text += ` ${parts.working}`;
916
- return text;
917
- }
918
-
919
- function formatDirParts(
920
- data: TuiData,
921
- config: PowerlineConfig,
922
- sym: SymbolSet,
923
- iconVisible = true,
924
- ): Record<string, string> {
925
- return {
926
- icon: iconVisible ? sym.dir : "",
927
- value: formatDirValue(data, config),
928
- };
929
- }
930
-
931
- function formatDirValue(data: TuiData, config: PowerlineConfig): string {
932
- const raw = getDirectoryDisplay(data.hookData);
933
- const dirConfig = config.display.lines
934
- .map((line) => line.segments.directory)
935
- .find((d) => d?.enabled);
936
- const style =
937
- dirConfig?.style ?? (dirConfig?.showBasename ? "basename" : "fish");
938
- if (style === "basename") {
939
- const sep = raw.includes("/") ? "/" : "\\";
940
- return raw.split(sep).pop() || raw;
941
- }
942
- if (style === "full") return raw;
943
- return abbreviateFishStyle(raw);
944
- }
945
-
946
- function formatVersionParts(
947
- data: TuiData,
948
- sym: SymbolSet,
949
- iconVisible = true,
950
- ): Record<string, string> {
951
- if (!data.hookData.version) return { icon: "", value: "" };
952
- return {
953
- icon: iconVisible ? sym.version : "",
954
- value: `v${data.hookData.version}`,
955
- };
956
- }
957
-
958
- function formatVersionSegment(
959
- data: TuiData,
960
- sym: SymbolSet,
961
- iconVisible = true,
962
- ): string {
963
- const parts = formatVersionParts(data, sym, iconVisible);
964
- if (!parts.value) return "";
965
- return parts.icon ? `${parts.icon} ${parts.value}` : parts.value;
966
- }
967
-
968
- function formatAgentParts(
969
- data: TuiData,
970
- sym: SymbolSet,
971
- iconVisible = true,
972
- ): Record<string, string> {
973
- const raw = data.hookData.agent?.name;
974
- if (typeof raw !== "string") return { icon: "", name: "" };
975
- const name = raw.trim();
976
- if (!name) return { icon: "", name: "" };
977
- return {
978
- icon: iconVisible ? sym.agent : "",
979
- name,
980
- };
981
- }
982
-
983
- function formatAgentSegment(
984
- data: TuiData,
985
- sym: SymbolSet,
986
- config: PowerlineConfig,
987
- iconVisible = true,
988
- ): string {
989
- const parts = formatAgentParts(data, sym, iconVisible);
990
- if (!parts.name) return "";
991
- const agentConfig = config.display.lines
992
- .map((line) => line.segments.agent)
993
- .find((a) => a?.enabled);
994
- const body = agentConfig?.showLabel ? `agent: ${parts.name}` : parts.name;
995
- return parts.icon ? `${parts.icon} ${body}` : body;
996
- }
997
-
998
- function buildThinkingBody(
999
- data: TuiData,
1000
- thinkingConfig: { showEnabled?: boolean; showEffort?: boolean } | undefined,
1001
- ): string {
1002
- const showEnabled = thinkingConfig?.showEnabled ?? true;
1003
- const showEffort = thinkingConfig?.showEffort ?? true;
1004
- if (!showEnabled && !showEffort) return "";
1005
-
1006
- const enabled = showEnabled ? getThinkingEnabled(data.hookData) : null;
1007
- const level = showEffort ? getEffortLevel(data.hookData) : null;
1008
-
1009
- const segments: string[] = [];
1010
- if (enabled !== null) segments.push(enabled ? "On" : "Off");
1011
- if (level) segments.push(level);
1012
- return segments.join(" · ");
1013
- }
1014
-
1015
- function formatThinkingParts(
1016
- data: TuiData,
1017
- sym: SymbolSet,
1018
- thinkingConfig: { showEnabled?: boolean; showEffort?: boolean } | undefined,
1019
- iconVisible = true,
1020
- ): Record<string, string> {
1021
- const showEnabled = thinkingConfig?.showEnabled ?? true;
1022
- const showEffort = thinkingConfig?.showEffort ?? true;
1023
- const enabled = showEnabled ? getThinkingEnabled(data.hookData) : null;
1024
- const level = showEffort ? getEffortLevel(data.hookData) : null;
1025
-
1026
- const enabledText = enabled === null ? "" : enabled ? "On" : "Off";
1027
- const effortText = level ?? "";
1028
- const hasAny = enabledText !== "" || effortText !== "";
1029
- return {
1030
- icon: hasAny && iconVisible ? sym.thinking : "",
1031
- enabled: enabledText,
1032
- effort: effortText,
1033
- };
1034
- }
1035
-
1036
- function formatThinkingSegment(
1037
- data: TuiData,
1038
- sym: SymbolSet,
1039
- thinkingConfig: { showEnabled?: boolean; showEffort?: boolean } | undefined,
1040
- iconVisible = true,
1041
- ): string {
1042
- const body = buildThinkingBody(data, thinkingConfig);
1043
- if (!body) return "";
1044
- return iconVisible ? `${sym.thinking} ${body}` : body;
1045
- }
1046
-
1047
- function formatCacheTimerParts(
1048
- data: TuiData,
1049
- sym: SymbolSet,
1050
- iconVisible = true,
1051
- ): Record<string, string> {
1052
- if (!data.cacheTimerInfo) return { icon: "", value: "" };
1053
- return {
1054
- icon: iconVisible ? sym.cache_timer : "",
1055
- value: formatCacheTimerElapsed(data.cacheTimerInfo.elapsedSeconds),
1056
- };
1057
- }
1058
-
1059
- function formatCacheTimerSegment(
1060
- data: TuiData,
1061
- sym: SymbolSet,
1062
- iconVisible = true,
1063
- ): string {
1064
- const parts = formatCacheTimerParts(data, sym, iconVisible);
1065
- if (!parts.value) return "";
1066
- return parts.icon ? `${parts.icon} ${parts.value}` : parts.value;
1067
- }
1068
-
1069
- function cacheTimerStyle(
1070
- elapsed: number,
1071
- colors: PowerlineColors,
1072
- ): { fg: string; bold: boolean } {
1073
- if (elapsed >= 300) {
1074
- return { fg: colors.contextCriticalFg, bold: colors.contextCriticalBold };
1075
- }
1076
- if (elapsed >= 180) {
1077
- return { fg: colors.contextWarningFg, bold: colors.contextWarningBold };
1078
- }
1079
- return { fg: colors.cacheTimerFg, bold: colors.cacheTimerBold };
1080
- }
1081
-
1082
- function formatTmuxParts(data: TuiData): Record<string, string> {
1083
- if (!data.tmuxSessionId) return { label: "", value: "" };
1084
- return { label: "tmux", value: data.tmuxSessionId };
1085
- }
1086
-
1087
- function formatTmuxSegment(data: TuiData): string {
1088
- const parts = formatTmuxParts(data);
1089
- if (!parts.label) return "";
1090
- return `${parts.label}:${parts.value}`;
1091
- }
1092
-
1093
- function formatEnvParts(config: PowerlineConfig): Record<string, string> {
1094
- const envConfig = config.display.lines
1095
- .map((line) => line.segments.env)
1096
- .find((env) => env?.enabled);
1097
-
1098
- if (!envConfig || !envConfig.variable) return { prefix: "", value: "" };
1099
- const envVal = globalThis.process?.env?.[envConfig.variable];
1100
- if (!envVal) return { prefix: "", value: "" };
1101
- const prefix = envConfig.prefix ?? envConfig.variable;
1102
- return { prefix: prefix || "", value: envVal };
1103
- }
1104
-
1105
- function formatEnvSegment(config: PowerlineConfig): string {
1106
- const parts = formatEnvParts(config);
1107
- if (!parts.value) return "";
1108
- return parts.prefix ? `${parts.prefix}:${parts.value}` : parts.value;
352
+ return `✓ bastra · ${info.recallCount} ${info.recallCount === 1 ? "call" : "calls"}${hits}${ms}`;
1109
353
  }
1110
354
 
1111
355
  function addParts(
@@ -1499,7 +743,11 @@ export function resolveSegments(
1499
743
 
1500
744
  // CacheTimer
1501
745
  const cacheTimerElapsed = data.cacheTimerInfo?.elapsedSeconds ?? 0;
1502
- const cacheTimerStyleResolved = cacheTimerStyle(cacheTimerElapsed, colors);
746
+ const cacheTimerStyleResolved = cacheTimerStyle(
747
+ cacheTimerElapsed,
748
+ colors,
749
+ cacheTimerTtl(config, data),
750
+ );
1503
751
  const cacheTimerColor = pf?.["cacheTimer"] ?? cacheTimerStyleResolved.fg;
1504
752
  result.cacheTimer = colorizeOrEmpty(
1505
753
  formatCacheTimerSegment(data, sym, iconVisible.cacheTimer),