pi-goal-x 0.26.1 → 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-questionnaire.ts +145 -34
- 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
|
|
@@ -108,54 +108,108 @@ export function findProposalPresentationSegments(lines: string[], tailStart: num
|
|
|
108
108
|
* question. The protected head (top border + tabs + question line) is always
|
|
109
109
|
* kept; the remaining budget is spent on the tail — options/footer/bottom
|
|
110
110
|
* border — so long context blocks (proposal confirmations) are sliced from
|
|
111
|
-
* their head exactly as the pre-fix tail-slice did (383ae52 surface).
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* recommended/first option stays visible; the footer hint and bottom border
|
|
115
|
-
* 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.
|
|
116
114
|
*
|
|
117
115
|
* Proposal confirmations (proposal segments given) keep the head, the tasks
|
|
118
116
|
* section (header + every task line), and the options/footer/bottom border in
|
|
119
117
|
* frame; only the objective-box middle is sacrificed in-frame — the full
|
|
120
118
|
* objective is always present in the scrollable transcript presentation
|
|
121
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.
|
|
122
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
|
+
|
|
123
138
|
export function fitDialogLines(
|
|
124
139
|
lines: string[],
|
|
125
140
|
maxDialogLines: number,
|
|
126
141
|
protectedHead: number,
|
|
127
|
-
optionsImmediatelyAfterHead = false,
|
|
128
142
|
proposal: ProposalPresentationSegments | null = null,
|
|
143
|
+
scroll: DialogScrollState | null = null,
|
|
144
|
+
dimStyle: (s: string) => string = (s) => s,
|
|
129
145
|
): string[] {
|
|
130
146
|
if (maxDialogLines <= 0 || lines.length <= maxDialogLines) return lines;
|
|
131
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.
|
|
132
157
|
const budget = maxDialogLines - keepHead;
|
|
133
158
|
if (budget <= 0) return lines.slice(0, keepHead);
|
|
134
159
|
const rest = lines.slice(protectedHead);
|
|
135
160
|
if (rest.length <= budget) return [...lines.slice(0, keepHead), ...rest];
|
|
136
|
-
|
|
137
|
-
|
|
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;
|
|
138
193
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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];
|
|
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`);
|
|
155
201
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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;
|
|
159
213
|
}
|
|
160
214
|
|
|
161
215
|
/**
|
|
@@ -291,6 +345,10 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
291
345
|
let inputQuestionId: string | null = null;
|
|
292
346
|
let cachedLines: string[] | undefined;
|
|
293
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]> = [];
|
|
294
352
|
let auditorEnabled = auditorToggleInit?.defaultEnabled ?? true;
|
|
295
353
|
const answers = new Map<string, GoalQuestionnaireAnswer>();
|
|
296
354
|
const drafts = new Map<string, string>();
|
|
@@ -340,6 +398,9 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
340
398
|
function enterQuestion(q: GoalQuestionnaireQuestion) {
|
|
341
399
|
const existing = answers.get(q.id);
|
|
342
400
|
const draft = drafts.get(q.id);
|
|
401
|
+
// §options-scroll: start each question tab from the top.
|
|
402
|
+
scrollTop = 0;
|
|
403
|
+
needsFollow = false;
|
|
343
404
|
if (q.options.length === 0) {
|
|
344
405
|
inputMode = true;
|
|
345
406
|
inputQuestionId = q.id;
|
|
@@ -354,7 +415,14 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
354
415
|
}
|
|
355
416
|
}
|
|
356
417
|
|
|
357
|
-
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() {
|
|
358
426
|
if (!isMulti) {
|
|
359
427
|
submit(false);
|
|
360
428
|
return;
|
|
@@ -363,7 +431,7 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
363
431
|
else currentTab = questions.length;
|
|
364
432
|
const nextQ = currentQuestion();
|
|
365
433
|
if (nextQ) enterQuestion(nextQ);
|
|
366
|
-
else
|
|
434
|
+
else enterSubmitTab();
|
|
367
435
|
refresh();
|
|
368
436
|
}
|
|
369
437
|
|
|
@@ -416,7 +484,7 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
416
484
|
currentTab = matchesKey(data, Key.tab) ? (currentTab + 1) % totalTabs : (currentTab - 1 + totalTabs) % totalTabs;
|
|
417
485
|
const nextQ = currentQuestion();
|
|
418
486
|
if (nextQ) enterQuestion(nextQ);
|
|
419
|
-
else
|
|
487
|
+
else enterSubmitTab();
|
|
420
488
|
refresh();
|
|
421
489
|
return;
|
|
422
490
|
}
|
|
@@ -433,7 +501,7 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
433
501
|
currentTab = (currentTab + 1) % totalTabs;
|
|
434
502
|
const nextQ = currentQuestion();
|
|
435
503
|
if (nextQ) enterQuestion(nextQ);
|
|
436
|
-
else
|
|
504
|
+
else enterSubmitTab();
|
|
437
505
|
refresh();
|
|
438
506
|
return;
|
|
439
507
|
}
|
|
@@ -441,12 +509,41 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
441
509
|
currentTab = (currentTab - 1 + totalTabs) % totalTabs;
|
|
442
510
|
const nextQ = currentQuestion();
|
|
443
511
|
if (nextQ) enterQuestion(nextQ);
|
|
444
|
-
else
|
|
512
|
+
else enterSubmitTab();
|
|
445
513
|
refresh();
|
|
446
514
|
return;
|
|
447
515
|
}
|
|
448
516
|
}
|
|
449
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
|
+
|
|
450
547
|
if (currentTab === questions.length) {
|
|
451
548
|
if (matchesKey(data, Key.enter) && allAnswered()) submit(false);
|
|
452
549
|
else if (matchesKey(data, Key.escape)) submit(true);
|
|
@@ -455,11 +552,13 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
455
552
|
|
|
456
553
|
if (matchesKey(data, Key.up)) {
|
|
457
554
|
optionIndex = Math.max(0, optionIndex - 1);
|
|
555
|
+
needsFollow = true; // §options-scroll: keep the selection visible
|
|
458
556
|
refresh();
|
|
459
557
|
return;
|
|
460
558
|
}
|
|
461
559
|
if (matchesKey(data, Key.down)) {
|
|
462
560
|
optionIndex = Math.min(opts.length - 1, optionIndex + 1);
|
|
561
|
+
needsFollow = true;
|
|
463
562
|
refresh();
|
|
464
563
|
return;
|
|
465
564
|
}
|
|
@@ -633,11 +732,13 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
633
732
|
function renderOptions() {
|
|
634
733
|
optionsStartIndex = lines.length;
|
|
635
734
|
for (let i = 0; i < opts.length; i++) {
|
|
735
|
+
const start = lines.length;
|
|
636
736
|
const opt = opts[i];
|
|
637
737
|
const selected = i === optionIndex;
|
|
638
738
|
const prefix = selected ? theme.fg("accent", "> ") : " ";
|
|
639
739
|
const recTag = !opt.isCustom && q?.recommended === i ? theme.fg("success", " ★") : "";
|
|
640
740
|
addWrapped(prefix + theme.fg(selected ? "accent" : "text", `${i + 1}. ${opt.label}`) + recTag);
|
|
741
|
+
optionRanges[i] = [start, lines.length - 1];
|
|
641
742
|
}
|
|
642
743
|
}
|
|
643
744
|
|
|
@@ -705,11 +806,21 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
705
806
|
// objective-box middle is sacrificed there because the full objective
|
|
706
807
|
// is always in the scrollable transcript presentation (renderCall).
|
|
707
808
|
if (maxDialogLines !== undefined && lines.length > maxDialogLines) {
|
|
708
|
-
const optionsImmediatelyAfterHead = !inputMode && currentTab !== questions.length && !!q && !q.context && opts.length > 0;
|
|
709
809
|
const proposalSegments = !inputMode && currentTab !== questions.length && !!q && q.context
|
|
710
810
|
? findProposalPresentationSegments(lines, optionsStartIndex)
|
|
711
811
|
: null;
|
|
712
|
-
|
|
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
|
+
}
|
|
713
824
|
}
|
|
714
825
|
cachedLines = lines;
|
|
715
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",
|