pi-bar 0.3.44 → 0.3.46

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/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  **Never accidentally run Opus on a typo again.** pi-bar keeps your model, thinking level, context pressure, a live progress update, and any extension statuses visible in pi's footer.
7
7
 
8
8
  ```text
9
- claude-opus-4.7 ❯ think:med ❯ 2.6% / 1.0M ❯ Reviewing package structure ❯ Plan active ❯ Queue: 2
9
+ claude-opus-4.7 ❯ med ❯ 2.6% / 1.0M ❯ Reviewing package structure ❯ Plan active ❯ Queue: 2
10
10
  ```
11
11
 
12
12
  ![pi-bar with low context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.38/assets/screenshot-green.png)
@@ -43,7 +43,7 @@ The footer adapts to terminal width without extra configuration:
43
43
  - Separators have two spaces on each side by default, for a roomier display.
44
44
  - Progress text shrinks first. If space is still tight, separator padding reduces to one space per side before compacting content. CWD then compacts from its preferred path to `parent/project`, then `project`; context window size is omitted when needed. An enabled provider prefix can yield before the model name is clipped.
45
45
  - Optional token totals, cost, and cache-hit ratio yield whole under width pressure, before overflowing extension badges. Numeric metrics are never shown partially.
46
- - Extension badges stay in their published order. Badges that cannot fit are hidden whole, with `+N` showing how many are behind the overflow. Open `/bar status` to inspect their text and visibility by key.
46
+ - Extension badges stay in their published order. Badges that cannot fit are hidden whole, with `+N` showing how many are behind the overflow. Open `/bar badges` to inspect their text and visibility by key.
47
47
  - On very narrow terminals, progress and CWD yield to core information. If even the core cannot fit, the model name is truncated first. At extreme widths, not every value or overflow count can remain visible.
48
48
  - Widening the terminal restores the full display. Thinking and context retain their semantic colors throughout.
49
49
 
@@ -51,24 +51,42 @@ Existing visibility settings still apply. CWD, usage metrics, and the provider p
51
51
 
52
52
  ## Customization
53
53
 
54
- pi-bar works out of the box. Run `/bar` inside pi to choose which footer segments and extension statuses are shown:
54
+ **Start with `/bar`.** It opens all footer settings; you do not need to learn command groups.
55
55
 
56
56
  ```text
57
57
  /bar
58
58
  ```
59
59
 
60
- Toggle `Model`, `Thinking level`, `Context usage`, `Cache hit ratio`, `Estimated session cost`, `Session token totals`, `Current directory`, `Progress update`, and `Extension statuses` between `shown` and `hidden`. `Show provider` controls the optional prefix inside the model segment. `Progress model` opens a searchable model picker. If other extensions have published status badges, `/bar` also shows fine-grained `Status: <key>` rows plus a `New extension statuses` default. You can also use commands:
60
+ Type to find a setting, then press Enter/Space to change it. Changes save immediately; Esc closes the screen rather than undoing them.
61
+
62
+ Related settings stay together:
63
+ - **Model → Show provider:** model visibility and its optional provider prefix.
64
+ - **Progress update → Progress model:** progress visibility and a searchable model picker.
65
+ - **Extension badges → Choose badges:** overall badge visibility and a separate screen for individual badges. Badge lists do not crowd the main settings panel. Esc from that screen returns to settings.
66
+
67
+ For quick changes, Tab offers action-oriented commands with short explanations:
68
+
69
+ | Command | Purpose |
70
+ | --- | --- |
71
+ | `/bar settings` | Open all settings, just like `/bar`. |
72
+ | `/bar show <items>` | Show footer items. |
73
+ | `/bar hide <items>` | Hide footer items. |
74
+ | `/bar badges` | Choose badges published by other extensions. |
75
+ | `/bar provider [show\|hide]` | Show, hide, or inspect the provider prefix. |
76
+ | `/bar progress-model [auto\|provider/model]` | Choose or inspect the progress model. |
77
+ | `/bar help` | Show commands, examples, and current visibility settings. |
61
78
 
62
79
  ```text
63
- /bar segments list
64
- /bar segments only model context
65
- /bar segments hide progress
66
- /bar segments show thinking
80
+ /bar hide progress
81
+ /bar show cost tokens
82
+ /bar badges hide mcp
67
83
  ```
68
84
 
69
- Allowed segments are `model`, `thinking`, `context`, `cache_hit_ratio`, `cost`, `tokens`, `cwd`, `progress`, and `extensions`. The `cwd`, `cache_hit_ratio`, `cost`, and `tokens` segments are off by default. The `progress` segment stays hidden until pi-bar has a current update. The `extensions` segment stays hidden when no extension has set a status.
85
+ Footer item names are `model`, `thinking`, `context`, `cache_hit_ratio`, `cost`, `tokens`, `cwd`, `progress`, and `extensions`. Use `/bar show all` or `/bar hide all` to change all footer items at once. `cwd`, `cache_hit_ratio`, `cost`, and `tokens` are off by default. Enabled items can still be invisible while waiting for data or when terminal space is tight.
86
+
87
+ Tab completes item names, badge keys, and configured progress-model IDs while preserving preceding arguments, including comma-separated lists such as `/bar show model,ca`.
70
88
 
71
- Tab completes `/bar` subcommands, actions, segment names, known status keys, and configured progress-model IDs. Completion preserves preceding arguments, including comma-separated lists such as `/bar segments show model,ca`.
89
+ **Existing commands still work:** `/bar config`, `/bar segments ...`, `/bar status ...`, and `/bar list` are compatibility aliases, not additional choices in the top-level menu. `/bar list` and `/bar segments list` only report enabled footer items. Advanced commands such as `/bar segments only model context` remain available.
72
90
 
73
91
  You can also set startup defaults with environment variables before launching pi:
74
92
 
@@ -82,13 +100,13 @@ PI_BAR_SHOW=model,context pi
82
100
  Enable the optional `cwd` segment to distinguish projects and sessions:
83
101
 
84
102
  ```text
85
- /bar segments show cwd
103
+ /bar show cwd
86
104
  ```
87
105
 
88
106
  Or set startup segments with `PI_BAR_SHOW=model,thinking,context,cwd,progress,extensions`.
89
107
 
90
108
  ```text
91
- claude-opus-4.7 ❯ think:med ❯ 2.6% / 1.0M ❯ ~/projects/pi-bar
109
+ claude-opus-4.7 ❯ med ❯ 2.6% / 1.0M ❯ ~/projects/pi-bar
92
110
  ```
93
111
 
94
112
  Your home directory becomes `~`. Long paths omit middle directories, retaining trailing directory names where possible. The segment is capped at 36 terminal columns, including wide Unicode characters. Override the cap before starting pi:
@@ -123,19 +141,19 @@ The prefix uses the actual model provider, not a namespace embedded in the model
123
141
 
124
142
  ### Show cache, cost, and token metrics
125
143
 
126
- Enable any of the optional metrics through `/bar` or the existing segment commands:
144
+ Enable any of the optional metrics through `/bar` or the quick visibility commands:
127
145
 
128
146
  ```text
129
- /bar segments show cache_hit_ratio cost tokens
147
+ /bar show cache_hit_ratio cost tokens
130
148
  ```
131
149
 
132
150
  ```text
133
- claude-opus-4.7 ❯ think:med ❯ 2.6% / 1.0M ❯ CH:84% ❯ ≈$0.123 ❯ ↑12k ↓3k
151
+ claude-opus-4.7 ❯ med ❯ 2.6% / 1.0M ❯ CH:84% ❯ ≈$0.123 ❯ ↑12k ↓3k
134
152
  ```
135
153
 
136
154
  | Segment | Meaning |
137
155
  | --- | --- |
138
- | `cache_hit_ratio` | Latest active-branch assistant prompt's cache-read percentage: `cacheRead / (input + cacheRead + cacheWrite)`. Hidden when that response has no reported prompt usage. Restored on resume and `/tree`; tool, summary, and progress calls do not replace this value. |
156
+ | `cache_hit_ratio` | Latest active-branch assistant prompt's cache-read percentage: `cacheRead / (input + cacheRead + cacheWrite)`. Hidden when that response has no reported prompt usage. Colored green at 90% or higher, yellow from 50% up to 90%, and red below 50% (mostly cache misses). Restored on resume and `/tree`; tool, summary, and progress calls do not replace this value. |
139
157
  | `cost` | Estimated dollar cost for recorded usage in the current session file. `≈` means an estimate, **not an invoice**. |
140
158
  | `tokens` | Cumulative input (`↑`, including cache reads and writes) and output (`↓`, including reported reasoning tokens). These are usage totals, not current context size or tokens/sec. |
141
159
 
@@ -147,7 +165,7 @@ Prices come from the usage estimates Pi/providers report. Subscription allowance
147
165
 
148
166
  ### Configure live progress updates
149
167
 
150
- pi-bar shows a short, plain-English description of what pi is working on right now. It refreshes as pi works and resets when you switch branches in the session tree, so stale updates never follow you across tasks. Hide `Progress update` in `/bar`, run `/bar segments hide progress`, or set `PI_BAR_SHOW` without `progress` to disable it.
168
+ pi-bar shows a short, plain-English description of what pi is working on right now. It refreshes as pi works and resets when you switch branches in the session tree, so stale updates never follow you across tasks. Hide `Progress update` in `/bar`, run `/bar hide progress`, or set `PI_BAR_SHOW` without `progress` to disable it.
151
169
 
152
170
  **Pick a model interactively:** open `/bar`, select `Progress model` directly below `Progress update`, and press Enter. Type a provider or model ID (for example `gpt`), then press Enter to select. Esc cancels without changing the preference. The picker shows models with configured Pi credentials and an `Auto` option; it does not validate keys or make model requests.
153
171
 
@@ -173,17 +191,19 @@ PI_BAR_PROGRESS_MODEL=openai/gpt-4.1-mini pi
173
191
 
174
192
  Preference order: `PI_BAR_PROGRESS_MODEL` → saved pi-bar `progressModel` → project Pi settings → global Pi settings → Auto. Within each Pi settings scope, `bar.progressModel` takes precedence over the legacy `progress.model` setting. A nonempty environment override makes both the picker and command read-only and suppresses model-value completion; unset it and restart Pi to choose another model. A saved `"auto"` explicitly enables automatic selection rather than falling back to Pi settings. Remove `progressModel` from the pi-bar config to use Pi settings again.
175
193
 
176
- ### Configure extension statuses
194
+ ### Configure extension badges
177
195
 
178
196
  Other pi extensions can publish small status badges. Pi-bar displays their text as written, without prepending internal keys: `setStatus("mcp", "MCP: 2/2 servers")` appears as `MCP: 2/2 servers`, not `mcp:MCP: 2/2 servers`. Extensions should publish self-describing text; pi-bar does not guess labels for bare values.
179
197
 
180
- Pi-bar strips embedded terminal colors and control sequences, and separates each badge with the same `❯` divider used by other footer segments. Keys remain available for filtering and identification in `/bar`; selecting a status row shows its current sanitized text. Run `/bar` or `/bar status` inside pi to inspect statuses or pick which ones to show:
198
+ Pi-bar strips embedded terminal colors and control sequences, and separates each badge with the same `❯` divider used by other footer segments. Keys remain available for filtering and identification. Open `/bar` → `Choose badges`, or use the shortcut:
181
199
 
182
200
  ```text
183
- /bar status
201
+ /bar badges
184
202
  ```
185
203
 
186
- Toggle each status between `shown` and `hidden`. The `New statuses` row controls the default for badges that appear later.
204
+ Each `Badge: <key>` row shows its current sanitized text when selected. Toggle badges between `shown` and `hidden`; the `New badges` row controls the default for badges discovered later without changing existing choices. An empty screen explains that extensions have not published badges yet, while still letting you set that default.
205
+
206
+ These choices do not enable a hidden `Extension badges` footer item. Use `/bar show extensions` to display the badge area. `/bar badges show <keys>` and `/bar badges hide <keys>` are command shortcuts; the older `/bar status ...` spellings also work.
187
207
 
188
208
  Your choices persist across pi sessions in `~/.pi/agent/pi-bar.json`. Override the path with `PI_BAR_CONFIG=/some/path.json`.
189
209
 
@@ -208,7 +228,7 @@ npm test
208
228
  npm run check
209
229
  ```
210
230
 
211
- Tests cover path formatting, display-ready statuses, responsive layout, all 512 segment visibility combinations, terminal safety, ANSI/Unicode widths, provider settings, progress-model selection, command completion, usage accounting, progress-call persistence, session/tree lifecycle, and config persistence. Test configuration mirrors Pi's `pi-ai/compat` loader alias; runtime dependencies remain optional peers supplied by Pi.
231
+ Tests cover path formatting, display-ready statuses, responsive layout, all 512 segment visibility combinations, terminal safety, ANSI/Unicode widths, provider settings, progress-model selection, menu navigation, command completion and legacy aliases, usage accounting, progress-call persistence, session/tree lifecycle, and config persistence. Test configuration mirrors Pi's `pi-ai/compat` loader alias; runtime dependencies remain optional peers supplied by Pi.
212
232
 
213
233
  ## Security note
214
234
 
@@ -2,10 +2,10 @@
2
2
  * pi-bar — footer / statusline extension.
3
3
  *
4
4
  * Replaces pi's built-in footer with left-aligned segments:
5
- * <model> ❯ think:<level> ❯ <context% / window> ❯ [metrics] ❯ [cwd] ❯ <progress> ❯ <extensions>
5
+ * <model> ❯ <level> ❯ <context% / window> ❯ [metrics] ❯ [cwd] ❯ <progress> ❯ <extensions>
6
6
  *
7
7
  * Example:
8
- * claude-opus-4.7 ❯ think:med ❯ 2.6% / 1.0M ❯ Reviewing package structure
8
+ * claude-opus-4.7 ❯ med ❯ 2.6% / 1.0M ❯ Reviewing package structure
9
9
  *
10
10
  * Re-renders on model change, thinking-level change, status updates, and after
11
11
  * each assistant turn so context usage stays current.
@@ -140,8 +140,38 @@ const SEGMENT_LABELS: Record<SegmentName, string> = {
140
140
  tokens: "Session token totals",
141
141
  cwd: "Current directory",
142
142
  progress: "Progress update",
143
- extensions: "Extension statuses",
143
+ extensions: "Extension badges",
144
144
  };
145
+ const SEGMENT_DESCRIPTIONS: Record<SegmentName, string> = {
146
+ model: "Show the current chat model's name. This does not change your model.",
147
+ thinking: "Show the current thinking level, with its usual color.",
148
+ context: "Show context usage and the model's context window.",
149
+ cache_hit_ratio: "Show the latest response's prompt cache-hit rate. Hidden until prompt usage is available.",
150
+ cost: "Show estimated recorded session cost, not an invoice. Hidden until usage is available.",
151
+ tokens: "Show recorded input and output token totals, including cache usage.",
152
+ cwd: "Show this session's directory, not the directory of a tool's shell.",
153
+ progress: "Show live activity updates. Uses extra model calls; choose the progress model below.",
154
+ extensions: "Show badges published by other extensions. Choose individual badges below.",
155
+ };
156
+ // Advertise tasks, not the legacy command groups. Old spellings still work.
157
+ const BAR_COMMAND_DESCRIPTIONS = new Map([
158
+ ["settings", "Open all footer settings"],
159
+ ["show", "Show footer items"],
160
+ ["hide", "Hide footer items"],
161
+ ["badges", "Choose extension badges"],
162
+ ["provider", "Show or hide provider prefix"],
163
+ ["progress-model", "Choose progress model"],
164
+ ["help", "Commands and examples"],
165
+ ]);
166
+ const BAR_ACTION_DESCRIPTIONS = new Map([
167
+ ["show", "Show selected items"],
168
+ ["hide", "Hide selected items"],
169
+ ["only", "Show only selected items"],
170
+ ["all", "Show all items"],
171
+ ["none", "Hide all items"],
172
+ ["list", "Inspect visibility without changing it"],
173
+ ["settings", "Open visibility settings"],
174
+ ]);
145
175
  const DEFAULT_WARNING_THRESHOLD = 70;
146
176
  const DEFAULT_ERROR_THRESHOLD = 90;
147
177
 
@@ -357,6 +387,19 @@ function contextColor(
357
387
  return "success";
358
388
  }
359
389
 
390
+ // Cache hit ratio is "higher is better", the inverse of context usage. A healthy
391
+ // multi-turn conversation typically reads 90%+ of its prompt from cache; below
392
+ // 50% means most of the prompt was billed as fresh input (a mostly-miss turn).
393
+ const CACHE_HIT_GOOD_THRESHOLD = 90;
394
+ const CACHE_HIT_POOR_THRESHOLD = 50;
395
+
396
+ function cacheHitColor(percent: number | null | undefined): ThemeColor {
397
+ if (percent === null || percent === undefined) return "muted";
398
+ if (percent >= CACHE_HIT_GOOD_THRESHOLD) return "success";
399
+ if (percent >= CACHE_HIT_POOR_THRESHOLD) return "warning";
400
+ return "error";
401
+ }
402
+
360
403
  function isSegmentName(value: string): value is SegmentName {
361
404
  return (ALL_SEGMENTS as readonly string[]).includes(value);
362
405
  }
@@ -1834,11 +1877,11 @@ function describeSegments(segments: readonly SegmentName[]): string {
1834
1877
  function describeStatusFilter(filter: StatusFilter): string {
1835
1878
  if (filter.mode === "only") {
1836
1879
  const shown = Array.from(filter.shown).sort();
1837
- return shown.length > 0 ? `showing only: ${shown.join(", ")}` : "showing none";
1880
+ return shown.length > 0 ? `showing only: ${shown.map(stripTerminalControls).join(", ")}` : "showing none";
1838
1881
  }
1839
1882
 
1840
1883
  const hidden = Array.from(filter.hidden).sort();
1841
- return hidden.length > 0 ? `showing all except: ${hidden.join(", ")}` : "showing all";
1884
+ return hidden.length > 0 ? `showing all except: ${hidden.map(stripTerminalControls).join(", ")}` : "showing all";
1842
1885
  }
1843
1886
 
1844
1887
  function readGlobalConfig(): GlobalBarConfig {
@@ -1894,15 +1937,17 @@ export function completeBarArguments(
1894
1937
  let candidates: readonly string[];
1895
1938
  let selected: string[] = [];
1896
1939
  let segmentValues = false;
1940
+ let badgeValues = false;
1897
1941
  let modelValues = false;
1898
1942
  const section = words[0];
1943
+ const directVisibility = section === "show" || section === "hide";
1899
1944
  const segmentSection = section === "segments" || section === "segment" || section === "footer";
1900
- const statusSection = section === "status" || section === "statuses";
1945
+ const statusSection = section === "badges" || section === "status" || section === "statuses";
1901
1946
 
1902
1947
  if (words.length === 1) {
1903
- candidates = ["config", "segments", "status", "provider", "progress-model", "list"];
1948
+ candidates = [...BAR_COMMAND_DESCRIPTIONS.keys()];
1904
1949
  } else if (words.length === 2 && (segmentSection || statusSection)) {
1905
- candidates = ["list", "all", "none", "only", "show", "hide", "config"];
1950
+ candidates = [...BAR_ACTION_DESCRIPTIONS.keys()];
1906
1951
  } else if (words.length === 2 && section === "provider") {
1907
1952
  candidates = ["show", "hide"];
1908
1953
  } else if (words.length === 2 && section === "progress-model") {
@@ -1910,22 +1955,34 @@ export function completeBarArguments(
1910
1955
  candidates = ["auto", ...[...new Set(progressModelKeys.filter(
1911
1956
  (key) => typeof key === "string" && key !== "auto" && parseSerializedProgressModel(key) === key,
1912
1957
  ))].sort()];
1913
- } else if ((segmentSection || statusSection) && ["only", "show", "hide"].includes(words[1])) {
1958
+ } else if (directVisibility || ((segmentSection || statusSection) && ["only", "show", "hide"].includes(words[1]))) {
1914
1959
  const comma = fragment.lastIndexOf(",");
1915
1960
  const priorValues = fragment.slice(0, comma + 1);
1916
1961
  stem += priorValues;
1917
1962
  fragment = fragment.slice(comma + 1);
1918
- selected = [...words.slice(2, -1), priorValues].join(" ").split(/[\s,]+/).filter(Boolean);
1919
- segmentValues = segmentSection;
1963
+ selected = [...words.slice(directVisibility ? 1 : 2, -1), priorValues].join(" ").split(/[\s,]+/).filter(Boolean);
1964
+ segmentValues = directVisibility || segmentSection;
1965
+ badgeValues = !segmentValues;
1920
1966
  // Keys containing delimiters/controls are still configurable in the UI,
1921
1967
  // but cannot be represented safely by the existing command grammar.
1922
- candidates = segmentSection ? ALL_SEGMENTS : [...new Set(knownStatusKeys)].filter(
1923
- (key) => typeof key === "string" && key.length > 0 && !/[\s,\x00-\x1f\x7f-\x9f]/u.test(key),
1924
- ).sort();
1968
+ if (directVisibility && selected.some((value) => value.toLowerCase() === "all")) return null;
1969
+ candidates = segmentValues
1970
+ ? [...(directVisibility && selected.length === 0 ? ["all"] : []), ...ALL_SEGMENTS]
1971
+ : [...new Set(knownStatusKeys)].filter(
1972
+ (key) => typeof key === "string" && key.length > 0 && !/[\s,\x00-\x1f\x7f-\x9f]/u.test(key),
1973
+ ).sort();
1925
1974
  } else {
1926
1975
  return null;
1927
1976
  }
1928
1977
 
1978
+ const description = (value: string): string | undefined => {
1979
+ if (words.length === 1) return BAR_COMMAND_DESCRIPTIONS.get(value);
1980
+ if (segmentValues) return value === "all" ? "All footer items" : isSegmentName(value) ? SEGMENT_LABELS[value] : undefined;
1981
+ if (badgeValues) return "Extension badge";
1982
+ if (modelValues) return value === "auto" ? "Automatic fast-model selection" : "Progress model";
1983
+ if (section === "provider") return `${value === "show" ? "Show" : "Hide"} provider prefix`;
1984
+ return BAR_ACTION_DESCRIPTIONS.get(value);
1985
+ };
1929
1986
  const normalize = (value: string) => segmentValues || modelValues ? value.toLowerCase() : value;
1930
1987
  const used = new Set(selected.map(normalize));
1931
1988
  const query = normalize(fragment);
@@ -1934,13 +1991,7 @@ export function completeBarArguments(
1934
1991
  const normalized = normalize(value);
1935
1992
  return (modelValues ? normalized.includes(query) : normalized.startsWith(query)) && !used.has(normalized);
1936
1993
  })
1937
- .map((value) => ({
1938
- value: `${stem}${value}`,
1939
- label: value,
1940
- description: segmentValues && isSegmentName(value) ? SEGMENT_LABELS[value]
1941
- : modelValues ? value === "auto" ? "Automatic fast-model selection" : "Progress model"
1942
- : undefined,
1943
- }));
1994
+ .map((value) => ({ value: `${stem}${value}`, label: value, description: description(value) }));
1944
1995
  return matches.length > 0 ? matches : null;
1945
1996
  }
1946
1997
 
@@ -2035,265 +2086,167 @@ export default function (pi: ExtensionAPI) {
2035
2086
  if (!previousProgressVisible && nextProgressVisible && ctx) progress.startSession(ctx.cwd);
2036
2087
  refresh();
2037
2088
  };
2038
- const openSegmentConfigurator = async (ctx: ExtensionContext) => {
2089
+ const setBadgeVisibility = (key: string, shown: boolean) => {
2090
+ if (statusFilter.mode === "all") {
2091
+ if (shown) statusFilter.hidden.delete(key);
2092
+ else statusFilter.hidden.add(key);
2093
+ } else {
2094
+ if (shown) statusFilter.shown.add(key);
2095
+ else statusFilter.shown.delete(key);
2096
+ }
2097
+ };
2098
+ const badgeSummary = () => {
2099
+ const keys = getKnownStatusKeys(statusFilter, seenStatusKeys);
2100
+ const enabled = keys.filter((key) => shouldShowStatus(key, statusFilter)).length;
2101
+ return keys.length > 0 ? `${enabled}/${keys.length} enabled` : "No badges yet";
2102
+ };
2103
+ const savedSettingsTheme = (exit: "close" | "go back") => {
2104
+ const theme = getSettingsListTheme();
2105
+ return {
2106
+ ...theme,
2107
+ // These toggles save immediately; Esc is not an undo operation.
2108
+ hint: (text: string) => theme.hint(text.replace("Esc to cancel", `Esc to ${exit}`)),
2109
+ };
2110
+ };
2111
+ const createBadgeSettings = (onClose: () => void, exit: "close" | "go back") => {
2112
+ const knownKeys = getKnownStatusKeys(statusFilter, seenStatusKeys);
2113
+ const theme = savedSettingsTheme(exit);
2114
+ const items: SettingItem[] = [
2115
+ {
2116
+ id: "default:future",
2117
+ label: "New badges",
2118
+ description: "Default for badges discovered later. Existing badge choices stay unchanged.",
2119
+ currentValue: statusFilter.mode === "all" ? "shown" : "hidden",
2120
+ values: ["shown", "hidden"],
2121
+ },
2122
+ ...knownKeys.map((key): SettingItem => ({
2123
+ id: `badge:${key}`,
2124
+ label: `Badge: ${stripTerminalControls(key)}`,
2125
+ description: statusDescription(key),
2126
+ currentValue: shouldShowStatus(key, statusFilter) ? "shown" : "hidden",
2127
+ values: ["shown", "hidden"],
2128
+ })),
2129
+ ];
2130
+ const settings = new SettingsList(items, Math.min(items.length + 2, 15), theme, (id, value) => {
2131
+ if (id === "default:future") {
2132
+ const keys = getKnownStatusKeys(statusFilter, seenStatusKeys);
2133
+ statusFilter = value === "shown"
2134
+ ? { mode: "all", hidden: new Set(keys.filter((key) => !shouldShowStatus(key, statusFilter))) }
2135
+ : { mode: "only", shown: new Set(keys.filter((key) => shouldShowStatus(key, statusFilter))) };
2136
+ } else {
2137
+ setBadgeVisibility(id.slice("badge:".length), value === "shown");
2138
+ }
2139
+ persistStatusFilter();
2140
+ }, onClose, { enableSearch: true });
2141
+ return {
2142
+ render(width: number) {
2143
+ return [
2144
+ theme.label("Extension badges", true),
2145
+ theme.hint(knownKeys.length > 0
2146
+ ? "Choose badges by key; select a row to preview its text."
2147
+ : "No badges yet. Extensions publish them while you work."),
2148
+ theme.hint("Changes save immediately."),
2149
+ ...(!visibleSegments.includes("extensions")
2150
+ ? [theme.hint("All badges are hidden. Use /bar show extensions to display them.")]
2151
+ : []),
2152
+ "",
2153
+ ...settings.render(Math.max(8, width)),
2154
+ ].map((line) => truncateToWidth(line, Math.max(0, width)));
2155
+ },
2156
+ invalidate() { settings.invalidate(); },
2157
+ handleInput(data: string) { settings.handleInput(data); },
2158
+ };
2159
+ };
2160
+ const openSettings = async (ctx: ExtensionContext) => {
2039
2161
  if (ctx.mode !== "tui") {
2040
- ctx.ui.notify("/bar configuration requires TUI mode", "warning");
2162
+ ctx.ui.notify("/bar settings requires TUI mode", "warning");
2041
2163
  return;
2042
2164
  }
2043
2165
  await ctx.ui.custom((tui, theme, _kb, done) => {
2044
- const knownStatusKeys = getKnownStatusKeys(statusFilter, seenStatusKeys);
2045
- const segmentVisibility = new Map(
2046
- ALL_SEGMENTS.map(
2047
- (segment): [SegmentName, boolean] => [segment, visibleSegments.includes(segment)],
2048
- ),
2049
- );
2050
- let futureShown = statusFilter.mode === "all";
2051
- const statusVisibility = new Map(
2052
- knownStatusKeys.map((key): [string, boolean] => [
2053
- key,
2054
- shouldShowStatus(key, statusFilter),
2055
- ]),
2056
- );
2057
- const persistSegmentsFromVisibility = () => {
2058
- setVisibleSegments(
2059
- ALL_SEGMENTS.filter((segment) => segmentVisibility.get(segment)),
2060
- ctx,
2061
- );
2062
- };
2063
- const persistStatusesFromVisibility = () => {
2064
- if (futureShown) {
2065
- statusFilter = {
2066
- mode: "all",
2067
- hidden: new Set(
2068
- knownStatusKeys.filter((key) => !statusVisibility.get(key)),
2069
- ),
2070
- };
2071
- } else {
2072
- statusFilter = {
2073
- mode: "only",
2074
- shown: new Set(
2075
- knownStatusKeys.filter((key) => statusVisibility.get(key)),
2076
- ),
2077
- };
2078
- }
2079
- persistStatusFilter();
2080
- };
2081
-
2082
- const segmentItems: SettingItem[] = ALL_SEGMENTS.flatMap((segment): SettingItem[] => {
2083
- const visibilityItem: SettingItem = {
2166
+ const items = ALL_SEGMENTS.flatMap((segment): SettingItem[] => {
2167
+ const group: SettingItem[] = [{
2084
2168
  id: `segment:${segment}`,
2085
2169
  label: SEGMENT_LABELS[segment],
2086
- description: "Footer segment visibility",
2087
- currentValue: segmentVisibility.get(segment) ? "shown" : "hidden",
2170
+ description: SEGMENT_DESCRIPTIONS[segment],
2171
+ currentValue: visibleSegments.includes(segment) ? "shown" : "hidden",
2088
2172
  values: ["shown", "hidden"],
2089
- };
2090
- if (segment !== "progress") return [visibilityItem];
2091
- return [visibilityItem, {
2173
+ }];
2174
+ if (segment === "model") group.push({
2175
+ id: "show-provider",
2176
+ label: "Show provider",
2177
+ description: "Prefix the model name with its provider. Only visible when Model is shown.",
2178
+ currentValue: showProvider ? "shown" : "hidden",
2179
+ values: ["shown", "hidden"],
2180
+ });
2181
+ if (segment === "progress") group.push({
2092
2182
  id: "progress-model",
2093
2183
  label: "Progress model",
2094
2184
  description: process.env.PI_BAR_PROGRESS_MODEL
2095
2185
  ? "Controlled by PI_BAR_PROGRESS_MODEL (read-only). Unset it and restart Pi to choose here."
2096
- : "Choose Auto or search models with configured Pi credentials. Saved for all projects; changes only progress updates.",
2186
+ : "Choose Auto or search models with configured Pi credentials. Changes only progress updates, not the chat model.",
2097
2187
  currentValue: formatProgressModelKey(resolveProgressModelPreference(ctx.cwd)),
2098
2188
  submenu: process.env.PI_BAR_PROGRESS_MODEL
2099
2189
  ? undefined
2100
2190
  : (currentValue, close) => createProgressModelPicker(ctx, currentValue, close),
2101
- }];
2191
+ });
2192
+ if (segment === "extensions") group.push({
2193
+ id: "badge-settings",
2194
+ label: "Choose badges",
2195
+ description: "Choose individual badges from other extensions. They display only when Extension badges is shown.",
2196
+ currentValue: badgeSummary(),
2197
+ submenu: (_currentValue, close) => createBadgeSettings(() => close(badgeSummary()), "go back"),
2198
+ });
2199
+ return group;
2102
2200
  });
2103
- const statusItems: SettingItem[] = knownStatusKeys.length > 0
2104
- ? [
2105
- {
2106
- id: "status:__future",
2107
- label: "New extension statuses",
2108
- description: "Default visibility for status keys discovered later",
2109
- currentValue: futureShown ? "shown" : "hidden",
2110
- values: ["shown", "hidden"],
2111
- },
2112
- ...knownStatusKeys.map((key): SettingItem => ({
2113
- id: `status:${key}`,
2114
- label: `Status: ${stripTerminalControls(key)}`,
2115
- description: statusDescription(key),
2116
- currentValue: statusVisibility.get(key) ? "shown" : "hidden",
2117
- values: ["shown", "hidden"],
2118
- })),
2119
- ]
2120
- : [];
2121
- const items: SettingItem[] = [
2122
- ...segmentItems,
2123
- {
2124
- id: "show-provider",
2125
- label: "Show provider",
2126
- description: "Provider prefix inside the model segment (hidden by default)",
2127
- currentValue: showProvider ? "shown" : "hidden",
2128
- values: ["shown", "hidden"],
2129
- },
2130
- ...statusItems,
2131
- ];
2132
-
2133
2201
  const container = new Container();
2134
- container.addChild(
2135
- new (class {
2136
- render(_width: number) {
2137
- return [
2138
- theme.fg("accent", theme.bold("pi-bar configuration")),
2139
- theme.fg(
2140
- "dim",
2141
- knownStatusKeys.length > 0
2142
- ? "Footer settings + extension statuses · Enter/Space changes · Esc closes"
2143
- : "Footer settings · Enter/Space changes · Esc closes",
2144
- ),
2145
- "",
2146
- ];
2147
- }
2148
- invalidate() {}
2149
- })(),
2150
- );
2151
-
2152
- const settingsList = new SettingsList(
2153
- items,
2154
- Math.min(items.length + 2, 18),
2155
- getSettingsListTheme(),
2156
- (id, newValue) => {
2157
- if (id === "progress-model") {
2158
- setProgressModel(newValue, ctx);
2159
- settingsList.updateValue(id, formatProgressModelKey(resolveProgressModelPreference(ctx.cwd)));
2160
- return;
2161
- }
2162
- if (id === "show-provider") {
2163
- setShowProvider(newValue === "shown");
2164
- return;
2165
- }
2166
- if (id.startsWith("segment:")) {
2167
- const segment = id.slice("segment:".length);
2168
- if (!isSegmentName(segment)) return;
2169
- segmentVisibility.set(segment, newValue === "shown");
2170
- persistSegmentsFromVisibility();
2171
- return;
2172
- }
2173
-
2174
- if (id === "status:__future") {
2175
- futureShown = newValue === "shown";
2176
- persistStatusesFromVisibility();
2177
- return;
2178
- }
2179
-
2180
- if (id.startsWith("status:")) {
2181
- statusVisibility.set(id.slice("status:".length), newValue === "shown");
2182
- persistStatusesFromVisibility();
2183
- }
2184
- },
2185
- () => done(undefined),
2186
- { enableSearch: true },
2187
- );
2188
-
2189
- container.addChild(settingsList);
2190
-
2202
+ container.addChild(new (class {
2203
+ render(_width: number) {
2204
+ return [
2205
+ theme.fg("accent", theme.bold("pi-bar settings")),
2206
+ theme.fg("dim", "Choose what appears in your footer. Changes save immediately."),
2207
+ "",
2208
+ ];
2209
+ }
2210
+ invalidate() {}
2211
+ })());
2212
+ const settings = new SettingsList(items, Math.min(items.length + 2, 18), savedSettingsTheme("close"), (id, value) => {
2213
+ if (id === "progress-model") {
2214
+ setProgressModel(value, ctx);
2215
+ settings.updateValue(id, formatProgressModelKey(resolveProgressModelPreference(ctx.cwd)));
2216
+ } else if (id === "show-provider") {
2217
+ setShowProvider(value === "shown");
2218
+ } else if (id.startsWith("segment:")) {
2219
+ const segment = id.slice("segment:".length);
2220
+ if (!isSegmentName(segment)) return;
2221
+ setVisibleSegments(value === "shown"
2222
+ ? [...visibleSegments, segment]
2223
+ : visibleSegments.filter((item) => item !== segment), ctx);
2224
+ }
2225
+ }, () => done(undefined), { enableSearch: true });
2226
+ container.addChild(settings);
2191
2227
  return {
2192
2228
  render(width: number) {
2193
2229
  return container.render(Math.max(8, width)).map((line) => truncateToWidth(line, Math.max(0, width)));
2194
2230
  },
2195
- invalidate() {
2196
- container.invalidate();
2197
- },
2231
+ invalidate() { container.invalidate(); },
2198
2232
  handleInput(data: string) {
2199
- settingsList.handleInput?.(data);
2233
+ settings.handleInput(data);
2200
2234
  tui.requestRender();
2201
2235
  },
2202
2236
  };
2203
2237
  });
2204
2238
  };
2205
- const openStatusConfigurator = async (ctx: ExtensionContext) => {
2206
- const knownStatusKeys = getKnownStatusKeys(statusFilter, seenStatusKeys);
2207
- if (knownStatusKeys.length === 0) {
2208
- ctx.ui.notify(
2209
- "No extension statuses seen yet. Open /bar after another extension calls ctx.ui.setStatus().",
2210
- "info",
2211
- );
2239
+ const openBadges = async (ctx: ExtensionContext) => {
2240
+ if (ctx.mode !== "tui") {
2241
+ ctx.ui.notify("/bar badges requires TUI mode", "warning");
2212
2242
  return;
2213
2243
  }
2214
-
2215
- await ctx.ui.custom((tui, theme, _kb, done) => {
2216
- let futureShown = statusFilter.mode === "all";
2217
- const statusVisibility = new Map(
2218
- knownStatusKeys.map((key) => [key, shouldShowStatus(key, statusFilter)]),
2219
- );
2220
- const persistFromVisibility = () => {
2221
- if (futureShown) {
2222
- statusFilter = {
2223
- mode: "all",
2224
- hidden: new Set(
2225
- knownStatusKeys.filter((key) => !statusVisibility.get(key)),
2226
- ),
2227
- };
2228
- } else {
2229
- statusFilter = {
2230
- mode: "only",
2231
- shown: new Set(
2232
- knownStatusKeys.filter((key) => statusVisibility.get(key)),
2233
- ),
2234
- };
2235
- }
2236
- persistStatusFilter();
2237
- };
2238
-
2239
- const items: SettingItem[] = [
2240
- {
2241
- id: "__future",
2242
- label: "New statuses",
2243
- description: "Default visibility for status keys discovered later",
2244
- currentValue: futureShown ? "shown" : "hidden",
2245
- values: ["shown", "hidden"],
2246
- },
2247
- ...knownStatusKeys.map((key): SettingItem => ({
2248
- id: key,
2249
- label: stripTerminalControls(key),
2250
- description: statusDescription(key),
2251
- currentValue: statusVisibility.get(key) ? "shown" : "hidden",
2252
- values: ["shown", "hidden"],
2253
- })),
2254
- ];
2255
-
2256
- const container = new Container();
2257
- container.addChild(
2258
- new (class {
2259
- render(_width: number) {
2260
- return [
2261
- theme.fg("accent", theme.bold("pi-bar status visibility")),
2262
- theme.fg("dim", "Enter/Space toggles · Esc closes"),
2263
- "",
2264
- ];
2265
- }
2266
- invalidate() {}
2267
- })(),
2268
- );
2269
-
2270
- const settingsList = new SettingsList(
2271
- items,
2272
- Math.min(items.length + 2, 15),
2273
- getSettingsListTheme(),
2274
- (id, newValue) => {
2275
- if (id === "__future") {
2276
- futureShown = newValue === "shown";
2277
- } else {
2278
- statusVisibility.set(id, newValue === "shown");
2279
- }
2280
- persistFromVisibility();
2281
- },
2282
- () => done(undefined),
2283
- { enableSearch: true },
2284
- );
2285
-
2286
- container.addChild(settingsList);
2287
-
2244
+ await ctx.ui.custom((tui, _theme, _kb, done) => {
2245
+ const settings = createBadgeSettings(() => done(undefined), "close");
2288
2246
  return {
2289
- render(width: number) {
2290
- return container.render(width);
2291
- },
2292
- invalidate() {
2293
- container.invalidate();
2294
- },
2247
+ ...settings,
2295
2248
  handleInput(data: string) {
2296
- settingsList.handleInput?.(data);
2249
+ settings.handleInput(data);
2297
2250
  tui.requestRender();
2298
2251
  },
2299
2252
  };
@@ -2301,7 +2254,7 @@ export default function (pi: ExtensionAPI) {
2301
2254
  };
2302
2255
 
2303
2256
  pi.registerCommand("bar", {
2304
- description: "Configure pi-bar footer",
2257
+ description: "Open footer settings; choose what appears in the bar",
2305
2258
  getArgumentCompletions: (prefix) => {
2306
2259
  let modelKeys: string[] = [];
2307
2260
  if (/^\s*progress-model\s+\S*$/.test(prefix)) {
@@ -2313,15 +2266,55 @@ export default function (pi: ExtensionAPI) {
2313
2266
  return completeBarArguments(prefix, getKnownStatusKeys(statusFilter, seenStatusKeys), modelKeys);
2314
2267
  },
2315
2268
  handler: async (args, ctx) => {
2316
- const [section, action, ...rest] = args.trim().split(/\s+/).filter(Boolean);
2317
- if (!section || section === "config" || section === "configure" || section === "edit") {
2318
- await openSegmentConfigurator(ctx);
2269
+ const words = args.trim().split(/\s+/).filter(Boolean);
2270
+ if (words[0] === "show" || words[0] === "hide") {
2271
+ const names = splitStatusKeys(words.slice(1).join(" ")).map((name) => name.toLowerCase());
2272
+ if (names.includes("all")) {
2273
+ if (names.length !== 1) {
2274
+ ctx.ui.notify("Use all by itself: /bar show all or /bar hide all.", "warning");
2275
+ return;
2276
+ }
2277
+ words.splice(0, words.length, "segments", words[0] === "show" ? "all" : "none");
2278
+ } else {
2279
+ const unknown = names.filter((name) => !isSegmentName(name));
2280
+ if (unknown.length > 0) {
2281
+ ctx.ui.notify(`Unknown footer items: ${unknown.map(stripTerminalControls).join(", ")}. Use Tab to choose items.`, "warning");
2282
+ return;
2283
+ }
2284
+ words.unshift("segments");
2285
+ }
2286
+ }
2287
+ if (words[0] === "badges") {
2288
+ if (["show", "hide", "only"].includes(words[1]) && splitStatusKeys(words.slice(2).join(" ")).length === 0) {
2289
+ ctx.ui.notify("Choose badge keys with Tab, or open /bar badges to choose them interactively.", "warning");
2290
+ return;
2291
+ }
2292
+ words[0] = "status";
2293
+ }
2294
+ const [section, action, ...rest] = words;
2295
+ if (!section || section === "settings" || section === "config" || section === "configure" || section === "edit") {
2296
+ await openSettings(ctx);
2319
2297
  return;
2320
2298
  }
2321
2299
  if (section === "list" || section === "ls") {
2322
2300
  ctx.ui.notify(`pi-bar footer: ${describeSegments(visibleSegments)}`, "info");
2323
2301
  return;
2324
2302
  }
2303
+ if (section === "help") {
2304
+ ctx.ui.notify([
2305
+ "Open /bar for all footer settings. Changes save immediately.",
2306
+ "",
2307
+ ...Array.from(BAR_COMMAND_DESCRIPTIONS, ([name, description]) => `/bar ${name} — ${description}`),
2308
+ "",
2309
+ "Examples: /bar show cost tokens · /bar hide progress · /bar badges hide mcp",
2310
+ "Use /bar show all or /bar hide all to change every footer item.",
2311
+ `Footer items: ${ALL_SEGMENTS.join(", ")}`,
2312
+ `Current configuration: ${describeSegments(visibleSegments)}`,
2313
+ "Enabled items may wait for data or yield under width pressure.",
2314
+ "Old commands still work: /bar config, /bar segments, /bar status, /bar list.",
2315
+ ].join("\n"), "info");
2316
+ return;
2317
+ }
2325
2318
 
2326
2319
  if (section === "progress-model") {
2327
2320
  if (!action) {
@@ -2359,7 +2352,7 @@ export default function (pi: ExtensionAPI) {
2359
2352
  const segments = splitSegmentNames(rest.join(" "));
2360
2353
  if ((action === "only" || action === "show" || action === "hide") && segments.length === 0) {
2361
2354
  ctx.ui.notify(
2362
- `Segments: ${ALL_SEGMENTS.join(", ")}`,
2355
+ `Footer items: ${ALL_SEGMENTS.join(", ")}. Use Tab to choose.`,
2363
2356
  "warning",
2364
2357
  );
2365
2358
  return;
@@ -2367,10 +2360,11 @@ export default function (pi: ExtensionAPI) {
2367
2360
 
2368
2361
  switch (action) {
2369
2362
  case undefined:
2363
+ case "settings":
2370
2364
  case "config":
2371
2365
  case "configure":
2372
2366
  case "edit":
2373
- await openSegmentConfigurator(ctx);
2367
+ await openSettings(ctx);
2374
2368
  return;
2375
2369
  case "list":
2376
2370
  case "ls":
@@ -2396,7 +2390,7 @@ export default function (pi: ExtensionAPI) {
2396
2390
  break;
2397
2391
  default:
2398
2392
  ctx.ui.notify(
2399
- "Usage: /bar [config] or /bar segments [list|all|none|only <segments>|show <segments>|hide <segments>]",
2393
+ "Use /bar show <items> or /bar hide <items>. Open /bar for all settings.",
2400
2394
  "warning",
2401
2395
  );
2402
2396
  return;
@@ -2407,7 +2401,7 @@ export default function (pi: ExtensionAPI) {
2407
2401
  }
2408
2402
 
2409
2403
  if ((section === "status" || section === "statuses") && !action) {
2410
- await openStatusConfigurator(ctx);
2404
+ await openBadges(ctx);
2411
2405
  return;
2412
2406
  }
2413
2407
 
@@ -2415,16 +2409,17 @@ export default function (pi: ExtensionAPI) {
2415
2409
  const keys = splitStatusKeys(rest.join(" "));
2416
2410
 
2417
2411
  switch (action) {
2412
+ case "settings":
2418
2413
  case "config":
2419
2414
  case "configure":
2420
2415
  case "edit":
2421
- await openStatusConfigurator(ctx);
2416
+ await openBadges(ctx);
2422
2417
  return;
2423
2418
  case "list":
2424
2419
  case "ls": {
2425
2420
  const known = getKnownStatusKeys(statusFilter, seenStatusKeys);
2426
2421
  ctx.ui.notify(
2427
- `pi-bar statuses: ${describeStatusFilter(statusFilter)}${known.length > 0 ? `; known: ${known.join(", ")}` : "; known: none yet"}`,
2422
+ `pi-bar badges: ${describeStatusFilter(statusFilter)}${known.length > 0 ? `; known: ${known.map(stripTerminalControls).join(", ")}` : "; known: none yet"}`,
2428
2423
  "info",
2429
2424
  );
2430
2425
  return;
@@ -2439,34 +2434,24 @@ export default function (pi: ExtensionAPI) {
2439
2434
  statusFilter = { mode: "only", shown: new Set(keys) };
2440
2435
  break;
2441
2436
  case "hide":
2442
- if (statusFilter.mode === "only") {
2443
- for (const key of keys) statusFilter.shown.delete(key);
2444
- } else {
2445
- for (const key of keys) statusFilter.hidden.add(key);
2446
- }
2447
- break;
2448
2437
  case "show":
2449
- if (statusFilter.mode === "only") {
2450
- for (const key of keys) statusFilter.shown.add(key);
2451
- } else {
2452
- for (const key of keys) statusFilter.hidden.delete(key);
2453
- }
2438
+ for (const key of keys) setBadgeVisibility(key, action === "show");
2454
2439
  break;
2455
2440
  default:
2456
2441
  ctx.ui.notify(
2457
- "Usage: /bar status [list|all|none|only <keys>|show <keys>|hide <keys>]",
2442
+ "Open /bar badges to choose badges, or use /bar badges show|hide <keys>.",
2458
2443
  "warning",
2459
2444
  );
2460
2445
  return;
2461
2446
  }
2462
2447
 
2463
2448
  persistStatusFilter();
2464
- ctx.ui.notify(`pi-bar statuses: ${describeStatusFilter(statusFilter)}`, "info");
2449
+ ctx.ui.notify(`pi-bar badges: ${describeStatusFilter(statusFilter)}`, "info");
2465
2450
  return;
2466
2451
  }
2467
2452
 
2468
2453
  ctx.ui.notify(
2469
- "Usage: /bar [config] or /bar segments [list|all|none|only <segments>|show <segments>|hide <segments>] or /bar status [list|all|none|only <keys>|show <keys>|hide <keys>] or /bar provider [show|hide] or /bar progress-model [auto|provider/model]",
2454
+ "Unknown /bar command. Open /bar for settings or /bar help for examples.",
2470
2455
  "warning",
2471
2456
  );
2472
2457
  },
@@ -2572,15 +2557,15 @@ export default function (pi: ExtensionAPI) {
2572
2557
  alternatives: showProvider ? [theme.fg("accent", formatModelName(ctx.model))] : [],
2573
2558
  },
2574
2559
  thinking: {
2575
- text: theme.fg(thinkingColor(thinkingLevel), `think:${thinkingLevel}`),
2576
- alternatives: [theme.fg(thinkingColor(thinkingLevel), `think:${shortThinking}`)],
2560
+ text: theme.fg(thinkingColor(thinkingLevel), thinkingLevel),
2561
+ alternatives: [theme.fg(thinkingColor(thinkingLevel), shortThinking)],
2577
2562
  },
2578
2563
  context: {
2579
2564
  text: theme.fg(contextSegmentColor, contextText),
2580
2565
  alternatives: [theme.fg(contextSegmentColor, contextCompact)],
2581
2566
  },
2582
2567
  cache_hit_ratio: {
2583
- text: metrics.cacheHitRate === null ? "" : theme.fg("muted", `CH:${metrics.cacheHitRate.toFixed(1).replace(/\.0$/, "")}%`),
2568
+ text: metrics.cacheHitRate === null ? "" : theme.fg(cacheHitColor(metrics.cacheHitRate), `CH:${metrics.cacheHitRate.toFixed(1).replace(/\.0$/, "")}%`),
2584
2569
  },
2585
2570
  cost: { text: reportedUsage ? theme.fg("muted", `≈$${metrics.cost.toFixed(3)}`) : "" },
2586
2571
  tokens: { text: reportedUsage ? theme.fg("muted", `↑${formatTokens(metrics.input)} ↓${formatTokens(metrics.output)}`) : "" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-bar",
3
- "version": "0.3.44",
3
+ "version": "0.3.46",
4
4
  "description": "Pi footer/statusline showing active model, thinking level, context pressure, live progress updates, and extension statuses.",
5
5
  "type": "module",
6
6
  "keywords": [