pi-bar 0.3.32 → 0.3.33

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
@@ -9,9 +9,9 @@
9
9
  claude-opus-4.7 ❯ think:med ❯ 2.6% / 1.0M ❯ Reviewing package structure ❯ plan:active
10
10
  ```
11
11
 
12
- ![pi-bar with low context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.32/assets/screenshot-green.png)
13
- ![pi-bar with medium context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.32/assets/screenshot-yellow.png)
14
- ![pi-bar with high context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.32/assets/screenshot-red.png)
12
+ ![pi-bar with low context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.33/assets/screenshot-green.png)
13
+ ![pi-bar with medium context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.33/assets/screenshot-yellow.png)
14
+ ![pi-bar with high context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.33/assets/screenshot-red.png)
15
15
 
16
16
  ## Why use it?
17
17
 
@@ -21,7 +21,7 @@ claude-opus-4.7 ❯ think:med ❯ 2.6% / 1.0M ❯ Reviewing package struct
21
21
  - **Follow what pi is doing** — a one-line progress update keeps the current task visible without scrolling.
22
22
  - **Keep extension statuses visible** — statuses other extensions set still appear in the footer.
23
23
 
24
- pi-bar is intentionally tiny: one small extension, no broad behavior changes, and no commands to learn. It replaces pi's built-in footer with a compact model / thinking / context / progress / extension-status line.
24
+ pi-bar is intentionally tiny: one small extension and no broad behavior changes. It replaces pi's built-in footer with a compact model / thinking / context / progress / extension-status line.
25
25
 
26
26
  ## Install
27
27
 
@@ -37,20 +37,33 @@ If pi is already running after install, reload resources:
37
37
 
38
38
  ## Customization
39
39
 
40
- pi-bar works out of the box, but you can tune it with environment variables before launching pi.
40
+ pi-bar works out of the box. Run `/bar` inside pi to choose which footer segments are shown:
41
41
 
42
- ### Show or hide segments
42
+ ```text
43
+ /bar
44
+ ```
45
+
46
+ Toggle `Model`, `Thinking level`, `Context usage`, `Progress update`, and `Extension statuses` between `shown` and `hidden`. You can also use commands:
47
+
48
+ ```text
49
+ /bar segments list
50
+ /bar segments only model context
51
+ /bar segments hide progress
52
+ /bar segments show thinking
53
+ ```
54
+
55
+ Allowed segments are `model`, `thinking`, `context`, `progress`, and `extensions`. The `progress` segment stays hidden until pi-bar has a current update. The `extensions` segment stays hidden when no extension has set a status.
56
+
57
+ You can also set startup defaults with environment variables before launching pi:
43
58
 
44
59
  ```bash
45
60
  PI_BAR_SHOW=model,thinking,context,progress,extensions pi
46
61
  PI_BAR_SHOW=model,context pi
47
62
  ```
48
63
 
49
- Allowed segments are `model`, `thinking`, `context`, `progress`, and `extensions`. The `progress` segment stays hidden until pi-bar has a current update. The `extensions` segment stays hidden when no extension has set a status.
50
-
51
64
  ### Configure live progress updates
52
65
 
53
- 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. Set `PI_BAR_SHOW` without `progress` to hide it.
66
+ 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.
54
67
 
55
68
  Pick a specific model for the update by setting the env var or pi settings:
56
69
 
@@ -62,10 +75,10 @@ Or in pi settings: `bar.progressModel`. Otherwise pi-bar picks a fast model you
62
75
 
63
76
  ### Configure extension statuses
64
77
 
65
- Other pi extensions can publish small status badges. Pi-bar collects them into the `extensions` segment. Run `/bar` inside pi to pick which ones to show:
78
+ Other pi extensions can publish small status badges. Pi-bar collects them into the `extensions` segment. Run `/bar status` inside pi to pick which ones to show:
66
79
 
67
80
  ```text
68
- /bar
81
+ /bar status
69
82
  ```
70
83
 
71
84
  Toggle each status between `shown` and `hidden`. The `New statuses` row controls the default for badges that appear later.
@@ -89,4 +102,4 @@ The first number is the warning/yellow threshold. The second number is the dange
89
102
 
90
103
  Pi extensions run with your local user permissions. Review any pi package source before installing it.
91
104
 
92
- Progress updates send short snippets of your session activity to the selected model provider. Disable the `progress` segment with `PI_BAR_SHOW` if that is not acceptable.
105
+ Progress updates send short snippets of your session activity to the selected model provider. Hide `Progress update` in `/bar` or disable the `progress` segment with `PI_BAR_SHOW` if that is not acceptable.
@@ -14,7 +14,7 @@
14
14
  * PI_BAR_SHOW comma-separated list of segments to show
15
15
  * PI_BAR_THRESHOLDS warning,danger context-usage percentages
16
16
  * PI_BAR_PROGRESS_MODEL provider/id for the progress update model
17
- * PI_BAR_CONFIG override the persisted status-filter config path
17
+ * PI_BAR_CONFIG override the persisted pi-bar config path
18
18
  */
19
19
 
20
20
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
@@ -42,6 +42,10 @@ type StatusFilter =
42
42
  type SerializedStatusFilter =
43
43
  | { mode: "all"; hidden: string[] }
44
44
  | { mode: "only"; shown: string[] };
45
+ type GlobalBarConfig = {
46
+ statusFilter?: SerializedStatusFilter;
47
+ segments?: SegmentName[];
48
+ };
45
49
  type ProgressActivityType =
46
50
  | "user_message"
47
51
  | "assistant_update"
@@ -110,6 +114,20 @@ const DEFAULT_SEGMENTS: SegmentName[] = [
110
114
  "progress",
111
115
  "extensions",
112
116
  ];
117
+ const ALL_SEGMENTS: readonly SegmentName[] = [
118
+ "model",
119
+ "thinking",
120
+ "context",
121
+ "progress",
122
+ "extensions",
123
+ ];
124
+ const SEGMENT_LABELS: Record<SegmentName, string> = {
125
+ model: "Model",
126
+ thinking: "Thinking level",
127
+ context: "Context usage",
128
+ progress: "Progress update",
129
+ extensions: "Extension statuses",
130
+ };
113
131
  const DEFAULT_WARNING_THRESHOLD = 70;
114
132
  const DEFAULT_ERROR_THRESHOLD = 90;
115
133
 
@@ -166,6 +184,10 @@ function contextColor(
166
184
  return "success";
167
185
  }
168
186
 
187
+ function isSegmentName(value: string): value is SegmentName {
188
+ return (ALL_SEGMENTS as readonly string[]).includes(value);
189
+ }
190
+
169
191
  function parseSegments(): SegmentName[] {
170
192
  const raw = process.env.PI_BAR_SHOW;
171
193
  if (!raw) return DEFAULT_SEGMENTS;
@@ -173,9 +195,7 @@ function parseSegments(): SegmentName[] {
173
195
  const requested = raw
174
196
  .split(",")
175
197
  .map((segment) => segment.trim().toLowerCase())
176
- .filter((segment): segment is SegmentName =>
177
- ["model", "thinking", "context", "progress", "extensions"].includes(segment),
178
- );
198
+ .filter(isSegmentName);
179
199
 
180
200
  return requested.length > 0 ? requested : DEFAULT_SEGMENTS;
181
201
  }
@@ -1523,6 +1543,18 @@ function serializeStatusFilter(filter: StatusFilter): SerializedStatusFilter {
1523
1543
  return { mode: "all", hidden: Array.from(filter.hidden).sort() };
1524
1544
  }
1525
1545
 
1546
+ function serializeSegments(segments: readonly SegmentName[]): SegmentName[] {
1547
+ return ALL_SEGMENTS.filter((segment) => segments.includes(segment));
1548
+ }
1549
+
1550
+ function parseSerializedSegments(value: unknown): SegmentName[] | null {
1551
+ if (!Array.isArray(value)) return null;
1552
+ const segments = value.filter(
1553
+ (segment): segment is SegmentName => typeof segment === "string" && isSegmentName(segment),
1554
+ );
1555
+ return serializeSegments(segments);
1556
+ }
1557
+
1526
1558
  function parseSerializedStatusFilter(value: unknown): StatusFilter | null {
1527
1559
  if (!value || typeof value !== "object") return null;
1528
1560
  const data = value as Partial<SerializedStatusFilter>;
@@ -1543,6 +1575,18 @@ function splitStatusKeys(raw: string): string[] {
1543
1575
  .filter(Boolean);
1544
1576
  }
1545
1577
 
1578
+ function splitSegmentNames(raw: string): SegmentName[] {
1579
+ return raw
1580
+ .split(/[\s,]+/)
1581
+ .map((segment) => segment.trim().toLowerCase())
1582
+ .filter(isSegmentName);
1583
+ }
1584
+
1585
+ function describeSegments(segments: readonly SegmentName[]): string {
1586
+ if (segments.length === 0) return "showing none";
1587
+ return `showing: ${segments.map((segment) => SEGMENT_LABELS[segment]).join(", ")}`;
1588
+ }
1589
+
1546
1590
  function describeStatusFilter(filter: StatusFilter): string {
1547
1591
  if (filter.mode === "only") {
1548
1592
  const shown = Array.from(filter.shown).sort();
@@ -1553,29 +1597,45 @@ function describeStatusFilter(filter: StatusFilter): string {
1553
1597
  return hidden.length > 0 ? `showing all except: ${hidden.join(", ")}` : "showing all";
1554
1598
  }
1555
1599
 
1556
- function readGlobalStatusFilter(): StatusFilter | null {
1600
+ function readGlobalConfig(): GlobalBarConfig {
1557
1601
  try {
1558
- const data = JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as {
1559
- statusFilter?: unknown;
1602
+ const data = JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as Record<string, unknown>;
1603
+ const statusFilter = parseSerializedStatusFilter(data.statusFilter ?? data);
1604
+ return {
1605
+ statusFilter: statusFilter ? serializeStatusFilter(statusFilter) : undefined,
1606
+ segments: parseSerializedSegments(data.segments) ?? undefined,
1560
1607
  };
1561
- return parseSerializedStatusFilter(data.statusFilter ?? data);
1562
1608
  } catch {
1563
- return null;
1609
+ return {};
1564
1610
  }
1565
1611
  }
1566
1612
 
1567
- function writeGlobalStatusFilter(filter: StatusFilter): void {
1568
- const data = JSON.stringify(
1569
- { statusFilter: serializeStatusFilter(filter) },
1570
- null,
1571
- 2,
1572
- );
1613
+ function readGlobalStatusFilter(): StatusFilter | null {
1614
+ return parseSerializedStatusFilter(readGlobalConfig().statusFilter);
1615
+ }
1616
+
1617
+ function readGlobalSegments(): SegmentName[] | null {
1618
+ return process.env.PI_BAR_SHOW ? parseSegments() : readGlobalConfig().segments ?? null;
1619
+ }
1620
+
1621
+ function writeGlobalConfig(config: GlobalBarConfig): void {
1622
+ const data = JSON.stringify(config, null, 2);
1573
1623
  mkdirSync(dirname(CONFIG_PATH), { recursive: true });
1574
1624
  const tmpPath = `${CONFIG_PATH}.${process.pid}.tmp`;
1575
1625
  writeFileSync(tmpPath, `${data}\n`, "utf8");
1576
1626
  renameSync(tmpPath, CONFIG_PATH);
1577
1627
  }
1578
1628
 
1629
+ function writeGlobalStatusFilter(filter: StatusFilter): void {
1630
+ const existing = readGlobalConfig();
1631
+ writeGlobalConfig({ ...existing, statusFilter: serializeStatusFilter(filter) });
1632
+ }
1633
+
1634
+ function writeGlobalSegments(segments: readonly SegmentName[]): void {
1635
+ const existing = readGlobalConfig();
1636
+ writeGlobalConfig({ ...existing, segments: serializeSegments(segments) });
1637
+ }
1638
+
1579
1639
  function getKnownStatusKeys(filter: StatusFilter, seenStatusKeys: Set<string>): string[] {
1580
1640
  const keys = new Set(seenStatusKeys);
1581
1641
  if (filter.mode === "only") {
@@ -1589,6 +1649,7 @@ function getKnownStatusKeys(filter: StatusFilter, seenStatusKeys: Set<string>):
1589
1649
  export default function (pi: ExtensionAPI) {
1590
1650
  let requestRender: (() => void) | undefined;
1591
1651
  let statusFilter: StatusFilter = { mode: "all", hidden: new Set() };
1652
+ let visibleSegments: SegmentName[] = readGlobalSegments() ?? DEFAULT_SEGMENTS;
1592
1653
  const seenStatusKeys = new Set<string>();
1593
1654
  const refresh = () => requestRender?.();
1594
1655
  const progress = new FooterProgressEngine(refresh);
@@ -1607,6 +1668,81 @@ export default function (pi: ExtensionAPI) {
1607
1668
  writeGlobalStatusFilter(statusFilter);
1608
1669
  refresh();
1609
1670
  };
1671
+ const setVisibleSegments = (segments: readonly SegmentName[], ctx?: ExtensionContext) => {
1672
+ const previousProgressVisible = visibleSegments.includes("progress");
1673
+ visibleSegments = serializeSegments(segments);
1674
+ writeGlobalSegments(visibleSegments);
1675
+ const nextProgressVisible = visibleSegments.includes("progress");
1676
+ if (previousProgressVisible && !nextProgressVisible) progress.shutdown();
1677
+ if (!previousProgressVisible && nextProgressVisible && ctx) progress.startSession(ctx.cwd);
1678
+ refresh();
1679
+ };
1680
+ const openSegmentConfigurator = async (ctx: ExtensionContext) => {
1681
+ await ctx.ui.custom((tui, theme, _kb, done) => {
1682
+ const segmentVisibility = new Map(
1683
+ ALL_SEGMENTS.map(
1684
+ (segment): [SegmentName, boolean] => [segment, visibleSegments.includes(segment)],
1685
+ ),
1686
+ );
1687
+ const persistFromVisibility = () => {
1688
+ setVisibleSegments(
1689
+ ALL_SEGMENTS.filter((segment) => segmentVisibility.get(segment)),
1690
+ ctx,
1691
+ );
1692
+ };
1693
+
1694
+ const items: SettingItem[] = ALL_SEGMENTS.map((segment): SettingItem => ({
1695
+ id: segment,
1696
+ label: SEGMENT_LABELS[segment],
1697
+ description: "Footer segment visibility",
1698
+ currentValue: segmentVisibility.get(segment) ? "shown" : "hidden",
1699
+ values: ["shown", "hidden"],
1700
+ }));
1701
+
1702
+ const container = new Container();
1703
+ container.addChild(
1704
+ new (class {
1705
+ render(_width: number) {
1706
+ return [
1707
+ theme.fg("accent", theme.bold("pi-bar footer visibility")),
1708
+ theme.fg("dim", "Enter/Space toggles · Esc closes"),
1709
+ "",
1710
+ ];
1711
+ }
1712
+ invalidate() {}
1713
+ })(),
1714
+ );
1715
+
1716
+ const settingsList = new SettingsList(
1717
+ items,
1718
+ Math.min(items.length + 2, 15),
1719
+ getSettingsListTheme(),
1720
+ (id, newValue) => {
1721
+ if (isSegmentName(id)) {
1722
+ segmentVisibility.set(id, newValue === "shown");
1723
+ persistFromVisibility();
1724
+ }
1725
+ },
1726
+ () => done(undefined),
1727
+ { enableSearch: true },
1728
+ );
1729
+
1730
+ container.addChild(settingsList);
1731
+
1732
+ return {
1733
+ render(width: number) {
1734
+ return container.render(width);
1735
+ },
1736
+ invalidate() {
1737
+ container.invalidate();
1738
+ },
1739
+ handleInput(data: string) {
1740
+ settingsList.handleInput?.(data);
1741
+ tui.requestRender();
1742
+ },
1743
+ };
1744
+ });
1745
+ };
1610
1746
  const openStatusConfigurator = async (ctx: ExtensionContext) => {
1611
1747
  const knownStatusKeys = getKnownStatusKeys(statusFilter, seenStatusKeys);
1612
1748
  if (knownStatusKeys.length === 0) {
@@ -1706,10 +1842,70 @@ export default function (pi: ExtensionAPI) {
1706
1842
  };
1707
1843
 
1708
1844
  pi.registerCommand("bar", {
1709
- description: "Configure pi-bar footer status segments",
1845
+ description: "Configure pi-bar footer visibility",
1710
1846
  handler: async (args, ctx) => {
1711
1847
  const [section, action, ...rest] = args.trim().split(/\s+/).filter(Boolean);
1712
- if (!section || ((section === "status" || section === "statuses") && !action)) {
1848
+ if (!section || section === "config" || section === "configure" || section === "edit") {
1849
+ await openSegmentConfigurator(ctx);
1850
+ return;
1851
+ }
1852
+ if (section === "list" || section === "ls") {
1853
+ ctx.ui.notify(`pi-bar footer: ${describeSegments(visibleSegments)}`, "info");
1854
+ return;
1855
+ }
1856
+
1857
+ if (section === "segment" || section === "segments" || section === "footer") {
1858
+ const segments = splitSegmentNames(rest.join(" "));
1859
+ if ((action === "only" || action === "show" || action === "hide") && segments.length === 0) {
1860
+ ctx.ui.notify(
1861
+ `Segments: ${ALL_SEGMENTS.join(", ")}`,
1862
+ "warning",
1863
+ );
1864
+ return;
1865
+ }
1866
+
1867
+ switch (action) {
1868
+ case undefined:
1869
+ case "config":
1870
+ case "configure":
1871
+ case "edit":
1872
+ await openSegmentConfigurator(ctx);
1873
+ return;
1874
+ case "list":
1875
+ case "ls":
1876
+ ctx.ui.notify(`pi-bar footer: ${describeSegments(visibleSegments)}`, "info");
1877
+ return;
1878
+ case "all":
1879
+ setVisibleSegments(ALL_SEGMENTS, ctx);
1880
+ break;
1881
+ case "none":
1882
+ setVisibleSegments([], ctx);
1883
+ break;
1884
+ case "only":
1885
+ setVisibleSegments(segments, ctx);
1886
+ break;
1887
+ case "hide":
1888
+ setVisibleSegments(
1889
+ visibleSegments.filter((segment) => !segments.includes(segment)),
1890
+ ctx,
1891
+ );
1892
+ break;
1893
+ case "show":
1894
+ setVisibleSegments([...visibleSegments, ...segments], ctx);
1895
+ break;
1896
+ default:
1897
+ ctx.ui.notify(
1898
+ "Usage: /bar [config] or /bar segments [list|all|none|only <segments>|show <segments>|hide <segments>]",
1899
+ "warning",
1900
+ );
1901
+ return;
1902
+ }
1903
+
1904
+ ctx.ui.notify(`pi-bar footer: ${describeSegments(visibleSegments)}`, "info");
1905
+ return;
1906
+ }
1907
+
1908
+ if ((section === "status" || section === "statuses") && !action) {
1713
1909
  await openStatusConfigurator(ctx);
1714
1910
  return;
1715
1911
  }
@@ -1769,7 +1965,7 @@ export default function (pi: ExtensionAPI) {
1769
1965
  }
1770
1966
 
1771
1967
  ctx.ui.notify(
1772
- "Usage: /bar status [list|all|none|only <keys>|show <keys>|hide <keys>]",
1968
+ "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>]",
1773
1969
  "warning",
1774
1970
  );
1775
1971
  },
@@ -1779,37 +1975,39 @@ export default function (pi: ExtensionAPI) {
1779
1975
  pi.on("thinking_level_select", async () => refresh());
1780
1976
  pi.on("turn_end", async () => refresh());
1781
1977
  pi.on("before_agent_start", async (event, ctx) => {
1782
- progress.recordUserMessage(ctx, event.prompt);
1978
+ if (visibleSegments.includes("progress")) progress.recordUserMessage(ctx, event.prompt);
1783
1979
  });
1784
1980
  pi.on("message_update", async (event, ctx) => {
1785
- progress.recordAssistantUpdate(ctx, event.message);
1981
+ if (visibleSegments.includes("progress")) progress.recordAssistantUpdate(ctx, event.message);
1786
1982
  });
1787
1983
  pi.on("tool_call", async (event, ctx) => {
1788
- progress.recordToolCall(ctx, event);
1984
+ if (visibleSegments.includes("progress")) progress.recordToolCall(ctx, event);
1789
1985
  });
1790
1986
  pi.on("tool_result", async (event, ctx) => {
1791
- progress.recordToolResult(ctx, event);
1987
+ if (visibleSegments.includes("progress")) progress.recordToolResult(ctx, event);
1792
1988
  });
1793
1989
  pi.on("message_end", async (event, ctx) => {
1794
- progress.recordMessageEnd(ctx, event.message);
1990
+ if (visibleSegments.includes("progress")) progress.recordMessageEnd(ctx, event.message);
1795
1991
  refresh();
1796
1992
  });
1797
1993
  pi.on("session_before_tree", async () => {
1798
1994
  progress.shutdown();
1799
1995
  });
1800
1996
  pi.on("session_tree", async (_event, ctx) => {
1801
- progress.startSession(ctx.cwd);
1997
+ if (visibleSegments.includes("progress")) progress.startSession(ctx.cwd);
1998
+ else progress.shutdown();
1802
1999
  restoreStatusFilter(ctx);
1803
2000
  refresh();
1804
2001
  });
1805
2002
 
1806
2003
  pi.on("session_start", async (_event, ctx) => {
1807
- progress.startSession(ctx.cwd);
2004
+ visibleSegments = readGlobalSegments() ?? DEFAULT_SEGMENTS;
2005
+ if (visibleSegments.includes("progress")) progress.startSession(ctx.cwd);
2006
+ else progress.shutdown();
1808
2007
  restoreStatusFilter(ctx);
1809
2008
 
1810
2009
  if (!ctx.hasUI) return;
1811
2010
 
1812
- const visibleSegments = parseSegments();
1813
2011
  const { warningThreshold, errorThreshold } = parseThresholds();
1814
2012
 
1815
2013
  ctx.ui.setFooter((tui, theme, footerData) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-bar",
3
- "version": "0.3.32",
3
+ "version": "0.3.33",
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": [
@@ -51,6 +51,6 @@
51
51
  "extensions": [
52
52
  "./extensions/status-footer.ts"
53
53
  ],
54
- "image": "https://cdn.jsdelivr.net/npm/pi-bar@0.3.32/assets/screenshot-yellow.png"
54
+ "image": "https://cdn.jsdelivr.net/npm/pi-bar@0.3.33/assets/screenshot-yellow.png"
55
55
  }
56
56
  }