pi-goal-x 0.25.4 → 0.26.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/docs/architecture.md
CHANGED
|
@@ -19,7 +19,7 @@ handlers from their dedicated modules:
|
|
|
19
19
|
| `goal-task-tools.ts` | `set_goal_tasks` / `update_goal_task` executors plus flat parent-linked conversion, id-stable merge, `countTasks` |
|
|
20
20
|
| `goal-task-confirmation.ts` | Task-only result boundary (`{decision}`, no auditor toggle) with neutral Confirm task list / Keep current tasks labels |
|
|
21
21
|
| `goal-commands.ts` | The curated fourteen-command palette and its handlers |
|
|
22
|
-
| `goal-events.ts` | The
|
|
22
|
+
| `goal-events.ts` | The 14 lifecycle event handlers (`context`, `turn_start`, `tool_call`, `tool_execution_end`, `turn_end`, `message_end`, `session_start`, `session_before_compact`, `session_compact`, `session_tree`, `before_agent_start`, `agent_end`, `agent_settled`, `session_shutdown`) |
|
|
23
23
|
| `goal-widget.ts` | Terminal input keybindings (Esc pause / abort-audit, Ctrl+Shift+T overlay) and the hidden debug helpers |
|
|
24
24
|
| `goal-format.ts` | Pure formatting/message-introspection helpers and renderers |
|
|
25
25
|
| `goal-service.ts` | `GoalService` — the sole mutation boundary: ordered reconcile → id/focus-revision validation → clone-mutate → write/archive → ledger → memory commit → returned effects |
|
package/extensions/goal-draft.ts
CHANGED
|
@@ -150,6 +150,7 @@ export function goalDraftingPrompt(topic: string, focus: GoalDraftingFocus): str
|
|
|
150
150
|
"- For simple single-step goals, no task list is required. The `tasks` parameter can be omitted.",
|
|
151
151
|
"- After goal creation, `propose_task_list` is still available for user-requested task additions or structural changes.",
|
|
152
152
|
"- propose_goal_draft opens the user's Confirm / Continue Chatting dialog. Confirm creates and focuses the goal; Continue Chatting means keep refining through normal proposal cycles.",
|
|
153
|
+
"- Before proposing, write the COMPLETE goal into your own message — every section (objective, success criteria, boundaries, constraints, verification contract) and the full task list — so the user can scroll up and re-read the whole goal while the confirmation dialog is open. Nothing may be omitted from the presented goal.",
|
|
153
154
|
"- create_goal is not a shortcut. Direct create_goal calls are rejected so the user keeps explicit say in goal creation.",
|
|
154
155
|
];
|
|
155
156
|
|
|
@@ -211,7 +211,10 @@ export function proposalText(draft: ActiveGoalDraft, objective: string, autoCont
|
|
|
211
211
|
} else if (taskList && taskList.tasks.length > 0) {
|
|
212
212
|
tasksText = "\n\nTasks proposed for confirmation:\n" + renderConfirmationTasks(taskList.tasks, 0).join("\n");
|
|
213
213
|
} else {
|
|
214
|
-
|
|
214
|
+
// §single-task-set: the derived preview must derive from the SAME
|
|
215
|
+
// objective text the apply path persists (extracted — the Verification
|
|
216
|
+
// contract line removed), so shown == persisted.
|
|
217
|
+
const derived = deriveTasksFromObjective(extractVerificationContract(objective).objective);
|
|
215
218
|
if (derived && derived.length > 0) {
|
|
216
219
|
tasksText = "\n\nTasks derived from the objective (confirm or ask the agent to adjust):\n" + renderConfirmationTasks(derived, 0).join("\n");
|
|
217
220
|
}
|
|
@@ -454,7 +457,44 @@ export function registerDraftingTools(core: GoalCore): void {
|
|
|
454
457
|
clearGoalDrafting(core, ctx);
|
|
455
458
|
return { content: [{ type: "text", text: `${summary}\n\nGoal tweak confirmed and applied.` }], details: goalDetails(result.goal), terminate: true };
|
|
456
459
|
},
|
|
457
|
-
renderCall(args, theme) {
|
|
460
|
+
renderCall(args, theme) {
|
|
461
|
+
// §proposal-presentation: the tool-call display lands in the terminal
|
|
462
|
+
// buffer the moment the tool call starts — BEFORE the confirmation
|
|
463
|
+
// dialog opens — so it is the scrollable "complete presentation" of
|
|
464
|
+
// the goal. It must show the full objective contract AND every task
|
|
465
|
+
// line; the dialog frame alone cannot (its churn-guard bound keeps
|
|
466
|
+
// it within the terminal height). The user can scroll up and read
|
|
467
|
+
// everything while the dialog is open ("the user can just scroll").
|
|
468
|
+
const objective = String(args?.objective ?? "");
|
|
469
|
+
let text = theme.fg("toolTitle", "propose_goal_draft ") + theme.fg("muted", objective);
|
|
470
|
+
const flatTasks = Array.isArray(args?.tasks) ? (args.tasks as FlatTaskInput[]) : [];
|
|
471
|
+
// §single-task-set: a proposal shows exactly ONE task set — the set
|
|
472
|
+
// that will actually be persisted. Explicit tasks are that set; a
|
|
473
|
+
// tweak without explicit tasks retains the current list (mirror
|
|
474
|
+
// proposalText — never a derived-from-objective phantom); a new
|
|
475
|
+
// draft without explicit tasks shows the F2-derived preview derived
|
|
476
|
+
// from the SAME objective text the apply path persists (extracted).
|
|
477
|
+
let taskBlock: { header: string; lines: string[] } | null = null;
|
|
478
|
+
if (flatTasks.length > 0) {
|
|
479
|
+
taskBlock = { header: "Tasks proposed for confirmation:", lines: flatTasks.map((t) => `[ ] ${t.id}: ${t.title}`) };
|
|
480
|
+
} else if (activeDraft(core)?.mode === "tweak") {
|
|
481
|
+
const retained = core.state.goal?.taskList?.tasks;
|
|
482
|
+
if (retained && retained.length > 0) {
|
|
483
|
+
taskBlock = { header: "Current task list (retained unchanged):", lines: renderConfirmationTasks(retained, 0) };
|
|
484
|
+
}
|
|
485
|
+
} else {
|
|
486
|
+
const derived = deriveTasksFromObjective(extractVerificationContract(objective).objective) ?? [];
|
|
487
|
+
if (derived.length > 0) {
|
|
488
|
+
taskBlock = { header: "Tasks derived from the objective (confirm or ask the agent to adjust):", lines: renderConfirmationTasks(derived, 0) };
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (taskBlock) {
|
|
492
|
+
text += "\n\n" + theme.fg("toolTitle", theme.bold(taskBlock.header)) + "\n" + theme.fg("toolOutput", taskBlock.lines.join("\n"));
|
|
493
|
+
}
|
|
494
|
+
if (args?.auto_continue === false) text += "\n\n" + theme.fg("muted", "Automatic continuation: disabled");
|
|
495
|
+
if (args?.block_completion === true) text += "\n\n" + theme.fg("muted", "Block completion on pending tasks: enabled");
|
|
496
|
+
return new Text(text, 0, 0);
|
|
497
|
+
},
|
|
458
498
|
renderResult(result, _opts, theme) { return renderGoalResult(result, _opts, theme); },
|
|
459
499
|
}));
|
|
460
500
|
}
|
|
@@ -34,10 +34,12 @@ import type { GoalMutationOutcome } from "./goal-service.ts";
|
|
|
34
34
|
* The goal extension's lifecycle event handlers (context, turn_start,
|
|
35
35
|
* tool_call, tool_execution_end, turn_end, message_end, session_start,
|
|
36
36
|
* session_before_compact, session_compact, session_tree, before_agent_start,
|
|
37
|
-
* agent_end, session_shutdown). All state flows through the
|
|
37
|
+
* agent_end, agent_settled, session_shutdown). All state flows through the
|
|
38
|
+
* GoalCore.
|
|
38
39
|
*/
|
|
39
40
|
export function registerGoalEvents(core: GoalCore): void {
|
|
40
41
|
const { pi } = core;
|
|
42
|
+
let continuationAfterSettleFor: string | null = null;
|
|
41
43
|
|
|
42
44
|
pi.on("context", async (event): Promise<{ messages: typeof event.messages } | undefined> => {
|
|
43
45
|
let changed = false;
|
|
@@ -418,6 +420,7 @@ export function registerGoalEvents(core: GoalCore): void {
|
|
|
418
420
|
pi.on("agent_end", async (event, ctx) => {
|
|
419
421
|
const endedGoalId = core.runningGoalId;
|
|
420
422
|
core.runningGoalId = null;
|
|
423
|
+
continuationAfterSettleFor = null;
|
|
421
424
|
|
|
422
425
|
// Account for any tokens from aborted in-flight assistant messages so
|
|
423
426
|
// they are not silently lost (but charge them to the original goal).
|
|
@@ -446,10 +449,23 @@ export function registerGoalEvents(core: GoalCore): void {
|
|
|
446
449
|
}
|
|
447
450
|
core.persist(ctx);
|
|
448
451
|
core.updateUI(ctx);
|
|
449
|
-
|
|
452
|
+
// agent_end runs before pi finishes retries, compaction, terminating-tool
|
|
453
|
+
// settlement, and queued messages. Starting the continuation timer here
|
|
454
|
+
// can poll a stale busy context for minutes on pi 0.84. agent_settled is
|
|
455
|
+
// available in both supported SDK lines (0.83 and 0.84) and is the first
|
|
456
|
+
// point where pi guarantees no automatic work remains.
|
|
457
|
+
continuationAfterSettleFor = core.state.goal.id;
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
pi.on("agent_settled", async (_event, ctx) => {
|
|
461
|
+
const goalId = continuationAfterSettleFor;
|
|
462
|
+
continuationAfterSettleFor = null;
|
|
463
|
+
if (!goalId || !core.isActionableContinuationGoal(goalId)) return;
|
|
464
|
+
core.queueContinuation(ctx, true);
|
|
450
465
|
});
|
|
451
466
|
|
|
452
467
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
468
|
+
continuationAfterSettleFor = null;
|
|
453
469
|
core.accountProgress(ctx);
|
|
454
470
|
core.clearContinuationTimer();
|
|
455
471
|
core.terminalInputUnsubscribe?.();
|
|
@@ -29,6 +29,175 @@ export interface GoalQuestionnaireResult {
|
|
|
29
29
|
|
|
30
30
|
export type ProposalDecision = "confirm" | "continue" | "cancel";
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Bound a custom dialog using the regular renderer's frame cache when
|
|
34
|
+
* available. The pi 0.84 fullscreen renderer does not expose that cache, so
|
|
35
|
+
* reserve four rows for host chrome and use the remaining terminal height.
|
|
36
|
+
*/
|
|
37
|
+
export function computeDialogLineLimit(args: { terminalRows?: number; baseFrameLines?: number }): number | undefined {
|
|
38
|
+
const rows = args.terminalRows;
|
|
39
|
+
if (!rows || rows <= 0) return undefined;
|
|
40
|
+
if (args.baseFrameLines && args.baseFrameLines > 0) {
|
|
41
|
+
return Math.min(rows, Math.max(10, rows - args.baseFrameLines + 1));
|
|
42
|
+
}
|
|
43
|
+
return Math.min(rows, Math.max(4, rows - 4));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Proposal-confirmation segment descriptor: absolute line indices of the
|
|
48
|
+
* tasks section (header + every task line) and the start of the options tail
|
|
49
|
+
* inside a rendered questionnaire dialog.
|
|
50
|
+
*/
|
|
51
|
+
export interface ProposalPresentationSegments {
|
|
52
|
+
tasksStart: number;
|
|
53
|
+
tasksEnd: number;
|
|
54
|
+
tailStart: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Strip ANSI SGR sequences so structural scans see the plain text. */
|
|
58
|
+
function stripAnsiCodes(value: string): string {
|
|
59
|
+
return value.replace(/\x1b\[[0-9;]*m/g, "");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Locate the proposal tasks segment and the options tail in a rendered dialog.
|
|
64
|
+
* Returns null when the dialog is not a proposal-style confirmation (no
|
|
65
|
+
* "Tasks proposed for confirmation:" header / "┌─ TASKS ─" box).
|
|
66
|
+
*
|
|
67
|
+
* The scan is ANSI-aware: every rendered line carries `theme.fg(...)` escape
|
|
68
|
+
* sequences in the real TUI (the mock theme does not), so plain-text regexes
|
|
69
|
+
* must be run against the stripped line or the tasks section collapses to its
|
|
70
|
+
* header and every styled task line is sliced out of the bounded frame. The
|
|
71
|
+
* box-drawn bottom border (`└───┘`) after the last task line is kept so the
|
|
72
|
+
* `┌─ TASKS ─┐` box renders complete.
|
|
73
|
+
*
|
|
74
|
+
* The interactive auditor toggle line ("press 'a' to toggle") renders between
|
|
75
|
+
* the context and the options; `tailStart` is pulled back to it so the bounded
|
|
76
|
+
* fit always keeps the auditor status visible (its status text is the only
|
|
77
|
+
* visible toggle feedback in the dialog).
|
|
78
|
+
*/
|
|
79
|
+
export function findProposalPresentationSegments(lines: string[], tailStart: number): ProposalPresentationSegments | null {
|
|
80
|
+
if (tailStart < 0) return null;
|
|
81
|
+
const tasksStart = lines.findIndex((l) => l.includes("Tasks proposed for confirmation:") || l.includes("┌─ TASKS ─"));
|
|
82
|
+
if (tasksStart < 0 || tasksStart >= tailStart) return null;
|
|
83
|
+
let tasksEnd = tasksStart;
|
|
84
|
+
for (let i = tasksStart + 1; i < tailStart; i++) {
|
|
85
|
+
const plain = stripAnsiCodes(lines[i]);
|
|
86
|
+
if (/^\s*\[[ x~]\]/.test(plain)) {
|
|
87
|
+
tasksEnd = i;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
// Box-drawn TASKS section bottom border after the last task line.
|
|
91
|
+
if (tasksEnd > tasksStart && /^[└┐┤┘─]/.test(plain.trim())) tasksEnd = i;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
// Protect the auditor toggle line: it renders between context and options,
|
|
95
|
+
// so pull the tail start back to it when present.
|
|
96
|
+
let effectiveTail = tailStart;
|
|
97
|
+
for (let i = tailStart - 1; i > tasksEnd; i--) {
|
|
98
|
+
if (lines[i].includes("press 'a' to toggle")) {
|
|
99
|
+
effectiveTail = i;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { tasksStart, tasksEnd, tailStart: effectiveTail };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Fit a rendered dialog to the terminal-height bound without ever hiding the
|
|
108
|
+
* question. The protected head (top border + tabs + question line) is always
|
|
109
|
+
* kept; the remaining budget is spent on the tail — options/footer/bottom
|
|
110
|
+
* border — so long context blocks (proposal confirmations) are sliced from
|
|
111
|
+
* their head exactly as the pre-fix tail-slice did (383ae52 surface). When the
|
|
112
|
+
* options block starts immediately after the head (plain agent questions with
|
|
113
|
+
* no context block), the TOP options are kept instead of the tail so the
|
|
114
|
+
* recommended/first option stays visible; the footer hint and bottom border
|
|
115
|
+
* are always the last rendered lines. Never returns more than maxDialogLines.
|
|
116
|
+
*
|
|
117
|
+
* Proposal confirmations (proposal segments given) keep the head, the tasks
|
|
118
|
+
* section (header + every task line), and the options/footer/bottom border in
|
|
119
|
+
* frame; only the objective-box middle is sacrificed in-frame — the full
|
|
120
|
+
* objective is always present in the scrollable transcript presentation
|
|
121
|
+
* (propose_goal_draft renderCall), so nothing of the goal is ever omitted.
|
|
122
|
+
*/
|
|
123
|
+
export function fitDialogLines(
|
|
124
|
+
lines: string[],
|
|
125
|
+
maxDialogLines: number,
|
|
126
|
+
protectedHead: number,
|
|
127
|
+
optionsImmediatelyAfterHead = false,
|
|
128
|
+
proposal: ProposalPresentationSegments | null = null,
|
|
129
|
+
): string[] {
|
|
130
|
+
if (maxDialogLines <= 0 || lines.length <= maxDialogLines) return lines;
|
|
131
|
+
const keepHead = Math.min(protectedHead, maxDialogLines);
|
|
132
|
+
const budget = maxDialogLines - keepHead;
|
|
133
|
+
if (budget <= 0) return lines.slice(0, keepHead);
|
|
134
|
+
const rest = lines.slice(protectedHead);
|
|
135
|
+
if (rest.length <= budget) return [...lines.slice(0, keepHead), ...rest];
|
|
136
|
+
if (proposal) {
|
|
137
|
+
return fitProposalPresentation(lines, maxDialogLines, keepHead, proposal);
|
|
138
|
+
}
|
|
139
|
+
if (optionsImmediatelyAfterHead) {
|
|
140
|
+
// Plain select-mode question: keep the footer hint + bottom border (as
|
|
141
|
+
// many as fit), then spend the remaining room on the TOP options so the
|
|
142
|
+
// recommended/first option stays visible; leading blank separators are
|
|
143
|
+
// dropped first when room is short. Never exceeds maxDialogLines.
|
|
144
|
+
const headLines = lines.slice(0, keepHead);
|
|
145
|
+
const firstContent = rest.findIndex((l) => l.trim().length > 0);
|
|
146
|
+
const lead = firstContent > 0 ? rest.slice(0, firstContent) : [];
|
|
147
|
+
const contentStart = firstContent > 0 ? firstContent : 0;
|
|
148
|
+
const tailBudget = Math.min(2, budget); // footer hint + bottom border
|
|
149
|
+
const tail = rest.slice(-tailBudget);
|
|
150
|
+
const room = budget - tailBudget;
|
|
151
|
+
const keepContent = Math.max(0, Math.min(rest.length - contentStart, room));
|
|
152
|
+
const keepLead = Math.max(0, Math.min(lead.length, room - keepContent));
|
|
153
|
+
const content = rest.slice(contentStart, contentStart + keepContent);
|
|
154
|
+
return [...headLines, ...lead.slice(0, keepLead), ...content, ...tail];
|
|
155
|
+
}
|
|
156
|
+
// Context-heavy / input / submit dialogs: keep the head and the tail
|
|
157
|
+
// (options, footer, bottom border) exactly as the pre-fix tail-slice did.
|
|
158
|
+
return [...lines.slice(0, keepHead), ...rest.slice(rest.length - budget)];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Proposal confirmation fit: keep the protected head, the tasks section, the
|
|
163
|
+
* interactive auditor toggle line (when present), and the options/footer/
|
|
164
|
+
* bottom border; the objective-box middle is sacrificed in-frame (it stays
|
|
165
|
+
* fully readable in the scrollback presentation). Interior blank spacing lines
|
|
166
|
+
* are dropped first when room is short; task lines are only dropped after
|
|
167
|
+
* that, from the end, when the bound is exhausted (those lines remain in the
|
|
168
|
+
* scrollback presentation). Never exceeds maxDialogLines.
|
|
169
|
+
*/
|
|
170
|
+
function fitProposalPresentation(
|
|
171
|
+
lines: string[],
|
|
172
|
+
maxDialogLines: number,
|
|
173
|
+
keepHead: number,
|
|
174
|
+
proposal: ProposalPresentationSegments,
|
|
175
|
+
): string[] {
|
|
176
|
+
const { tasksStart, tasksEnd, tailStart } = proposal;
|
|
177
|
+
const head = lines.slice(0, keepHead);
|
|
178
|
+
const tasks = lines.slice(Math.max(tasksStart, keepHead), Math.min(tasksEnd, tailStart) + 1);
|
|
179
|
+
const tail = lines.slice(Math.max(tailStart, keepHead));
|
|
180
|
+
const candidate = [...head, ...tasks, ...tail];
|
|
181
|
+
if (candidate.length <= maxDialogLines) return candidate;
|
|
182
|
+
// Tight: drop blank spacing lines below the head (e.g. the blank between
|
|
183
|
+
// the options and the footer hint) before touching any content line.
|
|
184
|
+
const stripped = candidate.filter((l, i) => i < keepHead || l.trim() !== "");
|
|
185
|
+
if (stripped.length <= maxDialogLines) return stripped;
|
|
186
|
+
// Bound still exhausted: keep the head, then the auditor toggle line (the
|
|
187
|
+
// actionable status control — its feedback must stay visible), then spend
|
|
188
|
+
// the room on the tail (options/footer/bottom border — the decision
|
|
189
|
+
// surface — kept from its end so the border and footer never drop), then on
|
|
190
|
+
// the tasks from the start. The dropped task lines remain fully readable in
|
|
191
|
+
// the scrollback presentation. Never exceeds maxDialogLines.
|
|
192
|
+
const tailNoBlanks = tail.filter((l) => l.trim() !== "");
|
|
193
|
+
const hasAuditor = tail.length > 0 && tail[0].includes("press 'a' to toggle");
|
|
194
|
+
const restTail = hasAuditor ? tailNoBlanks.slice(1) : tailNoBlanks;
|
|
195
|
+
const keepRest = Math.min(restTail.length, Math.max(0, maxDialogLines - keepHead - (hasAuditor ? 1 : 0)));
|
|
196
|
+
const tailKept = [...(hasAuditor ? [tail[0]] : []), ...restTail.slice(restTail.length - keepRest)];
|
|
197
|
+
const keepTasks = Math.min(tasks.length, Math.max(0, maxDialogLines - keepHead - tailKept.length));
|
|
198
|
+
return [...head, ...tasks.slice(0, keepTasks), ...tailKept];
|
|
199
|
+
}
|
|
200
|
+
|
|
32
201
|
export function normalizeQuestionnaireQuestions(rawQuestions: GoalQuestionnaireQuestion[]): GoalQuestionnaireQuestion[] {
|
|
33
202
|
const seenIds = new Set<string>();
|
|
34
203
|
return rawQuestions.map((q, i) => {
|
|
@@ -108,18 +277,20 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
108
277
|
// the frame never exceeds the terminal height — without this, closing a dialog
|
|
109
278
|
// taller than the terminal triggers pi-tui's generic shrink full-render
|
|
110
279
|
// (\x1b[2J\x1b[H\x1b[3J), erasing terminal scrollback and yanking the viewport
|
|
111
|
-
// so the window takes ~10s to scroll back to the bottom. The
|
|
112
|
-
// the actionable options/footer in view
|
|
113
|
-
//
|
|
280
|
+
// so the window takes ~10s to scroll back to the bottom. The slice keeps
|
|
281
|
+
// the question and the actionable options/footer in view (see
|
|
282
|
+
// fitDialogLines); content that fits renders exactly as the
|
|
283
|
+
// pre-regression (383ae52) UI. Only applies with real TUI dimensions.
|
|
114
284
|
const tuiInfo = tui as unknown as { terminal?: { rows?: number }; previousLines?: string[] };
|
|
115
285
|
const terminalRows = tuiInfo.terminal?.rows;
|
|
116
286
|
const baseFrame = tuiInfo.previousLines?.length;
|
|
117
|
-
const maxDialogLines =
|
|
287
|
+
const maxDialogLines = computeDialogLineLimit({ terminalRows, baseFrameLines: baseFrame });
|
|
118
288
|
let currentTab = 0;
|
|
119
289
|
let optionIndex = 0;
|
|
120
290
|
let inputMode = false;
|
|
121
291
|
let inputQuestionId: string | null = null;
|
|
122
292
|
let cachedLines: string[] | undefined;
|
|
293
|
+
let optionsStartIndex = -1;
|
|
123
294
|
let auditorEnabled = auditorToggleInit?.defaultEnabled ?? true;
|
|
124
295
|
const answers = new Map<string, GoalQuestionnaireAnswer>();
|
|
125
296
|
const drafts = new Map<string, string>();
|
|
@@ -439,6 +610,9 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
439
610
|
};
|
|
440
611
|
|
|
441
612
|
add(theme.fg("accent", "─".repeat(safeWidth)));
|
|
613
|
+
// Lines up to the question line (top border, tabs, question incl.
|
|
614
|
+
// wraps) are the protected head — the slice below must never hide them.
|
|
615
|
+
let protectedCount = lines.length;
|
|
442
616
|
if (isMulti) {
|
|
443
617
|
const tabs: string[] = ["← "];
|
|
444
618
|
for (let i = 0; i < questions.length; i++) {
|
|
@@ -453,9 +627,11 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
453
627
|
tabs.push(" →");
|
|
454
628
|
add(` ${tabs.join("")}`);
|
|
455
629
|
lines.push("");
|
|
630
|
+
protectedCount = lines.length;
|
|
456
631
|
}
|
|
457
632
|
|
|
458
633
|
function renderOptions() {
|
|
634
|
+
optionsStartIndex = lines.length;
|
|
459
635
|
for (let i = 0; i < opts.length; i++) {
|
|
460
636
|
const opt = opts[i];
|
|
461
637
|
const selected = i === optionIndex;
|
|
@@ -467,6 +643,7 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
467
643
|
|
|
468
644
|
if (inputMode && q) {
|
|
469
645
|
addWrapped(theme.fg("text", ` ${q.question}`));
|
|
646
|
+
protectedCount = lines.length;
|
|
470
647
|
if (q.context) renderContextLines(q.context);
|
|
471
648
|
lines.push("");
|
|
472
649
|
if (q.options.length > 0) {
|
|
@@ -479,6 +656,7 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
479
656
|
add(theme.fg("dim", " Enter to submit • Esc to cancel"));
|
|
480
657
|
} else if (currentTab === questions.length) {
|
|
481
658
|
add(theme.fg("accent", theme.bold(" Ready to submit")));
|
|
659
|
+
protectedCount = lines.length;
|
|
482
660
|
lines.push("");
|
|
483
661
|
for (const question of questions) {
|
|
484
662
|
const answer = answers.get(question.id);
|
|
@@ -488,6 +666,7 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
488
666
|
add(allAnswered() ? theme.fg("success", " Press Enter to submit") : theme.fg("warning", ` Unanswered: ${questions.filter((qq) => !answers.has(qq.id)).map((qq) => qq.id).join(", ")}`));
|
|
489
667
|
} else if (q) {
|
|
490
668
|
addWrapped(theme.fg("text", ` ${q.question}`));
|
|
669
|
+
protectedCount = lines.length;
|
|
491
670
|
if (q.context) renderContextLines(q.context);
|
|
492
671
|
// Auditor toggle line between context and options
|
|
493
672
|
if (auditorToggleInit) {
|
|
@@ -516,9 +695,21 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
516
695
|
lines[i] = truncateToWidth(lines[i], safeWidth);
|
|
517
696
|
}
|
|
518
697
|
}
|
|
519
|
-
// Churn guard:
|
|
698
|
+
// Churn guard: bound to the terminal height (see factory top) so the
|
|
699
|
+
// opened frame never exceeds the screen. Never slice the question:
|
|
700
|
+
// keep the protected head, then spend the rest of the budget on the
|
|
701
|
+
// tail (options/footer/bottom border) — or, for plain select-mode
|
|
702
|
+
// questions where the options start right after the head, keep the
|
|
703
|
+
// top options so the recommended one stays visible. Proposal
|
|
704
|
+
// confirmations additionally keep the tasks section in frame; the
|
|
705
|
+
// objective-box middle is sacrificed there because the full objective
|
|
706
|
+
// is always in the scrollable transcript presentation (renderCall).
|
|
520
707
|
if (maxDialogLines !== undefined && lines.length > maxDialogLines) {
|
|
521
|
-
|
|
708
|
+
const optionsImmediatelyAfterHead = !inputMode && currentTab !== questions.length && !!q && !q.context && opts.length > 0;
|
|
709
|
+
const proposalSegments = !inputMode && currentTab !== questions.length && !!q && q.context
|
|
710
|
+
? findProposalPresentationSegments(lines, optionsStartIndex)
|
|
711
|
+
: null;
|
|
712
|
+
lines = fitDialogLines(lines, maxDialogLines, protectedCount, optionsImmediatelyAfterHead, proposalSegments);
|
|
522
713
|
}
|
|
523
714
|
cachedLines = lines;
|
|
524
715
|
return lines;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-x",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.1",
|
|
4
4
|
"description": "Goal mode extension for pi: durable long-running objectives, five model tools, structured tasks, independent completion audit, Sisyphus mode, auto-continue, and a status overlay.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "pi-goal-x contributors",
|
|
@@ -62,9 +62,9 @@
|
|
|
62
62
|
"typebox": "*"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
|
-
"@earendil-works/pi-ai": "^0.
|
|
66
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
67
|
-
"@earendil-works/pi-tui": "^0.
|
|
65
|
+
"@earendil-works/pi-ai": "^0.84.1",
|
|
66
|
+
"@earendil-works/pi-coding-agent": "^0.84.1",
|
|
67
|
+
"@earendil-works/pi-tui": "^0.84.1",
|
|
68
68
|
"typebox": "^1.0.58",
|
|
69
69
|
"typescript": "^5.9.3"
|
|
70
70
|
},
|