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 +2 -0
- package/extensions/input-bar.ts +20 -1
- package/extensions/render.ts +39 -0
- package/package.json +1 -1
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
|
|
package/extensions/input-bar.ts
CHANGED
|
@@ -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) =>
|
|
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
|
});
|
package/extensions/render.ts
CHANGED
|
@@ -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.
|
|
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",
|