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.
@@ -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 13 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`, `session_shutdown`) |
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 |
@@ -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
- const derived = deriveTasksFromObjective(objective);
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) { return new Text(theme.fg("toolTitle", "propose_goal_draft ") + theme.fg("muted", args?.objective ?? ""), 0, 0); },
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 GoalCore.
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
- core.queueContinuation(ctx);
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 tail slice keeps
112
- // the actionable options/footer in view; content that fits renders exactly as
113
- // the pre-regression (383ae52) UI. Only applies with real TUI dimensions.
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 = terminalRows && baseFrame ? Math.max(10, terminalRows - baseFrame + 1) : undefined;
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: tail-slice to the terminal-height bound (see factory top).
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
- lines = lines.slice(lines.length - maxDialogLines);
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.25.4",
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.83.0",
66
- "@earendil-works/pi-coding-agent": "^0.83.0",
67
- "@earendil-works/pi-tui": "^0.83.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
  },