pi-input-bar 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,6 +34,7 @@ No dependencies, no build step, nothing to configure — the bar appears as soon
34
34
  - Current folder and git branch
35
35
  - A dot that pulses while the agent is responding (`●`)
36
36
  - `provider/model` and thinking effort level (color-coded)
37
+ - If [pi-loop-police](https://github.com/sebaxzero/pi-loop-police) is installed and has caught loops this session, a summary appears after the dot: `⚠ 10 loops (tool 7, think 3)`. No loop-police (or no loops) — nothing shown.
37
38
 
38
39
  **Bottom border** — what's happening under the hood:
39
40
  - **Context usage** — fill level, colored green → yellow → red as it fills
@@ -65,6 +66,7 @@ Persistent configuration lives in `extensions/input-bar.json` next to the instal
65
66
  | `ANIM` | `true` | Random working word while streaming |
66
67
  | `ICONS` | `true` | Glyphs (⌂ ⎇ ⛁ ⚡) in the bars |
67
68
  | `WORDS` | `""` | Comma-separated custom working-word pool |
69
+ | `LOOPS` | `true` | Summary of pi-loop-police detections in the top border (only shown once a loop is caught) |
68
70
 
69
71
  ## License
70
72
 
@@ -31,6 +31,7 @@ import {
31
31
  composeBottomLine,
32
32
  composeLine,
33
33
  DEFAULT_WORDS,
34
+ loopSummary,
34
35
  pickWord,
35
36
  replaceEdgeChars,
36
37
  stripAnsi,
@@ -51,6 +52,7 @@ const DEFAULTS = {
51
52
  ANIM: true, // random working word while streaming
52
53
  ICONS: true, // glyphs (⌂ ⎇ ⛁ ⚡) in the bars
53
54
  WORDS: "", // comma-separated override for the working-word pool
55
+ LOOPS: true, // loop-police detection summary in the top border (if installed)
54
56
  };
55
57
 
56
58
  const cfg: typeof DEFAULTS = (() => {
@@ -122,6 +124,18 @@ export default function (pi: ExtensionAPI) {
122
124
  let ctxRef: ExtensionContext | undefined;
123
125
  // FooterDataProvider captured from the setFooter factory; the editor reads it too.
124
126
  let footerData: { getGitBranch(): string | null } | undefined;
127
+ // TUI handle captured from the editor factory, to repaint on bus events.
128
+ let tuiRef: { requestRender(): void } | undefined;
129
+
130
+ // Per-session loop-police detections, keyed by detector name (payload.event).
131
+ // If pi-loop-police isn't installed the event never fires and the map stays
132
+ // empty — the bar renders exactly as before.
133
+ const loopCounts = new Map<string, number>();
134
+ (pi as any).events?.on?.("loop-police:detection", (data: any) => {
135
+ const kind = typeof data?.event === "string" ? data.event : "unknown";
136
+ loopCounts.set(kind, (loopCounts.get(kind) ?? 0) + 1);
137
+ tuiRef?.requestRender();
138
+ });
125
139
 
126
140
  // t/s tracking. The clock starts at the FIRST streamed token, not at
127
141
  // message_start — otherwise prompt-processing time (long on local servers)
@@ -170,6 +184,7 @@ export default function (pi: ExtensionAPI) {
170
184
  provider: ctx?.model?.provider,
171
185
  modelId: ctx?.model?.id,
172
186
  effort: ctx?.model?.reasoning ? effort : undefined,
187
+ loops: cfg.LOOPS ? loopSummary(loopCounts) : undefined,
173
188
  icons: cfg.ICONS,
174
189
  });
175
190
  const line = composeLine(left, right, width, makeColorize(theme, effort), tuiVisibleWidth);
@@ -272,7 +287,10 @@ export default function (pi: ExtensionAPI) {
272
287
  // paddingX: 2 keeps a spare space column on each side of the content so
273
288
  // the │ sides can be painted without touching text or the cursor cell.
274
289
  ctx.ui.setEditorComponent(
275
- (tui, theme, keybindings) => new BarEditor(tui, theme, keybindings, { paddingX: 2 }),
290
+ (tui, theme, keybindings) => {
291
+ tuiRef = tui;
292
+ return new BarEditor(tui, theme, keybindings, { paddingX: 2 });
293
+ },
276
294
  );
277
295
  } else {
278
296
  ctx.ui.setEditorComponent(undefined);
@@ -304,6 +322,7 @@ export default function (pi: ExtensionAPI) {
304
322
 
305
323
  pi.on("session_start", (_event, ctx) => {
306
324
  ctxRef = ctx;
325
+ loopCounts.clear();
307
326
  if (ctx.mode !== "tui") return;
308
327
  installBars(ctx);
309
328
  });
@@ -107,6 +107,40 @@ export function composeLine(
107
107
  return paint(l, c) + c("dim", "─".repeat(gap)) + paint(r, c);
108
108
  }
109
109
 
110
+ /** Short labels for loop-police detector names, grouped by family. */
111
+ export const LOOP_KIND_LABELS: Record<string, string> = {
112
+ thinking_loop: "think",
113
+ semantic_loop: "think",
114
+ output_loop: "out",
115
+ output_semantic_loop: "out",
116
+ stagnation: "stag",
117
+ file_read_loop: "file",
118
+ file_scan_loop: "file",
119
+ search_spiral: "search",
120
+ tool_loop: "tool",
121
+ };
122
+
123
+ /**
124
+ * Compact per-session summary of loop-police detections:
125
+ * "10 loops (tool 7, think 3)" — undefined when there are none, so the bar
126
+ * renders exactly as without loop-police installed.
127
+ */
128
+ export function loopSummary(counts: ReadonlyMap<string, number>): string | undefined {
129
+ let total = 0;
130
+ const byLabel = new Map<string, number>();
131
+ for (const [kind, n] of counts) {
132
+ if (n <= 0) continue;
133
+ total += n;
134
+ const label = LOOP_KIND_LABELS[kind] ?? kind;
135
+ byLabel.set(label, (byLabel.get(label) ?? 0) + n);
136
+ }
137
+ if (total === 0) return undefined;
138
+ const parts = [...byLabel.entries()]
139
+ .sort((a, b) => b[1] - a[1])
140
+ .map(([label, n]) => `${label} ${n}`);
141
+ return `${total} loop${total === 1 ? "" : "s"} (${parts.join(", ")})`;
142
+ }
143
+
110
144
  export interface TopBarData {
111
145
  folder: string;
112
146
  branch: string | null;
@@ -115,6 +149,8 @@ export interface TopBarData {
115
149
  modelId: string | undefined;
116
150
  /** Thinking level, or undefined when the model has no reasoning. */
117
151
  effort: string | undefined;
152
+ /** loopSummary() text, or undefined when loop-police reported nothing. */
153
+ loops: string | undefined;
118
154
  icons: boolean;
119
155
  }
120
156
 
@@ -129,6 +165,9 @@ export function topBarSegments(d: TopBarData): { left: Segment[]; right: Segment
129
165
  left.push(sep, { text: d.icons ? "⎇ " : "", color: "success" }, { text: d.branch, color: "success" });
130
166
  }
131
167
  left.push({ text: d.streaming ? " ● " : " ○ ", color: d.streaming ? "warning" : "dim" });
168
+ if (d.loops) {
169
+ left.push({ text: (d.icons ? "⚠ " : "") + d.loops + " ", color: "warning" });
170
+ }
132
171
 
133
172
  const right: Segment[] = [];
134
173
  if (d.modelId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-input-bar",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Pi extension: decorated input bar — folder/branch/model/effort above the editor, context/token usage and tokens-per-second below, plus random working words while the agent streams.",
5
5
  "keywords": ["pi-package", "pi", "pi-coding-agent", "extension", "tui", "statusbar", "footer", "input-bar"],
6
6
  "license": "MIT",