pi-goal-x 0.26.0 → 0.26.2
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 +2 -0
- package/extensions/goal-drafting.ts +24 -7
- package/extensions/goal-questionnaire.ts +197 -50
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -129,6 +129,8 @@ Start a guided regular goal:
|
|
|
129
129
|
|
|
130
130
|
The agent can ask questions and propose a complete objective and task plan. Confirm the proposal to create the goal and begin work.
|
|
131
131
|
|
|
132
|
+
The questionnaire dialog never truncates the question; when it is taller than the terminal it stays within the height and scrolls — `PgUp`/`PgDn` page and `Ctrl+↑/↓` line-scroll without moving the selection, `↑/↓` selection auto-follows into view, and a `▲`/`… +N more` edge indicator shows what is clipped.
|
|
133
|
+
|
|
132
134
|
Start a guided Sisyphus goal:
|
|
133
135
|
|
|
134
136
|
```text
|
|
@@ -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
|
}
|
|
@@ -465,12 +468,26 @@ export function registerDraftingTools(core: GoalCore): void {
|
|
|
465
468
|
const objective = String(args?.objective ?? "");
|
|
466
469
|
let text = theme.fg("toolTitle", "propose_goal_draft ") + theme.fg("muted", objective);
|
|
467
470
|
const flatTasks = Array.isArray(args?.tasks) ? (args.tasks as FlatTaskInput[]) : [];
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
+
}
|
|
474
491
|
if (taskBlock) {
|
|
475
492
|
text += "\n\n" + theme.fg("toolTitle", theme.bold(taskBlock.header)) + "\n" + theme.fg("toolOutput", taskBlock.lines.join("\n"));
|
|
476
493
|
}
|
|
@@ -54,10 +54,27 @@ export interface ProposalPresentationSegments {
|
|
|
54
54
|
tailStart: number;
|
|
55
55
|
}
|
|
56
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
|
+
|
|
57
62
|
/**
|
|
58
63
|
* Locate the proposal tasks segment and the options tail in a rendered dialog.
|
|
59
64
|
* Returns null when the dialog is not a proposal-style confirmation (no
|
|
60
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).
|
|
61
78
|
*/
|
|
62
79
|
export function findProposalPresentationSegments(lines: string[], tailStart: number): ProposalPresentationSegments | null {
|
|
63
80
|
if (tailStart < 0) return null;
|
|
@@ -65,10 +82,25 @@ export function findProposalPresentationSegments(lines: string[], tailStart: num
|
|
|
65
82
|
if (tasksStart < 0 || tasksStart >= tailStart) return null;
|
|
66
83
|
let tasksEnd = tasksStart;
|
|
67
84
|
for (let i = tasksStart + 1; i < tailStart; i++) {
|
|
68
|
-
|
|
69
|
-
|
|
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;
|
|
70
93
|
}
|
|
71
|
-
|
|
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 };
|
|
72
104
|
}
|
|
73
105
|
|
|
74
106
|
/**
|
|
@@ -76,63 +108,118 @@ export function findProposalPresentationSegments(lines: string[], tailStart: num
|
|
|
76
108
|
* question. The protected head (top border + tabs + question line) is always
|
|
77
109
|
* kept; the remaining budget is spent on the tail — options/footer/bottom
|
|
78
110
|
* border — so long context blocks (proposal confirmations) are sliced from
|
|
79
|
-
* their head exactly as the pre-fix tail-slice did (383ae52 surface).
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* recommended/first option stays visible; the footer hint and bottom border
|
|
83
|
-
* are always the last rendered lines. Never returns more than maxDialogLines.
|
|
111
|
+
* their head exactly as the pre-fix tail-slice did (383ae52 surface). The
|
|
112
|
+
* footer hint and bottom border are always the last rendered lines. Never
|
|
113
|
+
* returns more than maxDialogLines.
|
|
84
114
|
*
|
|
85
115
|
* Proposal confirmations (proposal segments given) keep the head, the tasks
|
|
86
116
|
* section (header + every task line), and the options/footer/bottom border in
|
|
87
117
|
* frame; only the objective-box middle is sacrificed in-frame — the full
|
|
88
118
|
* objective is always present in the scrollable transcript presentation
|
|
89
119
|
* (propose_goal_draft renderCall), so nothing of the goal is ever omitted.
|
|
120
|
+
*
|
|
121
|
+
* §options-scroll: when a `scroll` state is given (select-mode question tabs
|
|
122
|
+
* and the submit summary), the dialog becomes a `less`-style viewport over the
|
|
123
|
+
* FULL content — nothing is truncated, nothing is sliced away — and the
|
|
124
|
+
* options are always reachable by scrolling (PageUp/PageDown, Ctrl+↑/↓, and
|
|
125
|
+
* ↑/↓ selection auto-follow). See fitDialogViewport.
|
|
90
126
|
*/
|
|
127
|
+
export interface DialogScrollState {
|
|
128
|
+
/** Current viewport offset over the full content lines. */
|
|
129
|
+
scrollTop: number;
|
|
130
|
+
/** When true, the fitter nudges scrollTop so the selection range is visible. */
|
|
131
|
+
needsFollow: boolean;
|
|
132
|
+
/** Line ranges [start, end] of each selectable option in the full content. */
|
|
133
|
+
optionRanges: Array<[number, number]>;
|
|
134
|
+
/** Index of the current selection in optionRanges. */
|
|
135
|
+
followIndex: number;
|
|
136
|
+
}
|
|
137
|
+
|
|
91
138
|
export function fitDialogLines(
|
|
92
139
|
lines: string[],
|
|
93
140
|
maxDialogLines: number,
|
|
94
141
|
protectedHead: number,
|
|
95
|
-
optionsImmediatelyAfterHead = false,
|
|
96
142
|
proposal: ProposalPresentationSegments | null = null,
|
|
143
|
+
scroll: DialogScrollState | null = null,
|
|
144
|
+
dimStyle: (s: string) => string = (s) => s,
|
|
97
145
|
): string[] {
|
|
98
146
|
if (maxDialogLines <= 0 || lines.length <= maxDialogLines) return lines;
|
|
99
147
|
const keepHead = Math.min(protectedHead, maxDialogLines);
|
|
148
|
+
if (proposal) {
|
|
149
|
+
return fitProposalPresentation(lines, maxDialogLines, keepHead, proposal);
|
|
150
|
+
}
|
|
151
|
+
if (scroll) {
|
|
152
|
+
return fitDialogViewport(lines, maxDialogLines, scroll, dimStyle);
|
|
153
|
+
}
|
|
154
|
+
// Context-heavy / input / submit dialogs without a scroll state: keep the
|
|
155
|
+
// head and the tail (options/footer/bottom border) exactly as the pre-fix
|
|
156
|
+
// tail-slice did.
|
|
100
157
|
const budget = maxDialogLines - keepHead;
|
|
101
158
|
if (budget <= 0) return lines.slice(0, keepHead);
|
|
102
159
|
const rest = lines.slice(protectedHead);
|
|
103
160
|
if (rest.length <= budget) return [...lines.slice(0, keepHead), ...rest];
|
|
104
|
-
|
|
105
|
-
|
|
161
|
+
return [...lines.slice(0, keepHead), ...rest.slice(rest.length - budget)];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* §options-scroll viewport fit: never truncate or drop content — window the
|
|
166
|
+
* full dialog lines with a scrollTop offset so every line (question, context,
|
|
167
|
+
* options, footer, border) stays reachable. The last viewport row is reserved
|
|
168
|
+
* as the bottom edge: the bottom border when the end is reached, otherwise a
|
|
169
|
+
* themed `… +N more · PgUp/PgDn scroll` indicator (mirrors the dashboard's
|
|
170
|
+
* `… +N more task` rows). A themed `▲ N more` indicator replaces the first
|
|
171
|
+
* content row when scrolled down. Selection auto-follow (needsFollow) nudges
|
|
172
|
+
* the window so the selected option's line range is inside it. Output length
|
|
173
|
+
* is exactly maxDialogLines when clipped, never more (churn-guard invariant).
|
|
174
|
+
*/
|
|
175
|
+
function fitDialogViewport(
|
|
176
|
+
lines: string[],
|
|
177
|
+
maxDialogLines: number,
|
|
178
|
+
scroll: DialogScrollState,
|
|
179
|
+
dimStyle: (s: string) => string,
|
|
180
|
+
): string[] {
|
|
181
|
+
const contentWindow = Math.max(0, maxDialogLines - 1);
|
|
182
|
+
const contentEnd = Math.max(0, lines.length - 1);
|
|
183
|
+
// Selection auto-follow: nudge scrollTop so the selected option's range is
|
|
184
|
+
// inside the content window (minimal scroll; clamp below).
|
|
185
|
+
if (scroll.needsFollow && scroll.optionRanges[scroll.followIndex]) {
|
|
186
|
+
const [os, oe] = scroll.optionRanges[scroll.followIndex];
|
|
187
|
+
if (os < scroll.scrollTop || oe >= scroll.scrollTop + contentWindow) {
|
|
188
|
+
scroll.scrollTop = oe >= scroll.scrollTop + contentWindow
|
|
189
|
+
? Math.min(os, Math.max(0, oe - contentWindow + 1))
|
|
190
|
+
: os;
|
|
191
|
+
}
|
|
192
|
+
scroll.needsFollow = false;
|
|
106
193
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const lead = firstContent > 0 ? rest.slice(0, firstContent) : [];
|
|
115
|
-
const contentStart = firstContent > 0 ? firstContent : 0;
|
|
116
|
-
const tailBudget = Math.min(2, budget); // footer hint + bottom border
|
|
117
|
-
const tail = rest.slice(-tailBudget);
|
|
118
|
-
const room = budget - tailBudget;
|
|
119
|
-
const keepContent = Math.max(0, Math.min(rest.length - contentStart, room));
|
|
120
|
-
const keepLead = Math.max(0, Math.min(lead.length, room - keepContent));
|
|
121
|
-
const content = rest.slice(contentStart, contentStart + keepContent);
|
|
122
|
-
return [...headLines, ...lead.slice(0, keepLead), ...content, ...tail];
|
|
194
|
+
scroll.scrollTop = Math.max(0, Math.min(scroll.scrollTop, Math.max(0, contentEnd - contentWindow)));
|
|
195
|
+
const s = scroll.scrollTop;
|
|
196
|
+
const hiddenAbove = s;
|
|
197
|
+
const hiddenBelow = contentEnd - (s + contentWindow);
|
|
198
|
+
const viewport = lines.slice(s, s + contentWindow);
|
|
199
|
+
if (hiddenAbove > 0 && viewport.length > 0) {
|
|
200
|
+
viewport[0] = dimStyle(`▲ ${hiddenAbove} more`);
|
|
123
201
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
202
|
+
if (hiddenBelow > 0) {
|
|
203
|
+
// The reserved bottom edge row advertises the scroll affordance — it
|
|
204
|
+
// replaces the border while content is clipped below (mirrors the
|
|
205
|
+
// dashboard's `… +N more task` rows) and gives way to the border at the
|
|
206
|
+
// end. The footer hint sits directly above the border, so it is only
|
|
207
|
+
// visible when the end is reached (never while clipped — geometry).
|
|
208
|
+
viewport.push(dimStyle(`… +${hiddenBelow} more · PgUp/PgDn scroll`));
|
|
209
|
+
} else {
|
|
210
|
+
viewport.push(lines[lines.length - 1]);
|
|
211
|
+
}
|
|
212
|
+
return viewport;
|
|
127
213
|
}
|
|
128
214
|
|
|
129
215
|
/**
|
|
130
|
-
* Proposal confirmation fit: keep the protected head, the tasks section,
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
216
|
+
* Proposal confirmation fit: keep the protected head, the tasks section, the
|
|
217
|
+
* interactive auditor toggle line (when present), and the options/footer/
|
|
218
|
+
* bottom border; the objective-box middle is sacrificed in-frame (it stays
|
|
219
|
+
* fully readable in the scrollback presentation). Interior blank spacing lines
|
|
220
|
+
* are dropped first when room is short; task lines are only dropped after
|
|
221
|
+
* that, from the end, when the bound is exhausted (those lines remain in the
|
|
222
|
+
* scrollback presentation). Never exceeds maxDialogLines.
|
|
136
223
|
*/
|
|
137
224
|
function fitProposalPresentation(
|
|
138
225
|
lines: string[],
|
|
@@ -150,15 +237,18 @@ function fitProposalPresentation(
|
|
|
150
237
|
// the options and the footer hint) before touching any content line.
|
|
151
238
|
const stripped = candidate.filter((l, i) => i < keepHead || l.trim() !== "");
|
|
152
239
|
if (stripped.length <= maxDialogLines) return stripped;
|
|
153
|
-
// Bound still exhausted: keep the head, then
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
240
|
+
// Bound still exhausted: keep the head, then the auditor toggle line (the
|
|
241
|
+
// actionable status control — its feedback must stay visible), then spend
|
|
242
|
+
// the room on the tail (options/footer/bottom border — the decision
|
|
243
|
+
// surface — kept from its end so the border and footer never drop), then on
|
|
244
|
+
// the tasks from the start. The dropped task lines remain fully readable in
|
|
157
245
|
// the scrollback presentation. Never exceeds maxDialogLines.
|
|
158
246
|
const tailNoBlanks = tail.filter((l) => l.trim() !== "");
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
const
|
|
247
|
+
const hasAuditor = tail.length > 0 && tail[0].includes("press 'a' to toggle");
|
|
248
|
+
const restTail = hasAuditor ? tailNoBlanks.slice(1) : tailNoBlanks;
|
|
249
|
+
const keepRest = Math.min(restTail.length, Math.max(0, maxDialogLines - keepHead - (hasAuditor ? 1 : 0)));
|
|
250
|
+
const tailKept = [...(hasAuditor ? [tail[0]] : []), ...restTail.slice(restTail.length - keepRest)];
|
|
251
|
+
const keepTasks = Math.min(tasks.length, Math.max(0, maxDialogLines - keepHead - tailKept.length));
|
|
162
252
|
return [...head, ...tasks.slice(0, keepTasks), ...tailKept];
|
|
163
253
|
}
|
|
164
254
|
|
|
@@ -255,6 +345,10 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
255
345
|
let inputQuestionId: string | null = null;
|
|
256
346
|
let cachedLines: string[] | undefined;
|
|
257
347
|
let optionsStartIndex = -1;
|
|
348
|
+
// §options-scroll viewport state (select-mode + submit tabs only).
|
|
349
|
+
let scrollTop = 0;
|
|
350
|
+
let needsFollow = false;
|
|
351
|
+
let optionRanges: Array<[number, number]> = [];
|
|
258
352
|
let auditorEnabled = auditorToggleInit?.defaultEnabled ?? true;
|
|
259
353
|
const answers = new Map<string, GoalQuestionnaireAnswer>();
|
|
260
354
|
const drafts = new Map<string, string>();
|
|
@@ -304,6 +398,9 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
304
398
|
function enterQuestion(q: GoalQuestionnaireQuestion) {
|
|
305
399
|
const existing = answers.get(q.id);
|
|
306
400
|
const draft = drafts.get(q.id);
|
|
401
|
+
// §options-scroll: start each question tab from the top.
|
|
402
|
+
scrollTop = 0;
|
|
403
|
+
needsFollow = false;
|
|
307
404
|
if (q.options.length === 0) {
|
|
308
405
|
inputMode = true;
|
|
309
406
|
inputQuestionId = q.id;
|
|
@@ -318,7 +415,14 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
318
415
|
}
|
|
319
416
|
}
|
|
320
417
|
|
|
321
|
-
function
|
|
418
|
+
function enterSubmitTab() {
|
|
419
|
+
// §options-scroll: the submit summary also opens from the top.
|
|
420
|
+
optionIndex = 0;
|
|
421
|
+
scrollTop = 0;
|
|
422
|
+
needsFollow = false;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function advanceAfterAnswer() {
|
|
322
426
|
if (!isMulti) {
|
|
323
427
|
submit(false);
|
|
324
428
|
return;
|
|
@@ -327,7 +431,7 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
327
431
|
else currentTab = questions.length;
|
|
328
432
|
const nextQ = currentQuestion();
|
|
329
433
|
if (nextQ) enterQuestion(nextQ);
|
|
330
|
-
else
|
|
434
|
+
else enterSubmitTab();
|
|
331
435
|
refresh();
|
|
332
436
|
}
|
|
333
437
|
|
|
@@ -380,7 +484,7 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
380
484
|
currentTab = matchesKey(data, Key.tab) ? (currentTab + 1) % totalTabs : (currentTab - 1 + totalTabs) % totalTabs;
|
|
381
485
|
const nextQ = currentQuestion();
|
|
382
486
|
if (nextQ) enterQuestion(nextQ);
|
|
383
|
-
else
|
|
487
|
+
else enterSubmitTab();
|
|
384
488
|
refresh();
|
|
385
489
|
return;
|
|
386
490
|
}
|
|
@@ -397,7 +501,7 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
397
501
|
currentTab = (currentTab + 1) % totalTabs;
|
|
398
502
|
const nextQ = currentQuestion();
|
|
399
503
|
if (nextQ) enterQuestion(nextQ);
|
|
400
|
-
else
|
|
504
|
+
else enterSubmitTab();
|
|
401
505
|
refresh();
|
|
402
506
|
return;
|
|
403
507
|
}
|
|
@@ -405,12 +509,41 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
405
509
|
currentTab = (currentTab - 1 + totalTabs) % totalTabs;
|
|
406
510
|
const nextQ = currentQuestion();
|
|
407
511
|
if (nextQ) enterQuestion(nextQ);
|
|
408
|
-
else
|
|
512
|
+
else enterSubmitTab();
|
|
409
513
|
refresh();
|
|
410
514
|
return;
|
|
411
515
|
}
|
|
412
516
|
}
|
|
413
517
|
|
|
518
|
+
// §options-scroll viewport keys (select-mode question tabs AND the
|
|
519
|
+
// submit summary): page and line scroll without moving the selection.
|
|
520
|
+
// Handled before the submit-tab early return so the summary scrolls
|
|
521
|
+
// too. ↑/↓ still select and auto-follow into view on the next render.
|
|
522
|
+
if (matchesKey(data, Key.pageUp)) {
|
|
523
|
+
scrollTop -= Math.max(1, (maxDialogLines ?? 10) - 1);
|
|
524
|
+
needsFollow = false;
|
|
525
|
+
refresh();
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (matchesKey(data, Key.pageDown)) {
|
|
529
|
+
scrollTop += Math.max(1, (maxDialogLines ?? 10) - 1);
|
|
530
|
+
needsFollow = false;
|
|
531
|
+
refresh();
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (matchesKey(data, Key.ctrl("up"))) {
|
|
535
|
+
scrollTop -= 1;
|
|
536
|
+
needsFollow = false;
|
|
537
|
+
refresh();
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (matchesKey(data, Key.ctrl("down"))) {
|
|
541
|
+
scrollTop += 1;
|
|
542
|
+
needsFollow = false;
|
|
543
|
+
refresh();
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
414
547
|
if (currentTab === questions.length) {
|
|
415
548
|
if (matchesKey(data, Key.enter) && allAnswered()) submit(false);
|
|
416
549
|
else if (matchesKey(data, Key.escape)) submit(true);
|
|
@@ -419,11 +552,13 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
419
552
|
|
|
420
553
|
if (matchesKey(data, Key.up)) {
|
|
421
554
|
optionIndex = Math.max(0, optionIndex - 1);
|
|
555
|
+
needsFollow = true; // §options-scroll: keep the selection visible
|
|
422
556
|
refresh();
|
|
423
557
|
return;
|
|
424
558
|
}
|
|
425
559
|
if (matchesKey(data, Key.down)) {
|
|
426
560
|
optionIndex = Math.min(opts.length - 1, optionIndex + 1);
|
|
561
|
+
needsFollow = true;
|
|
427
562
|
refresh();
|
|
428
563
|
return;
|
|
429
564
|
}
|
|
@@ -597,11 +732,13 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
597
732
|
function renderOptions() {
|
|
598
733
|
optionsStartIndex = lines.length;
|
|
599
734
|
for (let i = 0; i < opts.length; i++) {
|
|
735
|
+
const start = lines.length;
|
|
600
736
|
const opt = opts[i];
|
|
601
737
|
const selected = i === optionIndex;
|
|
602
738
|
const prefix = selected ? theme.fg("accent", "> ") : " ";
|
|
603
739
|
const recTag = !opt.isCustom && q?.recommended === i ? theme.fg("success", " ★") : "";
|
|
604
740
|
addWrapped(prefix + theme.fg(selected ? "accent" : "text", `${i + 1}. ${opt.label}`) + recTag);
|
|
741
|
+
optionRanges[i] = [start, lines.length - 1];
|
|
605
742
|
}
|
|
606
743
|
}
|
|
607
744
|
|
|
@@ -669,11 +806,21 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
669
806
|
// objective-box middle is sacrificed there because the full objective
|
|
670
807
|
// is always in the scrollable transcript presentation (renderCall).
|
|
671
808
|
if (maxDialogLines !== undefined && lines.length > maxDialogLines) {
|
|
672
|
-
const optionsImmediatelyAfterHead = !inputMode && currentTab !== questions.length && !!q && !q.context && opts.length > 0;
|
|
673
809
|
const proposalSegments = !inputMode && currentTab !== questions.length && !!q && q.context
|
|
674
810
|
? findProposalPresentationSegments(lines, optionsStartIndex)
|
|
675
811
|
: null;
|
|
676
|
-
|
|
812
|
+
// §options-scroll: select-mode question tabs and the submit summary
|
|
813
|
+
// are a viewport over the full content (never truncated); input
|
|
814
|
+
// mode keeps the legacy tail-keep so the editor stays the priority.
|
|
815
|
+
let scrollState: DialogScrollState | null = null;
|
|
816
|
+
if (!inputMode) {
|
|
817
|
+
scrollState = { scrollTop, needsFollow, optionRanges, followIndex: optionIndex };
|
|
818
|
+
}
|
|
819
|
+
lines = fitDialogLines(lines, maxDialogLines, protectedCount, proposalSegments, scrollState, (s) => theme.fg("dim", s));
|
|
820
|
+
if (scrollState) {
|
|
821
|
+
scrollTop = scrollState.scrollTop;
|
|
822
|
+
needsFollow = scrollState.needsFollow;
|
|
823
|
+
}
|
|
677
824
|
}
|
|
678
825
|
cachedLines = lines;
|
|
679
826
|
return lines;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-x",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.2",
|
|
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",
|