pi-plans 0.1.2 → 0.2.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 +43 -17
- package/index.ts +34 -59
- package/package.json +1 -1
- package/references/pi-planning-workflow.md +6 -3
- package/references/state-and-config.md +1 -1
- package/src/autocomplete.ts +163 -0
- package/src/compaction.ts +502 -0
- package/src/exec.ts +557 -333
- package/src/plan.ts +37 -0
- package/src/query-hook.ts +82 -0
- package/src/refine-ui-helpers.ts +89 -0
- package/src/refine-ui-state.ts +78 -0
- package/src/refine-ui.ts +322 -0
- package/src/state.ts +9 -27
- package/src/subagent.ts +196 -69
- package/tests/autocomplete.test.ts +142 -0
- package/tests/compaction.test.ts +74 -0
- package/tests/exec.test.ts +153 -248
- package/tests/execute-plan.test.ts +65 -0
- package/tests/plan.test.ts +11 -1
- package/tests/plans.test.ts +6 -5
- package/tests/query-hook.test.ts +82 -0
- package/tests/refine-ui.test.ts +127 -0
- package/tests/state.test.ts +12 -15
- package/tests/subagent.test.ts +114 -0
- package/tools/ask-choice.ts +22 -0
- package/tools/execute-plan.ts +7 -39
- package/tools/plans.ts +1 -18
- package/tools/refine.ts +125 -71
- package/src/execution-panel.ts +0 -633
- package/tests/execution-panel.test.ts +0 -234
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
import * as assert from "node:assert/strict";
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
3
|
-
import * as fs from "node:fs";
|
|
4
|
-
import * as os from "node:os";
|
|
5
|
-
import * as path from "node:path";
|
|
6
|
-
import { after, before, describe, it } from "node:test";
|
|
7
|
-
import {
|
|
8
|
-
captureRepoSnapshot,
|
|
9
|
-
completeCompletedItems,
|
|
10
|
-
createExecutionPanelState,
|
|
11
|
-
refreshExecutionPanel,
|
|
12
|
-
restorePanelState,
|
|
13
|
-
subtractSnapshots,
|
|
14
|
-
toggleExpanded,
|
|
15
|
-
truncateAnsi,
|
|
16
|
-
visibleWidth,
|
|
17
|
-
} from "../src/execution-panel.ts";
|
|
18
|
-
|
|
19
|
-
function initGitRepo(dir: string): void {
|
|
20
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
21
|
-
const git = (args: string[]): void => {
|
|
22
|
-
const result = spawnSync("git", args, { cwd: dir, stdio: "ignore" });
|
|
23
|
-
if (result.status !== 0) {
|
|
24
|
-
throw new Error(`git ${args.join(" ")} failed`);
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
git(["init"]);
|
|
28
|
-
git(["config", "user.name", "Pi Plans Test"]);
|
|
29
|
-
git(["config", "user.email", "test@example.com"]);
|
|
30
|
-
fs.writeFileSync(path.join(dir, "tracked.txt"), "line 1\n");
|
|
31
|
-
git(["add", "tracked.txt"]);
|
|
32
|
-
git(["commit", "-m", "baseline"]);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
describe("execution panel helpers", () => {
|
|
36
|
-
let tmpRoot: string;
|
|
37
|
-
let repo: string;
|
|
38
|
-
|
|
39
|
-
before(() => {
|
|
40
|
-
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "pi-plans-panel-"));
|
|
41
|
-
repo = path.join(tmpRoot, "repo");
|
|
42
|
-
initGitRepo(repo);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
after(() => {
|
|
46
|
-
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("captures diff snapshots and restores panel state", () => {
|
|
50
|
-
const baseline = captureRepoSnapshot(repo);
|
|
51
|
-
fs.appendFileSync(path.join(repo, "tracked.txt"), "line 2\n");
|
|
52
|
-
fs.writeFileSync(path.join(repo, "new.txt"), "new file\n");
|
|
53
|
-
const current = captureRepoSnapshot(repo);
|
|
54
|
-
const delta = subtractSnapshots(baseline, current);
|
|
55
|
-
assert.ok(delta.added >= 1);
|
|
56
|
-
assert.ok(delta.files >= 1);
|
|
57
|
-
|
|
58
|
-
const panel = createExecutionPanelState();
|
|
59
|
-
panel.expanded = true;
|
|
60
|
-
panel.baseline = baseline;
|
|
61
|
-
panel.lastSnapshot = current;
|
|
62
|
-
panel.touchedPaths = ["tracked.txt", "new.txt"];
|
|
63
|
-
panel.itemSummaries = {
|
|
64
|
-
"VC-001": {
|
|
65
|
-
summary: {
|
|
66
|
-
added: 1,
|
|
67
|
-
removed: 0,
|
|
68
|
-
files: 2,
|
|
69
|
-
paths: ["tracked.txt", "new.txt"],
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
|
-
const restored = restorePanelState(panel);
|
|
74
|
-
assert.ok(restored);
|
|
75
|
-
assert.equal(restored?.expanded, true);
|
|
76
|
-
assert.equal(restored?.baseline?.added, baseline.added);
|
|
77
|
-
assert.equal(restored?.itemSummaries["VC-001"]?.summary?.files, 2);
|
|
78
|
-
assert.deepEqual(restored?.touchedPaths, ["tracked.txt", "new.txt"]);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("records completed-item summaries with touched paths", () => {
|
|
82
|
-
fs.writeFileSync(path.join(repo, "tracked.txt"), "line 1\n");
|
|
83
|
-
fs.rmSync(path.join(repo, "new.txt"), { force: true });
|
|
84
|
-
const baseline = captureRepoSnapshot(repo);
|
|
85
|
-
fs.appendFileSync(path.join(repo, "tracked.txt"), "line 2\n");
|
|
86
|
-
fs.writeFileSync(path.join(repo, "new.txt"), "new file\n");
|
|
87
|
-
|
|
88
|
-
const execution = {
|
|
89
|
-
planPath: path.join(repo, "PLAN_v1.md"),
|
|
90
|
-
items: [{ id: "VC-001", text: "`VC-001` demo item", done: true }],
|
|
91
|
-
panel: createExecutionPanelState(),
|
|
92
|
-
};
|
|
93
|
-
execution.panel.baseline = baseline;
|
|
94
|
-
execution.panel.lastSnapshot = baseline;
|
|
95
|
-
execution.panel.touchedPaths = ["tracked.txt", "new.txt"];
|
|
96
|
-
|
|
97
|
-
const summary = completeCompletedItems(execution as any, repo, ["VC-001"]);
|
|
98
|
-
assert.ok(summary);
|
|
99
|
-
assert.ok((summary?.added ?? 0) >= 1);
|
|
100
|
-
assert.ok((summary?.files ?? 0) >= 1);
|
|
101
|
-
assert.deepEqual(execution.panel.itemSummaries["VC-001"]?.summary?.paths, ["tracked.txt", "new.txt"]);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("toggles expanded state", () => {
|
|
105
|
-
const execution = { planPath: path.join(repo, "PLAN_v1.md"), items: [] as any[], panel: createExecutionPanelState() };
|
|
106
|
-
assert.equal(toggleExpanded(execution), true);
|
|
107
|
-
assert.equal(toggleExpanded(execution), false);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("reuses one registered widget factory across refreshes", () => {
|
|
111
|
-
const execution: any = {
|
|
112
|
-
planPath: "/tmp/PLAN_v1.md",
|
|
113
|
-
items: [{ id: "VC-001", text: "item", done: false }],
|
|
114
|
-
startedAt: "2026-08-29T00:00:00Z",
|
|
115
|
-
usage: { inToks: 0, outToks: 0 },
|
|
116
|
-
panel: createExecutionPanelState(),
|
|
117
|
-
};
|
|
118
|
-
const factories: unknown[] = [];
|
|
119
|
-
const statuses: Array<string | undefined> = [];
|
|
120
|
-
let renderRequests = 0;
|
|
121
|
-
const theme = { fg: (_color: string, text: string) => `\u001b[38;5;2m${text}\u001b[39m`, strikethrough: (text: string) => text };
|
|
122
|
-
const ctx = {
|
|
123
|
-
ui: {
|
|
124
|
-
setWidget: (_key: string, factory: any) => void factories.push(factory),
|
|
125
|
-
setStatus: (_key: string, value: string | undefined) => void statuses.push(value),
|
|
126
|
-
theme: { fg: (_color: string, text: string) => text },
|
|
127
|
-
},
|
|
128
|
-
} as any;
|
|
129
|
-
|
|
130
|
-
// Collapsed execution renders no widget — the footer owns the ⌛ line.
|
|
131
|
-
refreshExecutionPanel(ctx, execution);
|
|
132
|
-
refreshExecutionPanel(ctx, execution); // idempotent while collapsed
|
|
133
|
-
assert.equal(factories.length, 0);
|
|
134
|
-
assert.match(String(statuses.at(-1)), /⌛ plans 0\/1/);
|
|
135
|
-
|
|
136
|
-
// Expanding registers the detail widget exactly once and clears the
|
|
137
|
-
// footer ⌛ line (the panel renders its own header).
|
|
138
|
-
toggleExpanded(execution);
|
|
139
|
-
refreshExecutionPanel(ctx, execution);
|
|
140
|
-
assert.equal(factories.length, 1);
|
|
141
|
-
assert.equal(statuses.at(-1), undefined, "expanded must clear the footer ⌛ line");
|
|
142
|
-
const widget = (factories[0] as any)({ requestRender: () => { renderRequests += 1; } }, theme);
|
|
143
|
-
const expandedLines = widget.render(80);
|
|
144
|
-
assert.match(expandedLines[0] ?? "", /⌛ plans 0\/1/, "panel header must be the first line");
|
|
145
|
-
assert.ok(expandedLines.length >= 1, "expanded render should list items");
|
|
146
|
-
|
|
147
|
-
refreshExecutionPanel(ctx, execution); // same host: reuse, no re-register
|
|
148
|
-
assert.equal(factories.length, 1);
|
|
149
|
-
assert.equal(renderRequests, 1, "same-host refresh must request a TUI render");
|
|
150
|
-
execution.items[0]!.done = true;
|
|
151
|
-
refreshExecutionPanel(ctx, execution);
|
|
152
|
-
assert.equal(factories.length, 1);
|
|
153
|
-
assert.equal(renderRequests, 2, "expanded progress refresh must request a TUI render");
|
|
154
|
-
assert.match(widget.render(80)[0] ?? "", /⌛ plans 1\/1/, "refresh must invalidate the live widget cache");
|
|
155
|
-
|
|
156
|
-
// Clearing releases the slot so a future run registers afresh.
|
|
157
|
-
refreshExecutionPanel(ctx, null);
|
|
158
|
-
refreshExecutionPanel(ctx, execution);
|
|
159
|
-
assert.equal(factories.length, 3); // #2 was the explicit clear (undefined)
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
describe("execution panel width safety", () => {
|
|
164
|
-
const styledTheme = {
|
|
165
|
-
fg: (_color: string, text: string) => `\u001b[38;5;2m${text}\u001b[39m`,
|
|
166
|
-
strikethrough: (text: string) => text,
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
it("measures CJK code points as double width and zero-width runs as none", () => {
|
|
170
|
-
assert.equal(visibleWidth("新的"), 4);
|
|
171
|
-
assert.equal(visibleWidth("菜单配置"), 8);
|
|
172
|
-
assert.equal(visibleWidth("\uFF46\uFF55\uFF4C\uFF4C"), 8); // fullwidth “full”
|
|
173
|
-
assert.equal(visibleWidth("e\u0301"), 1); // combining acute
|
|
174
|
-
assert.equal(visibleWidth("\u2764\uFE0F"), 1); // variation selector
|
|
175
|
-
assert.equal(visibleWidth("a\u001b[31mbc\u001b[0m"), 3); // ANSI ignored
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("truncates styled CJK lines within the requested width", () => {
|
|
179
|
-
const cjk = "新的 Nemotron AIME 配置和批量入口能验证 seed vector、model pin、arm set 和 500 budget;" +
|
|
180
|
-
"evidence: pr… 后续还有很长的中文描述需要被安全截断。".repeat(6);
|
|
181
|
-
for (const width of [20, 40, 138]) {
|
|
182
|
-
const line = truncateAnsi(styledTheme.fg("accent", cjk), width);
|
|
183
|
-
assert.ok(
|
|
184
|
-
visibleWidth(line) <= width,
|
|
185
|
-
`width ${width}: rendered ${visibleWidth(line)} columns`,
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it("renders every expanded checklist line inside the terminal width", () => {
|
|
191
|
-
const execution: any = {
|
|
192
|
-
planPath: "/tmp/PLAN_v1.md",
|
|
193
|
-
startedAt: "2026-08-29T00:00:00Z",
|
|
194
|
-
usage: { inToks: 0, outToks: 0 },
|
|
195
|
-
items: [
|
|
196
|
-
{
|
|
197
|
-
id: "VC-001",
|
|
198
|
-
// Mirrors the crash-log line that rendered at 155 columns in a 138-column terminal.
|
|
199
|
-
text: "`VC-001` covers `I-001` 和 `I-003`;pass condition: 新的 Nemotron AIME 配置和批量入口能验证 seed vector、model pin、arm set 和 500 budget;evidence: pr…",
|
|
200
|
-
done: false,
|
|
201
|
-
},
|
|
202
|
-
{ id: "VC-002", text: "`VC-002` plain ascii item that is also far too long to fit one line ".repeat(4), done: false },
|
|
203
|
-
],
|
|
204
|
-
panel: createExecutionPanelState(),
|
|
205
|
-
};
|
|
206
|
-
toggleExpanded(execution);
|
|
207
|
-
|
|
208
|
-
let factory: ((tui: unknown, theme: unknown) => any) | undefined;
|
|
209
|
-
const statuses: Array<string | undefined> = [];
|
|
210
|
-
refreshExecutionPanel(
|
|
211
|
-
{
|
|
212
|
-
ui: {
|
|
213
|
-
setWidget: (_key: string, f: any) => void (factory = f),
|
|
214
|
-
setStatus: (_key: string, value: string | undefined) => void statuses.push(value),
|
|
215
|
-
theme: { fg: (_color: string, text: string) => text },
|
|
216
|
-
},
|
|
217
|
-
} as any,
|
|
218
|
-
execution,
|
|
219
|
-
);
|
|
220
|
-
const widget = factory!({}, styledTheme);
|
|
221
|
-
|
|
222
|
-
let sawEllipsis = false;
|
|
223
|
-
for (const width of [20, 40, 80, 138]) {
|
|
224
|
-
for (const line of widget.render(width)) {
|
|
225
|
-
if (line.endsWith("…") || line.includes("…\u001b[0m")) sawEllipsis = true;
|
|
226
|
-
assert.ok(
|
|
227
|
-
visibleWidth(line) <= width,
|
|
228
|
-
`width ${width}: rendered ${visibleWidth(line)} columns: ${JSON.stringify(line.slice(0, 60))}`,
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
assert.ok(sawEllipsis, "expected at least one truncated line with an ellipsis");
|
|
233
|
-
});
|
|
234
|
-
});
|