pi-harness-runtime 1.1.37 → 1.1.39
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/harness/langchain/daemon.ts +91 -15
- package/harness/langchain/graph.ts +3 -0
- package/harness/langchain/widget.test.ts +244 -0
- package/harness/langchain/widget.ts +463 -0
- package/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LoopWidget — harness progress display for the pi host status line.
|
|
3
|
+
*
|
|
4
|
+
* Architecture mirrors pi-lens's `clients/widget-state.ts` + `renderWidget()`:
|
|
5
|
+
* - State store: per-file step counts, iteration, phase
|
|
6
|
+
* - Render function: builds the `!NW ●ME` style summary line
|
|
7
|
+
* - Pi host integration: wired via ExtensionUIContext.setWidget() when available
|
|
8
|
+
*
|
|
9
|
+
* The widget is designed as a dependency injected into LoopDaemon/LoopRuntime,
|
|
10
|
+
* so it works in two modes:
|
|
11
|
+
* 1. Standalone (console): logs human-readable progress to stderr
|
|
12
|
+
* 2. Pi host: calls ui.setWidget("harness-loop", renderFn) for TUI display
|
|
13
|
+
*
|
|
14
|
+
* Wiki: wiki/multi-agent-langchain.md
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { TUI } from "@earendil-works/pi-tui";
|
|
18
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
|
|
20
|
+
/** Valid theme color keys passed to `theme.fg()`.*/
|
|
21
|
+
type ColorKey = "dim" | "error" | "warning" | "success" | "accent";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Count visible terminal columns. ANSI CSI sequences (\x1b[...m) contribute 0 width.
|
|
25
|
+
* Terminal wide chars (emoji, CJK) contribute 2 columns.
|
|
26
|
+
*/
|
|
27
|
+
function visibleWidth(s: string): number {
|
|
28
|
+
let w = 0;
|
|
29
|
+
let i = 0;
|
|
30
|
+
while (i < s.length) {
|
|
31
|
+
const cp = s.codePointAt(i)!;
|
|
32
|
+
if (cp === 0x1b) {
|
|
33
|
+
// Skip CSI sequences: \x1b[ <params> <final>
|
|
34
|
+
// Valid final bytes: m (SGR), H (CUP), J (ED), A/B/C/D (CUU/CUD/CUF/CUB)
|
|
35
|
+
i++;
|
|
36
|
+
while (i < s.length && !/[a-zA-Z@]/.test(s[i])) i++;
|
|
37
|
+
if (i < s.length) i++; // skip final byte
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (cp <= 0x1f || cp === 0x7f) { i++; continue; } // C0 + DEL = 0
|
|
41
|
+
w += cp > 0xffff ? 2 : 1;
|
|
42
|
+
i += cp > 0xffff ? 2 : 1; // advance by code point, not UTF-16 code unit
|
|
43
|
+
}
|
|
44
|
+
return w;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
export type LoopPhase =
|
|
50
|
+
| "idle"
|
|
51
|
+
| "planning"
|
|
52
|
+
| "writing"
|
|
53
|
+
| "reviewing"
|
|
54
|
+
| "fixing"
|
|
55
|
+
| "approving"
|
|
56
|
+
| "blocked"
|
|
57
|
+
| "surge-pause"
|
|
58
|
+
| "complete"
|
|
59
|
+
| "error";
|
|
60
|
+
|
|
61
|
+
export interface LoopWidgetOptions {
|
|
62
|
+
/** Called with the rendered lines each time the widget updates. */
|
|
63
|
+
onRender?: (lines: string[]) => void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Shaped like the pi-lens FileRecord tier system. */
|
|
67
|
+
interface FileStepRecord {
|
|
68
|
+
filePath: string;
|
|
69
|
+
/** Write steps touching this file in current iteration. */
|
|
70
|
+
writeSteps: number;
|
|
71
|
+
/** Review cycles passed (no blocking issues). */
|
|
72
|
+
reviewsPassed: number;
|
|
73
|
+
/** Blocking issues found by reviewer. */
|
|
74
|
+
blockers: number;
|
|
75
|
+
lastPhase: LoopPhase;
|
|
76
|
+
touchedAt: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── Widget State ─────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
export class LoopWidget {
|
|
82
|
+
private files = new Map<string, FileStepRecord>();
|
|
83
|
+
private phase: LoopPhase = "idle";
|
|
84
|
+
private iteration = 0;
|
|
85
|
+
private maxIterations: number;
|
|
86
|
+
private stepLabel: string = "";
|
|
87
|
+
private errors = 0;
|
|
88
|
+
private warnings = 0;
|
|
89
|
+
private readonly onRender?: (lines: string[]) => void;
|
|
90
|
+
|
|
91
|
+
constructor(options: LoopWidgetOptions = {}) {
|
|
92
|
+
this.maxIterations = 10;
|
|
93
|
+
this.onRender = options.onRender;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── Mutation API ───────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
/** Called when the loop starts a new iteration. */
|
|
99
|
+
startIteration(iter: number, max: number): void {
|
|
100
|
+
this.iteration = iter;
|
|
101
|
+
this.maxIterations = max;
|
|
102
|
+
// Clear per-iteration counts; keep file records for history
|
|
103
|
+
for (const rec of this.files.values()) {
|
|
104
|
+
rec.writeSteps = 0;
|
|
105
|
+
rec.reviewsPassed = 0;
|
|
106
|
+
rec.blockers = 0;
|
|
107
|
+
}
|
|
108
|
+
this.errors = 0;
|
|
109
|
+
this.warnings = 0;
|
|
110
|
+
this.render();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Called when the loop transitions to a new phase. */
|
|
114
|
+
setPhase(phase: LoopPhase, label?: string): void {
|
|
115
|
+
this.phase = phase;
|
|
116
|
+
this.stepLabel = label ?? this.phaseLabel(phase);
|
|
117
|
+
this.render();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Called when the coder writes to a file. */
|
|
121
|
+
recordWrite(filePath: string): void {
|
|
122
|
+
const rec = this.getOrCreate(filePath);
|
|
123
|
+
rec.writeSteps++;
|
|
124
|
+
rec.lastPhase = "writing";
|
|
125
|
+
rec.touchedAt = Date.now();
|
|
126
|
+
this.render();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Called when the reviewer reports blockers for a file. */
|
|
130
|
+
recordBlockers(filePath: string, count: number): void {
|
|
131
|
+
const rec = this.getOrCreate(filePath);
|
|
132
|
+
rec.blockers = count;
|
|
133
|
+
rec.lastPhase = count > 0 ? "blocked" : "reviewing";
|
|
134
|
+
rec.touchedAt = Date.now();
|
|
135
|
+
if (count > 0) {
|
|
136
|
+
this.errors += count;
|
|
137
|
+
}
|
|
138
|
+
this.render();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Called when the reviewer passes a file (no blockers). */
|
|
142
|
+
recordReviewPass(filePath: string): void {
|
|
143
|
+
const rec = this.getOrCreate(filePath);
|
|
144
|
+
rec.reviewsPassed++;
|
|
145
|
+
rec.blockers = 0;
|
|
146
|
+
rec.lastPhase = "reviewing";
|
|
147
|
+
rec.touchedAt = Date.now();
|
|
148
|
+
this.render();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Called on surge-pause / quota-wait events. */
|
|
152
|
+
setSurgePause(resetAt?: Date): void {
|
|
153
|
+
this.phase = "surge-pause";
|
|
154
|
+
this.stepLabel = resetAt
|
|
155
|
+
? `surge (${this.formatResetTime(resetAt)})`
|
|
156
|
+
: "surge pause";
|
|
157
|
+
this.render();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Called on unrecoverable error. */
|
|
161
|
+
setError(message: string): void {
|
|
162
|
+
this.phase = "error";
|
|
163
|
+
this.stepLabel = `error: ${message}`;
|
|
164
|
+
this.render();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Called when the loop completes with a verdict. */
|
|
168
|
+
setComplete(verdict: "approved" | "rejected" | "max_iterations"): void {
|
|
169
|
+
this.phase = "complete";
|
|
170
|
+
const labels = {
|
|
171
|
+
approved: "✓ loop complete",
|
|
172
|
+
rejected: "✗ loop rejected",
|
|
173
|
+
"max_iterations": `⚠ max iterations (${this.maxIterations})`,
|
|
174
|
+
};
|
|
175
|
+
this.stepLabel = labels[verdict];
|
|
176
|
+
this.render();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Reset to idle state. */
|
|
180
|
+
reset(): void {
|
|
181
|
+
this.phase = "idle";
|
|
182
|
+
this.iteration = 0;
|
|
183
|
+
this.errors = 0;
|
|
184
|
+
this.warnings = 0;
|
|
185
|
+
this.stepLabel = "";
|
|
186
|
+
this.files.clear();
|
|
187
|
+
this.render();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ─── Render (pi host + standalone) ────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Build widget lines for the pi host's `setWidget()` renderer.
|
|
194
|
+
*
|
|
195
|
+
* Returns 0-3 lines:
|
|
196
|
+
* Line 1: `harness [iter 2/5] ✍ coder !2W` — header + summary
|
|
197
|
+
* Line 2: ` surge pause — resets 19:17` — surge detail (when applicable)
|
|
198
|
+
* Line 3: ` ●surge.ts ✍2 🔍3 !1` — file rows (up to 5)
|
|
199
|
+
*/
|
|
200
|
+
renderWidget(width: number, theme: { fg: (color: ColorKey, s: string) => string }): string[] {
|
|
201
|
+
const dim = (s: string) => theme.fg("dim", s);
|
|
202
|
+
const red = (s: string) => theme.fg("error", s);
|
|
203
|
+
const yellow = (s: string) => theme.fg("warning", s);
|
|
204
|
+
const green = (s: string) => theme.fg("success", s);
|
|
205
|
+
const cyan = (s: string) => theme.fg("accent", s);
|
|
206
|
+
const w = Math.max(1, width || 80);
|
|
207
|
+
|
|
208
|
+
const lines: string[] = [];
|
|
209
|
+
|
|
210
|
+
// ── Header ──────────────────────────────────────────────────────────────
|
|
211
|
+
const iterStr = this.iteration > 0
|
|
212
|
+
? dim(`iter ${this.iteration}/${this.maxIterations}`)
|
|
213
|
+
: "";
|
|
214
|
+
// Use stepLabel if set (custom phase label, surge detail, error message)
|
|
215
|
+
const phaseStr = this.stepLabel
|
|
216
|
+
? this.phaseIcon(this.phase) + " " + this.stepLabel
|
|
217
|
+
: this.phaseIcon(this.phase) + " " + this.phaseLabel(this.phase);
|
|
218
|
+
|
|
219
|
+
// Summary chunks (matches pi-lens style: !NW ●ME)
|
|
220
|
+
const errorChunk = this.errors > 0
|
|
221
|
+
? red(`●${this.errors}E`)
|
|
222
|
+
: "";
|
|
223
|
+
const warningChunk = this.warnings > 0
|
|
224
|
+
? yellow(`!${this.warnings}W`)
|
|
225
|
+
: "";
|
|
226
|
+
const summary = errorChunk
|
|
227
|
+
? errorChunk + (warningChunk ? " " + warningChunk : "")
|
|
228
|
+
: warningChunk
|
|
229
|
+
? warningChunk
|
|
230
|
+
: this.phase === "idle" || this.phase === "complete"
|
|
231
|
+
? green("✓")
|
|
232
|
+
: dim("…");
|
|
233
|
+
|
|
234
|
+
const parts = [
|
|
235
|
+
cyan("harness"),
|
|
236
|
+
iterStr,
|
|
237
|
+
phaseStr,
|
|
238
|
+
summary,
|
|
239
|
+
].filter(Boolean).join(" ");
|
|
240
|
+
|
|
241
|
+
lines.push(this.fitLine(` ${parts}`, w));
|
|
242
|
+
|
|
243
|
+
// ── Surge detail ───────────────────────────────────────────────────────
|
|
244
|
+
if (this.phase === "surge-pause" && this.stepLabel) {
|
|
245
|
+
lines.push(this.fitLine(` ${yellow("▸")} ${dim(this.stepLabel)}`, w));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ── Error detail ───────────────────────────────────────────────────────
|
|
249
|
+
if (this.phase === "error" && this.stepLabel) {
|
|
250
|
+
lines.push(this.fitLine(` ${red("✗")} ${this.stepLabel}`, w));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── File rows ─────────────────────────────────────────────────────────
|
|
254
|
+
const sorted = this.fileRecords()
|
|
255
|
+
.sort((a, b) => b.touchedAt - a.touchedAt)
|
|
256
|
+
.slice(0, 5);
|
|
257
|
+
|
|
258
|
+
for (const rec of sorted) {
|
|
259
|
+
const tier = this.classifyFileTier(rec);
|
|
260
|
+
const dot = tier === "blocking"
|
|
261
|
+
? red("●")
|
|
262
|
+
: tier === "warning"
|
|
263
|
+
? yellow("!")
|
|
264
|
+
: dim("·");
|
|
265
|
+
const base = this.fileBasename(rec.filePath);
|
|
266
|
+
const steps = rec.writeSteps > 0 ? dim(` ✍${rec.writeSteps}`) : "";
|
|
267
|
+
const reviews = rec.reviewsPassed > 0 ? dim(` 🔍${rec.reviewsPassed}`) : "";
|
|
268
|
+
const blockers = rec.blockers > 0 ? red(` !${rec.blockers}`) : "";
|
|
269
|
+
const line = ` ${dot} ${base}${steps}${reviews}${blockers}`;
|
|
270
|
+
lines.push(this.fitLine(line, w));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return lines;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Console fallback (standalone mode). */
|
|
277
|
+
toConsoleString(): string {
|
|
278
|
+
const phase = this.phaseIcon(this.phase) + " " + this.stepLabel;
|
|
279
|
+
const iter = this.iteration > 0 ? ` [iter ${this.iteration}/${this.maxIterations}]` : "";
|
|
280
|
+
const err = this.errors > 0 ? ` ●${this.errors}E` : "";
|
|
281
|
+
const warn = this.warnings > 0 ? ` !${this.warnings}W` : "";
|
|
282
|
+
const files = [...this.files.values()]
|
|
283
|
+
.filter(r => r.writeSteps > 0 || r.blockers > 0)
|
|
284
|
+
.sort((a, b) => b.touchedAt - a.touchedAt)
|
|
285
|
+
.slice(0, 3)
|
|
286
|
+
.map(r => `${this.fileBasename(r.filePath)}(${r.writeSteps}w/${r.blockers}b)`)
|
|
287
|
+
.join(", ");
|
|
288
|
+
|
|
289
|
+
const parts = [
|
|
290
|
+
`harness${iter}`,
|
|
291
|
+
phase,
|
|
292
|
+
].filter(Boolean).join(" ");
|
|
293
|
+
|
|
294
|
+
return [
|
|
295
|
+
parts + err + warn,
|
|
296
|
+
files ? ` files: ${files}` : "",
|
|
297
|
+
].filter(Boolean).join("\n");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ─── Pi host widget factory (used by index.ts / ExtensionUIContext) ────────
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Returns a pi-tui Component factory for use with `ui.setWidget("harness-loop", factory)`.
|
|
304
|
+
* The caller (pi host integration) owns the `setWidget` call — this factory is just
|
|
305
|
+
* the render function.
|
|
306
|
+
*
|
|
307
|
+
* Usage in pi host:
|
|
308
|
+
* ```
|
|
309
|
+
* setWidget("harness-loop", makeWidgetRenderer(widget, theme), { placement: "belowEditor" });
|
|
310
|
+
* ```
|
|
311
|
+
*/
|
|
312
|
+
/**
|
|
313
|
+
* Returns a factory suitable for `ui.setWidget("harness-loop", factory)` in the
|
|
314
|
+
* pi host. The factory's `render(width)` is called by the pi-tui render loop
|
|
315
|
+
* on every frame; `invalidate` is called by the harness to trigger a re-render.
|
|
316
|
+
*/
|
|
317
|
+
makeRenderer(): {
|
|
318
|
+
factory: (tui: TUI, theme: Theme) => {
|
|
319
|
+
render: (width: number) => string[];
|
|
320
|
+
invalidate: () => void;
|
|
321
|
+
dispose: () => void;
|
|
322
|
+
};
|
|
323
|
+
invalidate: () => void;
|
|
324
|
+
} {
|
|
325
|
+
// Shared invalidate trigger — both the harness and the pi-tui component call this
|
|
326
|
+
const invalidate = () => { /* mark dirty for next frame */ };
|
|
327
|
+
|
|
328
|
+
const factory = (_tui: TUI, theme: Theme) => {
|
|
329
|
+
type ColorKey = "dim" | "error" | "warning" | "success" | "accent";
|
|
330
|
+
const fg = (color: ColorKey, s: string): string => theme.fg(color, s);
|
|
331
|
+
|
|
332
|
+
const render = (width: number): string[] => {
|
|
333
|
+
return this.renderWidget(width, { fg });
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// Called by harness to signal the widget needs to re-render
|
|
337
|
+
const widgetInvalidate = () => { /* trigger invalidate */ };
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
render,
|
|
341
|
+
invalidate: widgetInvalidate,
|
|
342
|
+
dispose: () => { /* clean up */ },
|
|
343
|
+
};
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
return { factory, invalidate };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ─── Internals ─────────────────────────────────────────────────────────────
|
|
350
|
+
|
|
351
|
+
private getOrCreate(filePath: string): FileStepRecord {
|
|
352
|
+
if (!this.files.has(filePath)) {
|
|
353
|
+
this.files.set(filePath, {
|
|
354
|
+
filePath,
|
|
355
|
+
writeSteps: 0,
|
|
356
|
+
reviewsPassed: 0,
|
|
357
|
+
blockers: 0,
|
|
358
|
+
lastPhase: "idle",
|
|
359
|
+
touchedAt: Date.now(),
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
return this.files.get(filePath)!;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private fileRecords(): FileStepRecord[] {
|
|
366
|
+
return [...this.files.values()];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private fileBasename(filePath: string): string {
|
|
370
|
+
const segs = filePath.replace(/\\/g, "/").split("/");
|
|
371
|
+
return segs[segs.length - 1] ?? filePath;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private classifyFileTier(rec: FileStepRecord): "blocking" | "warning" | "clean" {
|
|
375
|
+
if (rec.blockers > 0) return "blocking";
|
|
376
|
+
if (rec.writeSteps > 0 || rec.reviewsPassed > 0) return "warning";
|
|
377
|
+
return "clean";
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private phaseLabel(phase: LoopPhase): string {
|
|
381
|
+
const labels: Record<LoopPhase, string> = {
|
|
382
|
+
idle: "idle",
|
|
383
|
+
planning: "planning",
|
|
384
|
+
writing: "coding",
|
|
385
|
+
reviewing: "reviewing",
|
|
386
|
+
fixing: "fixing",
|
|
387
|
+
approving: "approving",
|
|
388
|
+
blocked: "blocked",
|
|
389
|
+
"surge-pause": "surge",
|
|
390
|
+
complete: "complete",
|
|
391
|
+
error: "error",
|
|
392
|
+
};
|
|
393
|
+
return labels[phase];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private phaseIcon(phase: LoopPhase): string {
|
|
397
|
+
const icons: Record<LoopPhase, string> = {
|
|
398
|
+
idle: "○",
|
|
399
|
+
planning: "🧭",
|
|
400
|
+
writing: "✍",
|
|
401
|
+
reviewing: "🔍",
|
|
402
|
+
fixing: "🔧",
|
|
403
|
+
approving: "✅",
|
|
404
|
+
blocked: "⛔",
|
|
405
|
+
"surge-pause": "⏸",
|
|
406
|
+
complete: "✓",
|
|
407
|
+
error: "✗",
|
|
408
|
+
};
|
|
409
|
+
return icons[phase];
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private formatResetTime(date: Date): string {
|
|
413
|
+
const now = new Date();
|
|
414
|
+
const diff = date.getTime() - now.getTime();
|
|
415
|
+
if (diff <= 0) return "now";
|
|
416
|
+
const mins = Math.ceil(diff / 60_000);
|
|
417
|
+
if (mins < 60) return `in ${mins}m`;
|
|
418
|
+
const hrs = Math.floor(mins / 60);
|
|
419
|
+
const rem = mins % 60;
|
|
420
|
+
return rem > 0 ? `in ${hrs}h ${rem}m` : `in ${hrs}h`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Truncate `line` to `width` visible columns.
|
|
425
|
+
* Uses `visibleWidth` (not raw char count) so ANSI codes don't cause
|
|
426
|
+
* false wrapping — matching how the terminal measures columns.
|
|
427
|
+
*/
|
|
428
|
+
private fitLine(line: string, width: number): string {
|
|
429
|
+
if (visibleWidth(line) <= width) return line;
|
|
430
|
+
// Walk forward, stopping when adding the next character would exceed width
|
|
431
|
+
let w = 0;
|
|
432
|
+
let cut = 0;
|
|
433
|
+
for (let i = 0; i < line.length; i++) {
|
|
434
|
+
const cp = line.codePointAt(i)!;
|
|
435
|
+
if (cp === 0x1b) {
|
|
436
|
+
// Skip ANSI sequences (don't count toward width)
|
|
437
|
+
while (i < line.length && line[i] !== "m" && line[i] !== "H" && line[i] !== "J") i++;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
const charW = cp > 0xffff ? 2 : 1;
|
|
441
|
+
if (w + charW > width - 1) break; // need room for the ellipsis
|
|
442
|
+
w += charW;
|
|
443
|
+
cut = i + (cp > 0xffff ? 2 : 1);
|
|
444
|
+
}
|
|
445
|
+
return line.slice(0, cut) + "…";
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private render(): void {
|
|
449
|
+
const console = this.toConsoleString();
|
|
450
|
+
this.onRender?.(console.split("\n"));
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ─── Singleton factory ───────────────────────────────────────────────────────
|
|
455
|
+
|
|
456
|
+
let _instance: LoopWidget | undefined;
|
|
457
|
+
|
|
458
|
+
export function getLoopWidget(): LoopWidget {
|
|
459
|
+
if (!_instance) {
|
|
460
|
+
_instance = new LoopWidget();
|
|
461
|
+
}
|
|
462
|
+
return _instance;
|
|
463
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-harness/capability-registry",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.39",
|
|
4
4
|
"description": "Capability Registry for pi-harness-runtime — tracks model capabilities and enables capability-based routing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|