pi-harness-runtime 1.1.38 → 1.1.41
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 +74 -1
- package/harness/langchain/graph.ts +3 -0
- package/harness/langchain/widget.test.ts +251 -0
- package/harness/langchain/widget.ts +480 -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
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
import { NotificationCenter } from "../../packages/notification/dist/notification-center.js";
|
|
49
49
|
|
|
50
50
|
import { invokeWithSurgeRetry, type SurgePolicy } from "./surge.js";
|
|
51
|
+
import type { LoopWidget } from "./widget.js";
|
|
51
52
|
import { createLoopCheckpointer } from "./checkpointer.js";
|
|
52
53
|
|
|
53
54
|
import {
|
|
@@ -111,6 +112,12 @@ export interface DaemonConfig {
|
|
|
111
112
|
* Default: none (no tunnel management)
|
|
112
113
|
*/
|
|
113
114
|
tunnelCommand?: string;
|
|
115
|
+
/**
|
|
116
|
+
* Optional widget for TUI / status-line display.
|
|
117
|
+
* When set, the daemon updates the widget at each loop transition
|
|
118
|
+
* (mirrors pi-lens footer style: !NW ●ME).
|
|
119
|
+
*/
|
|
120
|
+
widget?: LoopWidget;
|
|
114
121
|
}
|
|
115
122
|
|
|
116
123
|
// ─── TriggeredTask ──────────────────────────────────────────────────────────
|
|
@@ -668,6 +675,7 @@ export class LoopDaemon {
|
|
|
668
675
|
private readonly busWatcher: BusWatcher;
|
|
669
676
|
private readonly bdWatcher: BdTasksWatcher;
|
|
670
677
|
private readonly tunnelMonitor: TunnelMonitor | null;
|
|
678
|
+
private readonly widget: LoopWidget | null;
|
|
671
679
|
|
|
672
680
|
/** Currently running tasks (taskId → RunningTask) */
|
|
673
681
|
private readonly running = new Map<string, RunningTask>();
|
|
@@ -691,6 +699,7 @@ export class LoopDaemon {
|
|
|
691
699
|
workspace: config.workspace ?? getHerdrWorkspace(),
|
|
692
700
|
notificationConfig: config.notificationConfig ?? undefined,
|
|
693
701
|
tunnelCommand: config.tunnelCommand,
|
|
702
|
+
widget: config.widget,
|
|
694
703
|
} as Required<DaemonConfig>;
|
|
695
704
|
|
|
696
705
|
this.agentId = this.config.agentId;
|
|
@@ -718,6 +727,7 @@ export class LoopDaemon {
|
|
|
718
727
|
}
|
|
719
728
|
|
|
720
729
|
this.inboxWatcher = new InboxWatcher(this.inboxDir, this.config.pollMs);
|
|
730
|
+
this.widget = this.config.widget ?? null;
|
|
721
731
|
this.busWatcher = new BusWatcher(this.bus, this.config.pollMs);
|
|
722
732
|
this.bdWatcher = new BdTasksWatcher(this.config.pollMs);
|
|
723
733
|
|
|
@@ -879,11 +889,12 @@ export class LoopDaemon {
|
|
|
879
889
|
maxIterations: this.config.maxIterations,
|
|
880
890
|
}));
|
|
881
891
|
|
|
882
|
-
// Inject transition publishing into onStep
|
|
892
|
+
// Inject transition publishing into onStep (also drives the widget)
|
|
883
893
|
const originalOnStep = deps.onStep;
|
|
884
894
|
deps.onStep = (step, state) => {
|
|
885
895
|
originalOnStep?.(step, state);
|
|
886
896
|
this._publishTransition(loopId, step, state);
|
|
897
|
+
this._updateWidget(step, state);
|
|
887
898
|
};
|
|
888
899
|
|
|
889
900
|
// ── Run the graph (surge-aware: 529/overloaded pauses auto-resume) ──
|
|
@@ -952,6 +963,17 @@ export class LoopDaemon {
|
|
|
952
963
|
|
|
953
964
|
log(`Loop finished: ${verdict} after ${iterations} iteration(s)`);
|
|
954
965
|
|
|
966
|
+
// Update widget on loop completion
|
|
967
|
+
if (this.widget) {
|
|
968
|
+
this.widget.setComplete(
|
|
969
|
+
verdict === "approved"
|
|
970
|
+
? "approved"
|
|
971
|
+
: verdict === "blocked"
|
|
972
|
+
? "rejected"
|
|
973
|
+
: "max_iterations",
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
|
|
955
977
|
// ── Gate check after blocked verdict ────────────────────────────────
|
|
956
978
|
if (
|
|
957
979
|
verdict === "blocked" &&
|
|
@@ -1034,6 +1056,57 @@ export class LoopDaemon {
|
|
|
1034
1056
|
}
|
|
1035
1057
|
}
|
|
1036
1058
|
|
|
1059
|
+
/**
|
|
1060
|
+
* Drive the TUI widget through loop transitions.
|
|
1061
|
+
* Called from onStep within _runTask.
|
|
1062
|
+
*/
|
|
1063
|
+
private _updateWidget(step: string, state: LoopState): void {
|
|
1064
|
+
const w = this.widget;
|
|
1065
|
+
if (!w) return;
|
|
1066
|
+
|
|
1067
|
+
switch (step) {
|
|
1068
|
+
case "plan": {
|
|
1069
|
+
w.setPhase("planning", "GPT planning");
|
|
1070
|
+
break;
|
|
1071
|
+
}
|
|
1072
|
+
case "write": {
|
|
1073
|
+
w.setPhase("writing", "MiniMax coding");
|
|
1074
|
+
// After a write step, bump the iteration counter
|
|
1075
|
+
w.startIteration(state.iteration, this.config.maxIterations);
|
|
1076
|
+
break;
|
|
1077
|
+
}
|
|
1078
|
+
case "review": {
|
|
1079
|
+
w.setPhase("reviewing", "GPT reviewing");
|
|
1080
|
+
// Process review comments — one record per file
|
|
1081
|
+
const comments = state.review?.comments ?? [];
|
|
1082
|
+
if (comments.length === 0) break;
|
|
1083
|
+
|
|
1084
|
+
// Group blocking comments by file (severity: "critical" = blocking)
|
|
1085
|
+
const byFile = new Map<string, number>();
|
|
1086
|
+
for (const c of comments) {
|
|
1087
|
+
if (c.severity === "critical") {
|
|
1088
|
+
const prev = byFile.get(c.file ?? "") ?? 0;
|
|
1089
|
+
byFile.set(c.file ?? "", prev + 1);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
for (const [file, count] of byFile) {
|
|
1093
|
+
if (count > 0) {
|
|
1094
|
+
w.recordBlockers(file, count);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
// Files with no blocking comments pass
|
|
1098
|
+
const blockingFiles = new Set(byFile.keys());
|
|
1099
|
+
for (const c of comments) {
|
|
1100
|
+
const f = c.file ?? "";
|
|
1101
|
+
if (!blockingFiles.has(f) && f) {
|
|
1102
|
+
w.recordReviewPass(f);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
break;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1037
1110
|
private _reviewReportPath(loopId: string, iteration: number): string {
|
|
1038
1111
|
return `${ensureHerdrWorkspace().reviews}/${loopId}-review-${iteration}.md`;
|
|
1039
1112
|
}
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
type ReviewVerdict,
|
|
25
25
|
ReviewVerdictSchema,
|
|
26
26
|
} from "./agents.js";
|
|
27
|
+
import type { LoopWidget } from "./widget.js";
|
|
27
28
|
|
|
28
29
|
// ─── State ──────────────────────────────────────────────────────────────────
|
|
29
30
|
|
|
@@ -58,6 +59,8 @@ export interface LoopDeps {
|
|
|
58
59
|
review: (plan: string, code: string) => Promise<ReviewVerdict>;
|
|
59
60
|
maxIterations: number;
|
|
60
61
|
onStep?: (step: string, state: LoopState) => void;
|
|
62
|
+
/** Optional widget for TUI / status-line display (mirrors pi-lens footer style). */
|
|
63
|
+
widget?: LoopWidget;
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
// ─── Node implementations ───────────────────────────────────────────────────
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LoopWidget tests — T14
|
|
3
|
+
*
|
|
4
|
+
* Run:
|
|
5
|
+
* bun test harness/langchain/widget.test.ts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, beforeEach } from "node:test";
|
|
9
|
+
import assert from "node:assert";
|
|
10
|
+
import { LoopWidget } from "./widget.js";
|
|
11
|
+
|
|
12
|
+
// ─── Fixtures ────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
const stubTheme = {
|
|
15
|
+
fg: (color: string, s: string): string => {
|
|
16
|
+
const codes: Record<string, string> = {
|
|
17
|
+
dim: `\x1b[2m${s}\x1b[0m`,
|
|
18
|
+
error: `\x1b[31m${s}\x1b[0m`,
|
|
19
|
+
warning: `\x1b[33m${s}\x1b[0m`,
|
|
20
|
+
success: `\x1b[32m${s}\x1b[0m`,
|
|
21
|
+
accent: `\x1b[36m${s}\x1b[0m`,
|
|
22
|
+
};
|
|
23
|
+
return codes[color] ?? s;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
describe("T14 — LoopWidget", () => {
|
|
28
|
+
let widget: LoopWidget;
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
widget = new LoopWidget();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// ─── Idle state ────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
it("starts in idle phase", () => {
|
|
37
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
38
|
+
assert.match(lines[0], /idle/);
|
|
39
|
+
assert.match(lines[0], /harness/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// ─── Phase transitions ──────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
it("setPhase changes the phase label", () => {
|
|
45
|
+
widget.setPhase("writing");
|
|
46
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
47
|
+
assert.match(lines[0], /coding/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("setPhase with custom label uses the label", () => {
|
|
51
|
+
widget.setPhase("planning", "GPT planning");
|
|
52
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
53
|
+
assert.match(lines[0], /GPT planning/);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("surge-pause shows detail line", () => {
|
|
57
|
+
widget.setSurgePause(new Date(Date.now() + 30 * 60_000)); // 30 min
|
|
58
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
59
|
+
// Label is "surge (in 30m)"
|
|
60
|
+
assert.ok(lines.some((l) => l.includes("surge") && l.includes("in 30m")));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("setError shows error detail", () => {
|
|
64
|
+
widget.setError("model timeout");
|
|
65
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
66
|
+
assert.ok(lines.some((l) => l.includes("error")));
|
|
67
|
+
assert.ok(lines.some((l) => l.includes("model timeout")));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("setComplete approved shows ✓ loop complete", () => {
|
|
71
|
+
widget.setComplete("approved");
|
|
72
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
73
|
+
assert.match(lines[0], /loop complete/);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("setComplete rejected shows ✗ loop rejected", () => {
|
|
77
|
+
widget.setComplete("rejected");
|
|
78
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
79
|
+
assert.match(lines[0], /loop rejected/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("setComplete max_iterations shows iteration count", () => {
|
|
83
|
+
widget.setComplete("max_iterations");
|
|
84
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
85
|
+
assert.match(lines[0], /max iterations/);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ─── Iteration tracking ────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
it("startIteration sets iteration and max", () => {
|
|
91
|
+
widget.startIteration(3, 5);
|
|
92
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
93
|
+
assert.match(lines[0], /iter 3\/5/);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("startIteration resets per-file counters but keeps file records", () => {
|
|
97
|
+
widget.recordWrite("src/a.ts");
|
|
98
|
+
widget.recordBlockers("src/a.ts", 2);
|
|
99
|
+
widget.startIteration(2, 5);
|
|
100
|
+
// Files accumulate across iterations (same codebase); only counters reset.
|
|
101
|
+
// After reset, a.ts shows with 0 write steps and 0 blockers.
|
|
102
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
103
|
+
const fileLines = lines.filter((l) => l.includes("a.ts"));
|
|
104
|
+
assert.ok(fileLines.length > 0, "file should still appear");
|
|
105
|
+
// No blocker count in the file row (reset to 0)
|
|
106
|
+
assert.ok(
|
|
107
|
+
!fileLines.some((l) => l.includes("!2")),
|
|
108
|
+
"blocker count should be reset",
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ─── File record tracking ──────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
it("recordWrite shows file in widget", () => {
|
|
115
|
+
widget.recordWrite("src/foo.ts");
|
|
116
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
117
|
+
assert.ok(lines.some((l) => l.includes("foo.ts")));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("recordWrite accumulates across calls", () => {
|
|
121
|
+
widget.recordWrite("src/bar.ts");
|
|
122
|
+
widget.recordWrite("src/bar.ts");
|
|
123
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
124
|
+
assert.ok(lines.some((l) => l.includes("bar.ts") && l.includes("✍2")));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("recordBlockers shows blocker count", () => {
|
|
128
|
+
widget.recordBlockers("src/bad.ts", 2);
|
|
129
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
130
|
+
assert.ok(lines.some((l) => l.includes("bad.ts") && l.includes("!2")));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("recordReviewPass clears blockers and increments passes", () => {
|
|
134
|
+
widget.recordBlockers("src/ok.ts", 1);
|
|
135
|
+
widget.recordReviewPass("src/ok.ts");
|
|
136
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
137
|
+
assert.ok(lines.some((l) => l.includes("ok.ts") && l.includes("🔍1")));
|
|
138
|
+
// No blocker count
|
|
139
|
+
assert.ok(
|
|
140
|
+
!lines.some(
|
|
141
|
+
(l) => l.includes("ok.ts") && l.includes("!") && !l.includes("🔍"),
|
|
142
|
+
),
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ─── Summary counts ────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
it("no errors/warnings shows ✓", () => {
|
|
149
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
150
|
+
assert.ok(lines[0].includes("✓") || lines[0].includes("harness"));
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("errors shown as ●NE", () => {
|
|
154
|
+
widget.recordBlockers("src/err.ts", 3);
|
|
155
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
156
|
+
assert.ok(lines.some((l) => l.includes("●3E")));
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// ─── Surge pause ──────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
it("setSurgePause shows reset time in minutes/hours", () => {
|
|
162
|
+
const resetAt = new Date(Date.now() + 90 * 60_000); // 90 minutes → "in 1h 30m"
|
|
163
|
+
widget.setSurgePause(resetAt);
|
|
164
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
165
|
+
const surgeLine = lines.find((l) => l.includes("surge"));
|
|
166
|
+
assert.ok(surgeLine !== undefined);
|
|
167
|
+
assert.match(surgeLine, /\(in \d+h/); // "in 1h 30m" matches
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("setSurgePause shows 'now' for past dates", () => {
|
|
171
|
+
const resetAt = new Date(Date.now() - 1000);
|
|
172
|
+
widget.setSurgePause(resetAt);
|
|
173
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
174
|
+
// Label format: "surge (now)"
|
|
175
|
+
assert.ok(lines.some((l) => l.includes("surge") && l.includes("(now)")));
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ─── Console fallback ─────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
it("toConsoleString returns human-readable output", () => {
|
|
181
|
+
widget.startIteration(2, 5);
|
|
182
|
+
widget.setPhase("writing", "coding");
|
|
183
|
+
widget.recordWrite("src/test.ts");
|
|
184
|
+
const output = widget.toConsoleString();
|
|
185
|
+
assert.match(output, /harness/);
|
|
186
|
+
assert.match(output, /iter 2\/5/);
|
|
187
|
+
assert.match(output, /coding/);
|
|
188
|
+
assert.match(output, /test\.ts/);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// ─── Width / truncation ───────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
it("truncates long file names with …", () => {
|
|
194
|
+
widget.recordWrite(
|
|
195
|
+
"/very/long/path/to/a/very/long/file/name/that/exceeds/width.ts",
|
|
196
|
+
);
|
|
197
|
+
const lines = widget.renderWidget(40, stubTheme);
|
|
198
|
+
for (const line of lines) {
|
|
199
|
+
const stripped = line.replace(/\x1b\[[0-9;]*m/g, "");
|
|
200
|
+
assert.ok(stripped.length <= 41, `line too long: ${stripped}`);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ─── reset() ─────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
it("reset() clears all state", () => {
|
|
207
|
+
widget.startIteration(2, 5);
|
|
208
|
+
widget.setPhase("writing");
|
|
209
|
+
widget.recordWrite("src/a.ts");
|
|
210
|
+
widget.reset();
|
|
211
|
+
const lines = widget.renderWidget(80, stubTheme);
|
|
212
|
+
assert.match(lines[0], /idle/);
|
|
213
|
+
assert.ok(!lines.some((l) => l.includes("a.ts")));
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ─── makeRenderer ─────────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
it("makeRenderer returns factory and invalidate", () => {
|
|
219
|
+
const { factory, invalidate } = widget.makeRenderer();
|
|
220
|
+
assert.equal(typeof factory, "function");
|
|
221
|
+
assert.equal(typeof invalidate, "function");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("factory returns render + invalidate + dispose", () => {
|
|
225
|
+
const { factory } = widget.makeRenderer();
|
|
226
|
+
const comp = factory({} as any, stubTheme as any);
|
|
227
|
+
assert.equal(typeof comp.render, "function");
|
|
228
|
+
assert.equal(typeof comp.invalidate, "function");
|
|
229
|
+
assert.equal(typeof comp.dispose, "function");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("factory render() returns widget lines", () => {
|
|
233
|
+
widget.setPhase("planning");
|
|
234
|
+
const { factory } = widget.makeRenderer();
|
|
235
|
+
const comp = factory({} as any, stubTheme as any);
|
|
236
|
+
const lines = comp.render(80);
|
|
237
|
+
assert.ok(Array.isArray(lines));
|
|
238
|
+
assert.ok(lines.length > 0);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// ─── getLoopWidget singleton ────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
it("getLoopWidget returns the same instance", () => {
|
|
244
|
+
// Dynamic import to avoid module caching issues
|
|
245
|
+
import("./widget.js").then((m) => {
|
|
246
|
+
const a = m.getLoopWidget();
|
|
247
|
+
const b = m.getLoopWidget();
|
|
248
|
+
assert.strictEqual(a, b);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
});
|