ultimate-pi 0.10.0 → 0.10.1
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/.pi/extensions/harness-live-widget.ts +48 -28
- package/.pi/extensions/harness-plan-approval.ts +56 -22
- package/.pi/extensions/harness-run-context.ts +23 -14
- package/.pi/extensions/lib/harness-subagents/parent-harness-ui-bridge.ts +5 -1
- package/.pi/extensions/lib/harness-subagents/vendored/index.ts +3 -1
- package/.pi/extensions/lib/harness-subagents/vendored/ui/agent-widget.ts +2 -0
- package/.pi/extensions/lib/plan-approval/dialog.ts +231 -147
- package/.pi/prompts/harness-plan.md +15 -2
- package/CHANGELOG.md +6 -0
- package/package.json +2 -2
|
@@ -329,6 +329,52 @@ export default function harnessLiveWidget(pi: ExtensionAPI) {
|
|
|
329
329
|
let component: HarnessWidgetComponent | null = null;
|
|
330
330
|
let refreshQueued = false;
|
|
331
331
|
let lastRenderHash = "";
|
|
332
|
+
let mountCtx: ExtensionContext | null = null;
|
|
333
|
+
|
|
334
|
+
function mountHarnessWidget(ctx: ExtensionContext): void {
|
|
335
|
+
if (!ctx.hasUI) return;
|
|
336
|
+
const state = stateStore.refresh(ctx);
|
|
337
|
+
const inFlight: InFlightState = { toolCount: 0, lastToolName: null };
|
|
338
|
+
lastRenderHash = computeRenderHash(state, inFlight);
|
|
339
|
+
|
|
340
|
+
ctx.ui.setWidget(
|
|
341
|
+
"harness-live",
|
|
342
|
+
(tui, theme) => {
|
|
343
|
+
widgetMounted = true;
|
|
344
|
+
tuiHandle = tui;
|
|
345
|
+
component = new HarnessWidgetComponent(
|
|
346
|
+
stateStore.snapshot(),
|
|
347
|
+
inFlight,
|
|
348
|
+
theme,
|
|
349
|
+
);
|
|
350
|
+
return {
|
|
351
|
+
render(width: number): string[] {
|
|
352
|
+
component?.setTheme(theme);
|
|
353
|
+
return component?.render(width) ?? [];
|
|
354
|
+
},
|
|
355
|
+
invalidate(): void {
|
|
356
|
+
component?.invalidate();
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
},
|
|
360
|
+
{ placement: "aboveEditor" },
|
|
361
|
+
);
|
|
362
|
+
updateStatusFallback(ctx, state);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function remountHarnessLiveWidget(ctx: ExtensionContext): void {
|
|
366
|
+
if (!ctx.hasUI || !widgetMounted) return;
|
|
367
|
+
ctx.ui.setWidget("harness-live", undefined);
|
|
368
|
+
mountHarnessWidget(ctx);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
pi.events.on("subagents:agents-widget-mounted", () => {
|
|
372
|
+
if (mountCtx) remountHarnessLiveWidget(mountCtx);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
pi.events.on("plan-approval:mounted", () => {
|
|
376
|
+
if (mountCtx) remountHarnessLiveWidget(mountCtx);
|
|
377
|
+
});
|
|
332
378
|
|
|
333
379
|
function updateStatusFallback(
|
|
334
380
|
ctx: ExtensionContext,
|
|
@@ -385,34 +431,8 @@ export default function harnessLiveWidget(pi: ExtensionAPI) {
|
|
|
385
431
|
}
|
|
386
432
|
|
|
387
433
|
pi.on("session_start", (_event, ctx) => {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const inFlight: InFlightState = { toolCount: 0, lastToolName: null };
|
|
391
|
-
lastRenderHash = computeRenderHash(state, inFlight);
|
|
392
|
-
|
|
393
|
-
ctx.ui.setWidget(
|
|
394
|
-
"harness-live",
|
|
395
|
-
(tui, theme) => {
|
|
396
|
-
widgetMounted = true;
|
|
397
|
-
tuiHandle = tui;
|
|
398
|
-
component = new HarnessWidgetComponent(
|
|
399
|
-
stateStore.snapshot(),
|
|
400
|
-
inFlight,
|
|
401
|
-
theme,
|
|
402
|
-
);
|
|
403
|
-
return {
|
|
404
|
-
render(width: number): string[] {
|
|
405
|
-
component?.setTheme(theme);
|
|
406
|
-
return component?.render(width) ?? [];
|
|
407
|
-
},
|
|
408
|
-
invalidate(): void {
|
|
409
|
-
component?.invalidate();
|
|
410
|
-
},
|
|
411
|
-
};
|
|
412
|
-
},
|
|
413
|
-
{ placement: "aboveEditor" },
|
|
414
|
-
);
|
|
415
|
-
updateStatusFallback(ctx, state);
|
|
434
|
+
mountCtx = ctx;
|
|
435
|
+
mountHarnessWidget(ctx);
|
|
416
436
|
});
|
|
417
437
|
|
|
418
438
|
pi.on("context", (_event, ctx) => {
|
|
@@ -7,6 +7,7 @@ import { Text } from "@earendil-works/pi-tui";
|
|
|
7
7
|
import {
|
|
8
8
|
appendPlanApprovalIfNew,
|
|
9
9
|
getLatestRunContext,
|
|
10
|
+
hasPlanUserApproval,
|
|
10
11
|
parsePlanApprovalFromMessage,
|
|
11
12
|
} from "../lib/harness-run-context.js";
|
|
12
13
|
import { runPlanApprovalDialog } from "./lib/plan-approval/dialog.js";
|
|
@@ -32,26 +33,29 @@ import {
|
|
|
32
33
|
} from "./lib/plan-approval/validate.js";
|
|
33
34
|
|
|
34
35
|
export default function harnessPlanApproval(pi: ExtensionAPI) {
|
|
35
|
-
pi.registerMessageRenderer(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
36
|
+
pi.registerMessageRenderer(
|
|
37
|
+
"harness-plan-draft",
|
|
38
|
+
(message, _options, theme) => {
|
|
39
|
+
const data = message.details as
|
|
40
|
+
| {
|
|
41
|
+
plan_packet?: unknown;
|
|
42
|
+
human_summary?: string | null;
|
|
43
|
+
}
|
|
44
|
+
| undefined;
|
|
45
|
+
if (!data?.plan_packet) return undefined;
|
|
46
|
+
const lines = renderHarnessPlanDraft(
|
|
47
|
+
{
|
|
48
|
+
plan_packet: data.plan_packet as Parameters<
|
|
49
|
+
typeof renderHarnessPlanDraft
|
|
50
|
+
>[0]["plan_packet"],
|
|
51
|
+
human_summary: data.human_summary,
|
|
52
|
+
},
|
|
53
|
+
80,
|
|
54
|
+
theme,
|
|
55
|
+
);
|
|
56
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
57
|
+
},
|
|
58
|
+
);
|
|
55
59
|
|
|
56
60
|
pi.registerTool({
|
|
57
61
|
name: "approve_plan",
|
|
@@ -76,6 +80,33 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
|
|
|
76
80
|
};
|
|
77
81
|
}
|
|
78
82
|
|
|
83
|
+
const entries = ctx.sessionManager.getEntries();
|
|
84
|
+
if (
|
|
85
|
+
hasPlanUserApproval(entries, {
|
|
86
|
+
sincePlanCommand: true,
|
|
87
|
+
planId: validated.plan_packet.plan_id ?? null,
|
|
88
|
+
})
|
|
89
|
+
) {
|
|
90
|
+
const planId = String(validated.plan_packet.plan_id ?? "plan");
|
|
91
|
+
return {
|
|
92
|
+
content: [
|
|
93
|
+
{
|
|
94
|
+
type: "text",
|
|
95
|
+
text: `Plan ${planId} already approved in this harness run (planner subagent). Proceed with /harness-run.`,
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
details: {
|
|
99
|
+
plan_packet: validated.plan_packet,
|
|
100
|
+
options: validated.options,
|
|
101
|
+
response: {
|
|
102
|
+
kind: "selection",
|
|
103
|
+
selections: ["Approve"],
|
|
104
|
+
},
|
|
105
|
+
cancelled: false,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
79
110
|
const planId = String(validated.plan_packet.plan_id ?? "plan");
|
|
80
111
|
const summary =
|
|
81
112
|
validated.human_summary?.trim() ||
|
|
@@ -94,7 +125,11 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
|
|
|
94
125
|
|
|
95
126
|
let outcome: PlanApprovalDialogResult;
|
|
96
127
|
if (ctx.hasUI) {
|
|
97
|
-
outcome = await runPlanApprovalDialog(ctx.ui, validated
|
|
128
|
+
outcome = await runPlanApprovalDialog(ctx.ui, validated, {
|
|
129
|
+
onMounted: () => {
|
|
130
|
+
pi.events.emit("plan-approval:mounted", {});
|
|
131
|
+
},
|
|
132
|
+
});
|
|
98
133
|
} else {
|
|
99
134
|
outcome = await runPlanApprovalFallback(ctx.ui, validated);
|
|
100
135
|
}
|
|
@@ -109,7 +144,6 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
|
|
|
109
144
|
details,
|
|
110
145
|
});
|
|
111
146
|
if (approval) {
|
|
112
|
-
const entries = ctx.sessionManager.getEntries();
|
|
113
147
|
const runCtx = getLatestRunContext(entries);
|
|
114
148
|
appendPlanApprovalIfNew(
|
|
115
149
|
(type, data) => pi.appendEntry(type, data),
|
|
@@ -671,20 +671,29 @@ export default function harnessRunContext(pi: ExtensionAPI) {
|
|
|
671
671
|
});
|
|
672
672
|
|
|
673
673
|
pi.on("tool_call", async (event, ctx) => {
|
|
674
|
-
if (
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
674
|
+
if (activeCtx?.plan_packet_path) {
|
|
675
|
+
const entries = getEntries(ctx);
|
|
676
|
+
if (hasPlanUserApproval(entries, { sincePlanCommand: true })) {
|
|
677
|
+
if (event.toolName === "approve_plan") {
|
|
678
|
+
return {
|
|
679
|
+
block: true,
|
|
680
|
+
reason:
|
|
681
|
+
"harness-run-context: plan already approved via planner subagent; do not call approve_plan again in the parent session.",
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
if (event.toolName === "ask_user") {
|
|
685
|
+
const input = event.input as {
|
|
686
|
+
question?: string;
|
|
687
|
+
options?: unknown[];
|
|
688
|
+
};
|
|
689
|
+
if (isPlanApprovalAskUser(input)) {
|
|
690
|
+
return {
|
|
691
|
+
block: true,
|
|
692
|
+
reason:
|
|
693
|
+
"harness-run-context: plan already approved via planner subagent; do not call ask_user for plan approval in the parent session.",
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
}
|
|
688
697
|
}
|
|
689
698
|
}
|
|
690
699
|
if (!activeCtx?.plan_packet_path) return undefined;
|
|
@@ -198,7 +198,11 @@ export function createParentHarnessUiBridgeFactory(
|
|
|
198
198
|
|
|
199
199
|
let outcome: DialogResult;
|
|
200
200
|
if (parentCtx.hasUI) {
|
|
201
|
-
outcome = await runPlanApprovalDialog(parentCtx.ui, validated
|
|
201
|
+
outcome = await runPlanApprovalDialog(parentCtx.ui, validated, {
|
|
202
|
+
onMounted: () => {
|
|
203
|
+
pi.events.emit("plan-approval:mounted", {});
|
|
204
|
+
},
|
|
205
|
+
});
|
|
202
206
|
} else {
|
|
203
207
|
outcome = await runPlanApprovalFallback(parentCtx.ui, validated);
|
|
204
208
|
}
|
|
@@ -759,7 +759,9 @@ export function createHarnessSubagentsExtension(packageRoot: string) {
|
|
|
759
759
|
});
|
|
760
760
|
|
|
761
761
|
// Live widget: show running agents above editor
|
|
762
|
-
const widget = new AgentWidget(manager, agentActivity)
|
|
762
|
+
const widget = new AgentWidget(manager, agentActivity, () => {
|
|
763
|
+
pi.events.emit("subagents:agents-widget-mounted", {});
|
|
764
|
+
});
|
|
763
765
|
|
|
764
766
|
// ---- Join mode configuration ----
|
|
765
767
|
let defaultJoinMode: JoinMode = "smart";
|
|
@@ -264,6 +264,7 @@ export class AgentWidget {
|
|
|
264
264
|
constructor(
|
|
265
265
|
private manager: AgentManager,
|
|
266
266
|
private agentActivity: Map<string, AgentActivity>,
|
|
267
|
+
private onWidgetRegistered?: () => void,
|
|
267
268
|
) {}
|
|
268
269
|
|
|
269
270
|
/** Set the UI context (grabbed from first tool execution). */
|
|
@@ -615,6 +616,7 @@ export class AgentWidget {
|
|
|
615
616
|
{ placement: "aboveEditor" },
|
|
616
617
|
);
|
|
617
618
|
this.widgetRegistered = true;
|
|
619
|
+
this.onWidgetRegistered?.();
|
|
618
620
|
} else {
|
|
619
621
|
// Widget already registered — just request a re-render of existing components.
|
|
620
622
|
this.tui?.requestRender();
|
|
@@ -12,6 +12,51 @@ interface CustomAnswer {
|
|
|
12
12
|
response: { kind: "selection"; selections: string[] };
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/** Lines reserved below overlay: harness-live widget + editor + footer. */
|
|
16
|
+
export const PLAN_APPROVAL_BOTTOM_RESERVE_LINES = 11;
|
|
17
|
+
/** Estimate agents widget height when stacking above harness live. */
|
|
18
|
+
export const PLAN_APPROVAL_AGENTS_TOP_RESERVE_LINES = 12;
|
|
19
|
+
export const PLAN_APPROVAL_MIN_VIEWPORT = 6;
|
|
20
|
+
|
|
21
|
+
export function computePlanViewport(
|
|
22
|
+
availableHeight: number,
|
|
23
|
+
chromeLines: number,
|
|
24
|
+
): number {
|
|
25
|
+
return Math.max(PLAN_APPROVAL_MIN_VIEWPORT, availableHeight - chromeLines);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function computePlanOverlayMaxHeight(termHeight: number): number {
|
|
29
|
+
return Math.max(
|
|
30
|
+
PLAN_APPROVAL_MIN_VIEWPORT + 8,
|
|
31
|
+
termHeight -
|
|
32
|
+
PLAN_APPROVAL_BOTTOM_RESERVE_LINES -
|
|
33
|
+
PLAN_APPROVAL_AGENTS_TOP_RESERVE_LINES,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function countPlanChromeLines(
|
|
38
|
+
validated: ValidatedApprovePlanParams,
|
|
39
|
+
displayOptions: ValidatedApprovePlanParams["options"],
|
|
40
|
+
useOverlay: boolean,
|
|
41
|
+
): number {
|
|
42
|
+
let chrome = useOverlay ? 2 : 0; // borders
|
|
43
|
+
chrome += 1; // title
|
|
44
|
+
if (validated.human_summary) {
|
|
45
|
+
chrome += validated.human_summary.split("\n").length;
|
|
46
|
+
}
|
|
47
|
+
chrome += 1; // blank before plan
|
|
48
|
+
chrome += 1; // plan label
|
|
49
|
+
chrome += 1; // blank before options
|
|
50
|
+
chrome += 1; // options label
|
|
51
|
+
for (const opt of displayOptions) {
|
|
52
|
+
chrome += 1;
|
|
53
|
+
if (opt.description) chrome += 1;
|
|
54
|
+
}
|
|
55
|
+
chrome += 1; // blank before hints
|
|
56
|
+
chrome += 1; // hints
|
|
57
|
+
return chrome;
|
|
58
|
+
}
|
|
59
|
+
|
|
15
60
|
function withTimeout<T>(
|
|
16
61
|
promise: Promise<T | null>,
|
|
17
62
|
ms: number | undefined,
|
|
@@ -25,177 +70,216 @@ function withTimeout<T>(
|
|
|
25
70
|
]);
|
|
26
71
|
}
|
|
27
72
|
|
|
73
|
+
export type RunPlanApprovalDialogOptions = {
|
|
74
|
+
onMounted?: () => void;
|
|
75
|
+
};
|
|
76
|
+
|
|
28
77
|
export async function runPlanApprovalDialog(
|
|
29
78
|
ui: ExtensionUIContext,
|
|
30
79
|
validated: ValidatedApprovePlanParams,
|
|
80
|
+
options?: RunPlanApprovalDialogOptions,
|
|
31
81
|
): Promise<PlanApprovalDialogResult> {
|
|
32
82
|
const planLines = formatPlanPacketLines(validated.plan_packet, 100);
|
|
33
83
|
const displayOptions = validated.options;
|
|
84
|
+
const useOverlay = validated.displayMode !== "inline";
|
|
85
|
+
let overlayTermHeight = 24;
|
|
34
86
|
|
|
35
87
|
const result = await withTimeout(
|
|
36
|
-
ui.custom<CustomAnswer | null>(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function handleInput(data: string) {
|
|
55
|
-
if (focus === "plan") {
|
|
56
|
-
if (matchesKey(data, Key.up) || data === "k") {
|
|
57
|
-
scrollOffset = Math.max(0, scrollOffset - 1);
|
|
58
|
-
refresh();
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
if (matchesKey(data, Key.down) || data === "j") {
|
|
62
|
-
scrollOffset += 1;
|
|
63
|
-
refresh();
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
if (matchesKey(data, Key.pageUp)) {
|
|
67
|
-
scrollOffset = Math.max(0, scrollOffset - 8);
|
|
68
|
-
refresh();
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
if (matchesKey(data, Key.pageDown)) {
|
|
72
|
-
scrollOffset += 8;
|
|
73
|
-
refresh();
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
if (matchesKey(data, Key.tab)) {
|
|
77
|
-
focus = "options";
|
|
78
|
-
refresh();
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
88
|
+
ui.custom<CustomAnswer | null>(
|
|
89
|
+
(tui, theme, _kb, done) => {
|
|
90
|
+
const tuiHeight = (tui as unknown as { height?: number }).height;
|
|
91
|
+
overlayTermHeight =
|
|
92
|
+
typeof tuiHeight === "number" && tuiHeight > 10 ? tuiHeight : 24;
|
|
93
|
+
options?.onMounted?.();
|
|
94
|
+
|
|
95
|
+
let scrollOffset = 0;
|
|
96
|
+
let optionIndex = 0;
|
|
97
|
+
let focus: FocusRegion = "plan";
|
|
98
|
+
let cachedLines: string[] | undefined;
|
|
99
|
+
|
|
100
|
+
function refresh() {
|
|
101
|
+
cachedLines = undefined;
|
|
102
|
+
tui.requestRender();
|
|
81
103
|
}
|
|
82
104
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
105
|
+
function submitSelection() {
|
|
106
|
+
const opt = displayOptions[optionIndex];
|
|
107
|
+
done({
|
|
108
|
+
response: { kind: "selection", selections: [opt.title] },
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function handleInput(data: string) {
|
|
113
|
+
if (focus === "plan") {
|
|
114
|
+
if (matchesKey(data, Key.up) || data === "k") {
|
|
115
|
+
scrollOffset = Math.max(0, scrollOffset - 1);
|
|
116
|
+
refresh();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (matchesKey(data, Key.down) || data === "j") {
|
|
120
|
+
scrollOffset += 1;
|
|
121
|
+
refresh();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (matchesKey(data, Key.pageUp)) {
|
|
125
|
+
scrollOffset = Math.max(0, scrollOffset - 8);
|
|
126
|
+
refresh();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (matchesKey(data, Key.pageDown)) {
|
|
130
|
+
scrollOffset += 8;
|
|
131
|
+
refresh();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (matchesKey(data, Key.tab)) {
|
|
135
|
+
focus = "options";
|
|
136
|
+
refresh();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
93
139
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
140
|
+
|
|
141
|
+
if (focus === "options") {
|
|
142
|
+
if (matchesKey(data, Key.up)) {
|
|
143
|
+
optionIndex = Math.max(0, optionIndex - 1);
|
|
144
|
+
refresh();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (matchesKey(data, Key.down)) {
|
|
148
|
+
optionIndex = Math.min(
|
|
149
|
+
displayOptions.length - 1,
|
|
150
|
+
optionIndex + 1,
|
|
151
|
+
);
|
|
152
|
+
refresh();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (matchesKey(data, Key.tab)) {
|
|
156
|
+
focus = "plan";
|
|
157
|
+
refresh();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (matchesKey(data, Key.enter)) {
|
|
161
|
+
submitSelection();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
98
164
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
165
|
+
|
|
166
|
+
if (matchesKey(data, Key.escape)) {
|
|
167
|
+
done(null);
|
|
102
168
|
}
|
|
103
169
|
}
|
|
104
170
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function render(width: number): string[] {
|
|
111
|
-
if (cachedLines) return cachedLines;
|
|
112
|
-
|
|
113
|
-
const lines: string[] = [];
|
|
114
|
-
const add = (s: string) => lines.push(truncateToWidth(s, width));
|
|
115
|
-
const useOverlay = validated.displayMode !== "inline";
|
|
116
|
-
const dims = (tui as { height?: number }).height;
|
|
117
|
-
const termHeight = typeof dims === "number" && dims > 10 ? dims : 24;
|
|
118
|
-
const footerLines = displayOptions.length * 2 + 8;
|
|
119
|
-
const planViewport = Math.max(
|
|
120
|
-
6,
|
|
121
|
-
Math.floor(termHeight * 0.55) - footerLines,
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
if (useOverlay) {
|
|
125
|
-
add(theme.fg("accent", "─".repeat(width)));
|
|
126
|
-
}
|
|
171
|
+
function render(width: number): string[] {
|
|
172
|
+
if (cachedLines) return cachedLines;
|
|
127
173
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const maxScroll = Math.max(0, planLines.length - planViewport);
|
|
137
|
-
scrollOffset = Math.min(scrollOffset, maxScroll);
|
|
138
|
-
const visible = planLines.slice(
|
|
139
|
-
scrollOffset,
|
|
140
|
-
scrollOffset + planViewport,
|
|
141
|
-
);
|
|
142
|
-
const planLabel =
|
|
143
|
-
focus === "plan"
|
|
144
|
-
? theme.fg("accent", " [plan — ↑↓/Pg scroll, Tab → options]")
|
|
145
|
-
: theme.fg("dim", " [plan]");
|
|
146
|
-
add(planLabel);
|
|
147
|
-
for (const line of visible) {
|
|
148
|
-
add(theme.fg("text", ` ${line}`));
|
|
149
|
-
}
|
|
150
|
-
if (planLines.length > planViewport) {
|
|
151
|
-
add(
|
|
152
|
-
theme.fg(
|
|
153
|
-
"dim",
|
|
154
|
-
` … ${scrollOffset + 1}-${scrollOffset + visible.length} of ${planLines.length}`,
|
|
155
|
-
),
|
|
174
|
+
const lines: string[] = [];
|
|
175
|
+
const add = (s: string) => lines.push(truncateToWidth(s, width));
|
|
176
|
+
const dims = (tui as { height?: number }).height;
|
|
177
|
+
const termHeight = typeof dims === "number" && dims > 10 ? dims : 24;
|
|
178
|
+
const chromeLines = countPlanChromeLines(
|
|
179
|
+
validated,
|
|
180
|
+
displayOptions,
|
|
181
|
+
useOverlay,
|
|
156
182
|
);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
? theme.fg("accent", " Options (↑↓, Enter, Tab → plan):")
|
|
163
|
-
: theme.fg("dim", " Options (Tab to focus):");
|
|
164
|
-
add(optLabel);
|
|
165
|
-
for (let i = 0; i < displayOptions.length; i++) {
|
|
166
|
-
const opt = displayOptions[i];
|
|
167
|
-
const focused = focus === "options" && i === optionIndex;
|
|
168
|
-
const prefix = focused ? theme.fg("accent", "> ") : " ";
|
|
169
|
-
const num = `${i + 1}. `;
|
|
170
|
-
if (focused) {
|
|
171
|
-
add(prefix + theme.fg("accent", `${num}${opt.title}`));
|
|
183
|
+
|
|
184
|
+
let availableHeight: number;
|
|
185
|
+
if (useOverlay) {
|
|
186
|
+
const overlayMax = computePlanOverlayMaxHeight(termHeight);
|
|
187
|
+
availableHeight = overlayMax;
|
|
172
188
|
} else {
|
|
173
|
-
|
|
189
|
+
availableHeight = termHeight - PLAN_APPROVAL_BOTTOM_RESERVE_LINES;
|
|
174
190
|
}
|
|
175
|
-
|
|
176
|
-
|
|
191
|
+
const planViewport = computePlanViewport(
|
|
192
|
+
availableHeight,
|
|
193
|
+
chromeLines,
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
if (useOverlay) {
|
|
197
|
+
add(theme.fg("accent", "─".repeat(width)));
|
|
177
198
|
}
|
|
178
|
-
}
|
|
179
199
|
|
|
180
|
-
|
|
181
|
-
|
|
200
|
+
add(theme.fg("accent", " Plan approval"));
|
|
201
|
+
if (validated.human_summary) {
|
|
202
|
+
for (const line of validated.human_summary.split("\n")) {
|
|
203
|
+
add(theme.fg("muted", ` ${line}`));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
lines.push("");
|
|
182
207
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
208
|
+
const maxScroll = Math.max(0, planLines.length - planViewport);
|
|
209
|
+
scrollOffset = Math.min(scrollOffset, maxScroll);
|
|
210
|
+
const visible = planLines.slice(
|
|
211
|
+
scrollOffset,
|
|
212
|
+
scrollOffset + planViewport,
|
|
213
|
+
);
|
|
214
|
+
const planLabel =
|
|
215
|
+
focus === "plan"
|
|
216
|
+
? theme.fg("accent", " [plan — ↑↓/Pg scroll, Tab → options]")
|
|
217
|
+
: theme.fg("dim", " [plan]");
|
|
218
|
+
add(planLabel);
|
|
219
|
+
for (const line of visible) {
|
|
220
|
+
add(theme.fg("text", ` ${line}`));
|
|
221
|
+
}
|
|
222
|
+
if (planLines.length > planViewport) {
|
|
223
|
+
add(
|
|
224
|
+
theme.fg(
|
|
225
|
+
"dim",
|
|
226
|
+
` … ${scrollOffset + 1}-${scrollOffset + visible.length} of ${planLines.length}`,
|
|
227
|
+
),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
lines.push("");
|
|
186
231
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
232
|
+
const optLabel =
|
|
233
|
+
focus === "options"
|
|
234
|
+
? theme.fg("accent", " Options (↑↓, Enter, Tab → plan):")
|
|
235
|
+
: theme.fg("dim", " Options (Tab to focus):");
|
|
236
|
+
add(optLabel);
|
|
237
|
+
for (let i = 0; i < displayOptions.length; i++) {
|
|
238
|
+
const opt = displayOptions[i];
|
|
239
|
+
const focused = focus === "options" && i === optionIndex;
|
|
240
|
+
const prefix = focused ? theme.fg("accent", "> ") : " ";
|
|
241
|
+
const num = `${i + 1}. `;
|
|
242
|
+
if (focused) {
|
|
243
|
+
add(prefix + theme.fg("accent", `${num}${opt.title}`));
|
|
244
|
+
} else {
|
|
245
|
+
add(`${prefix}${theme.fg("text", `${num}${opt.title}`)}`);
|
|
246
|
+
}
|
|
247
|
+
if (opt.description) {
|
|
248
|
+
add(` ${theme.fg("muted", opt.description)}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
190
251
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
252
|
+
lines.push("");
|
|
253
|
+
add(theme.fg("dim", " Tab: plan ↔ options • Esc: cancel"));
|
|
254
|
+
|
|
255
|
+
if (useOverlay) {
|
|
256
|
+
add(theme.fg("accent", "─".repeat(width)));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
cachedLines = lines;
|
|
260
|
+
return lines;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
render,
|
|
265
|
+
invalidate: () => {
|
|
266
|
+
cachedLines = undefined;
|
|
267
|
+
},
|
|
268
|
+
handleInput,
|
|
269
|
+
};
|
|
270
|
+
},
|
|
271
|
+
useOverlay
|
|
272
|
+
? {
|
|
273
|
+
overlay: true,
|
|
274
|
+
overlayOptions: () => ({
|
|
275
|
+
anchor: "bottom-center",
|
|
276
|
+
width: "100%",
|
|
277
|
+
margin: { bottom: PLAN_APPROVAL_BOTTOM_RESERVE_LINES },
|
|
278
|
+
maxHeight: computePlanOverlayMaxHeight(overlayTermHeight),
|
|
279
|
+
}),
|
|
280
|
+
}
|
|
281
|
+
: undefined,
|
|
282
|
+
),
|
|
199
283
|
undefined,
|
|
200
284
|
);
|
|
201
285
|
|
|
@@ -40,11 +40,24 @@ Otherwise use `HarnessSpawnContext` from `[HarnessRunContext]` for greenfield `m
|
|
|
40
40
|
Agent({ subagent_type: "harness/planner", prompt: "<task + HarnessSpawnContext JSON + output schema>" })
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
3. `get_subagent_result` — parse final JSON (`status`, `plan_packet`, `human_summary`, `clarification`) via fenced `json` block.
|
|
44
|
-
4. If `status === "ready"` and
|
|
43
|
+
3. `get_subagent_result` — parse final JSON (`status`, `plan_packet`, `human_summary`, `clarification`) via fenced `json` block. Treat `plan_packet` in that JSON as **read-only summary context** — not input for another approval tool call.
|
|
44
|
+
4. If `status === "ready"` and `[HarnessRunContext]` shows `plan_ready: true` (planner called `create_plan`), confirm `plan_packet_path` exists — do **not** write the file yourself.
|
|
45
45
|
5. If `needs_clarification`, tell the user the planner is waiting — do **not** re-spawn; user should answer in the subagent or re-run `/harness-plan`.
|
|
46
46
|
6. Do **not** call `ask_user`, `approve_plan`, or `create_plan` in this parent session.
|
|
47
47
|
|
|
48
|
+
## After subagent returns (no second approval)
|
|
49
|
+
|
|
50
|
+
User approval happens **once**, inside the planner subagent: `approve_plan` uses the parent TUI bridge. You are the orchestrator, **not** an approver.
|
|
51
|
+
|
|
52
|
+
After `get_subagent_result`:
|
|
53
|
+
|
|
54
|
+
- If `[HarnessRunContext]` shows `plan_ready: true`, or the transcript already has `harness-plan-approval` / bridged `approve_plan` with **Approve** → planning is complete. **Stop.** Summarize the plan and set `next_command: /harness-run`.
|
|
55
|
+
- Do **not** call `approve_plan` to “confirm” using `plan_packet` from subagent JSON.
|
|
56
|
+
- Do **not** call `ask_user` with Approve / Request changes / Cancel for the same plan.
|
|
57
|
+
- Do **not** re-spawn the planner to “get approval again”.
|
|
58
|
+
|
|
59
|
+
If `status === "ready"` but `plan_ready` is false → planner approved but `create_plan` may have failed; tell the user to run `/harness-plan-commit` — **not** a second `approve_plan`.
|
|
60
|
+
|
|
48
61
|
## Parent rules
|
|
49
62
|
|
|
50
63
|
- Do not mutate project source files in the plan phase.
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@ All notable changes to this project are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [v0.10.1] — 2026-05-17
|
|
8
|
+
|
|
9
|
+
### 🐛 Fixes
|
|
10
|
+
|
|
11
|
+
- **Harness plan TUI:** agents widget stacks above harness-live; full-height bottom `approve_plan` overlay above the harness band; block duplicate parent `approve_plan` / plan `ask_user` after planner subagent approval.
|
|
12
|
+
|
|
7
13
|
## [v0.10.0] — 2026-05-17
|
|
8
14
|
|
|
9
15
|
### ✨ Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultimate-pi",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Ultimate AI coding harness for pi.dev — extensible skills, Obsidian wiki knowledge layer, compressed context, deterministic output",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"format": "biome format --write",
|
|
83
83
|
"format:check": "biome format",
|
|
84
84
|
"prepare": "lefthook install",
|
|
85
|
-
"test": "node --test test/harness-verify.test.mjs test/harness-ask-user.test.mjs test/harness-subagents-loader.test.mjs test/harness-subagents-import-path.test.mjs test/sentrux-rules-sync.test.mjs test/harness-budget-guard.test.mjs && npx -y tsx --test test/harness-vcc-settings.test.ts test/harness-plan-phase-policy.test.mjs test/harness-subagent-policy.test.mjs test/harness-turn-routing.test.mjs test/plan-approval-format.test.mjs test/plan-approval-sync.test.mjs test/plan-create-plan.test.mjs",
|
|
85
|
+
"test": "node --test test/harness-verify.test.mjs test/harness-ask-user.test.mjs test/harness-subagents-loader.test.mjs test/harness-subagents-import-path.test.mjs test/sentrux-rules-sync.test.mjs test/harness-budget-guard.test.mjs && npx -y tsx --test test/harness-vcc-settings.test.ts test/harness-plan-phase-policy.test.mjs test/harness-subagent-policy.test.mjs test/harness-turn-routing.test.mjs test/plan-approval-format.test.mjs test/plan-approval-dialog.test.mjs test/plan-approval-sync.test.mjs test/plan-create-plan.test.mjs",
|
|
86
86
|
"test:vcc": "npx -y tsx --test vendor/pi-vcc/tests/*.test.ts",
|
|
87
87
|
"harness:sentrux-bootstrap": "node .pi/scripts/harness-sentrux-bootstrap.mjs",
|
|
88
88
|
"harness:sentrux-sync": "node .pi/scripts/sentrux-rules-sync.mjs --force",
|