pi-goal-x 0.25.3 → 0.26.0
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 +5 -3
- package/docs/architecture.md +1 -1
- package/docs/unified-dashboard.md +9 -8
- package/extensions/goal-draft.ts +1 -0
- package/extensions/goal-drafting.ts +24 -1
- package/extensions/goal-events.ts +18 -2
- package/extensions/goal-questionnaire.ts +161 -6
- package/extensions/widgets/goal-dashboard-renderer.ts +15 -9
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -260,12 +260,14 @@ Always visible above the editor while a goal is focused:
|
|
|
260
260
|
│ Current t3 · Add the download button │
|
|
261
261
|
│ Verify Run npm test with zero failures. │
|
|
262
262
|
│ File .pi/goals/active_goal_... │
|
|
263
|
-
╰─ Ctrl+Shift+T: expand tasks
|
|
263
|
+
╰─ Ctrl+Shift+T: expand tasks─────── Ctrl+Shift+A: toggle auditor ● ─╯
|
|
264
264
|
```
|
|
265
265
|
|
|
266
266
|
The green/gray `●` at the bottom-right of the border is the focused goal's
|
|
267
|
-
independent-auditor status (green = on, gray = off)
|
|
268
|
-
|
|
267
|
+
independent-auditor status (green = on, gray = off). Wide/medium footers
|
|
268
|
+
right-align the `Ctrl+Shift+A: toggle auditor` note beside the dot — it
|
|
269
|
+
shows the shortcut that turns the auditor on and off; narrow/minimal keep
|
|
270
|
+
just the dot.
|
|
269
271
|
|
|
270
272
|
### Expanded mode
|
|
271
273
|
|
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 |
|
|
@@ -32,7 +32,7 @@ Always visible above the editor while a goal is focused:
|
|
|
32
32
|
│ Current t3 · Add the download button │
|
|
33
33
|
│ Verify Run npm test with zero failures. │
|
|
34
34
|
│ File .pi/goals/active_goal_g1.md │
|
|
35
|
-
╰─ Ctrl+Shift+T: expand tasks
|
|
35
|
+
╰─ Ctrl+Shift+T: expand tasks─────────────────────────── Ctrl+Shift+A: toggle auditor ● ─╯
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
Compact rows, in order:
|
|
@@ -74,13 +74,14 @@ Compact rows, in order:
|
|
|
74
74
|
split; the header line is likewise one tone with only the `pi-goal-x`
|
|
75
75
|
brand in accent. The auditor status lives in the bottom-right of this
|
|
76
76
|
border: a single `●` — green when the focused goal's independent auditor
|
|
77
|
-
is on, gray when off — drawn right before the `╯` corner
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
is on, gray when off — drawn right before the `╯` corner. In wide/medium
|
|
78
|
+
layouts the footer also right-aligns a `Ctrl+Shift+A: toggle auditor`
|
|
79
|
+
note beside the dot, making explicit that the shortcut turns the auditor
|
|
80
|
+
on and off; narrow and minimal keep just the dot. `Ctrl+Shift+A` toggles
|
|
81
|
+
the auditor per-goal (persisted, `auditor_toggled` ledger event,
|
|
82
|
+
dashboard refresh, notification; inert with no focused goal, a goal modal
|
|
83
|
+
open, or a complete goal). The expanded dashboard does not render the
|
|
84
|
+
auditor indicator.
|
|
84
85
|
|
|
85
86
|
When every top-level task is done, the current line reads
|
|
86
87
|
`Current All tasks complete`. With `disableTasks` enabled the task rows are
|
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
|
|
|
@@ -454,7 +454,30 @@ export function registerDraftingTools(core: GoalCore): void {
|
|
|
454
454
|
clearGoalDrafting(core, ctx);
|
|
455
455
|
return { content: [{ type: "text", text: `${summary}\n\nGoal tweak confirmed and applied.` }], details: goalDetails(result.goal), terminate: true };
|
|
456
456
|
},
|
|
457
|
-
renderCall(args, theme) {
|
|
457
|
+
renderCall(args, theme) {
|
|
458
|
+
// §proposal-presentation: the tool-call display lands in the terminal
|
|
459
|
+
// buffer the moment the tool call starts — BEFORE the confirmation
|
|
460
|
+
// dialog opens — so it is the scrollable "complete presentation" of
|
|
461
|
+
// the goal. It must show the full objective contract AND every task
|
|
462
|
+
// line; the dialog frame alone cannot (its churn-guard bound keeps
|
|
463
|
+
// it within the terminal height). The user can scroll up and read
|
|
464
|
+
// everything while the dialog is open ("the user can just scroll").
|
|
465
|
+
const objective = String(args?.objective ?? "");
|
|
466
|
+
let text = theme.fg("toolTitle", "propose_goal_draft ") + theme.fg("muted", objective);
|
|
467
|
+
const flatTasks = Array.isArray(args?.tasks) ? (args.tasks as FlatTaskInput[]) : [];
|
|
468
|
+
const derived = flatTasks.length === 0 ? (deriveTasksFromObjective(objective) ?? []) : [];
|
|
469
|
+
const taskBlock = flatTasks.length > 0
|
|
470
|
+
? { header: "Tasks proposed for confirmation:", lines: flatTasks.map((t) => `[ ] ${t.id}: ${t.title}`) }
|
|
471
|
+
: derived.length > 0
|
|
472
|
+
? { header: "Tasks derived from the objective (confirm or ask the agent to adjust):", lines: renderConfirmationTasks(derived, 0) }
|
|
473
|
+
: null;
|
|
474
|
+
if (taskBlock) {
|
|
475
|
+
text += "\n\n" + theme.fg("toolTitle", theme.bold(taskBlock.header)) + "\n" + theme.fg("toolOutput", taskBlock.lines.join("\n"));
|
|
476
|
+
}
|
|
477
|
+
if (args?.auto_continue === false) text += "\n\n" + theme.fg("muted", "Automatic continuation: disabled");
|
|
478
|
+
if (args?.block_completion === true) text += "\n\n" + theme.fg("muted", "Block completion on pending tasks: enabled");
|
|
479
|
+
return new Text(text, 0, 0);
|
|
480
|
+
},
|
|
458
481
|
renderResult(result, _opts, theme) { return renderGoalResult(result, _opts, theme); },
|
|
459
482
|
}));
|
|
460
483
|
}
|
|
@@ -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,139 @@ 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
|
+
/**
|
|
58
|
+
* Locate the proposal tasks segment and the options tail in a rendered dialog.
|
|
59
|
+
* Returns null when the dialog is not a proposal-style confirmation (no
|
|
60
|
+
* "Tasks proposed for confirmation:" header / "┌─ TASKS ─" box).
|
|
61
|
+
*/
|
|
62
|
+
export function findProposalPresentationSegments(lines: string[], tailStart: number): ProposalPresentationSegments | null {
|
|
63
|
+
if (tailStart < 0) return null;
|
|
64
|
+
const tasksStart = lines.findIndex((l) => l.includes("Tasks proposed for confirmation:") || l.includes("┌─ TASKS ─"));
|
|
65
|
+
if (tasksStart < 0 || tasksStart >= tailStart) return null;
|
|
66
|
+
let tasksEnd = tasksStart;
|
|
67
|
+
for (let i = tasksStart + 1; i < tailStart; i++) {
|
|
68
|
+
if (/^\s*\[[ x~]\]/.test(lines[i])) tasksEnd = i;
|
|
69
|
+
else break;
|
|
70
|
+
}
|
|
71
|
+
return { tasksStart, tasksEnd, tailStart };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Fit a rendered dialog to the terminal-height bound without ever hiding the
|
|
76
|
+
* question. The protected head (top border + tabs + question line) is always
|
|
77
|
+
* kept; the remaining budget is spent on the tail — options/footer/bottom
|
|
78
|
+
* border — so long context blocks (proposal confirmations) are sliced from
|
|
79
|
+
* their head exactly as the pre-fix tail-slice did (383ae52 surface). When the
|
|
80
|
+
* options block starts immediately after the head (plain agent questions with
|
|
81
|
+
* no context block), the TOP options are kept instead of the tail so the
|
|
82
|
+
* recommended/first option stays visible; the footer hint and bottom border
|
|
83
|
+
* are always the last rendered lines. Never returns more than maxDialogLines.
|
|
84
|
+
*
|
|
85
|
+
* Proposal confirmations (proposal segments given) keep the head, the tasks
|
|
86
|
+
* section (header + every task line), and the options/footer/bottom border in
|
|
87
|
+
* frame; only the objective-box middle is sacrificed in-frame — the full
|
|
88
|
+
* objective is always present in the scrollable transcript presentation
|
|
89
|
+
* (propose_goal_draft renderCall), so nothing of the goal is ever omitted.
|
|
90
|
+
*/
|
|
91
|
+
export function fitDialogLines(
|
|
92
|
+
lines: string[],
|
|
93
|
+
maxDialogLines: number,
|
|
94
|
+
protectedHead: number,
|
|
95
|
+
optionsImmediatelyAfterHead = false,
|
|
96
|
+
proposal: ProposalPresentationSegments | null = null,
|
|
97
|
+
): string[] {
|
|
98
|
+
if (maxDialogLines <= 0 || lines.length <= maxDialogLines) return lines;
|
|
99
|
+
const keepHead = Math.min(protectedHead, maxDialogLines);
|
|
100
|
+
const budget = maxDialogLines - keepHead;
|
|
101
|
+
if (budget <= 0) return lines.slice(0, keepHead);
|
|
102
|
+
const rest = lines.slice(protectedHead);
|
|
103
|
+
if (rest.length <= budget) return [...lines.slice(0, keepHead), ...rest];
|
|
104
|
+
if (proposal) {
|
|
105
|
+
return fitProposalPresentation(lines, maxDialogLines, keepHead, proposal);
|
|
106
|
+
}
|
|
107
|
+
if (optionsImmediatelyAfterHead) {
|
|
108
|
+
// Plain select-mode question: keep the footer hint + bottom border (as
|
|
109
|
+
// many as fit), then spend the remaining room on the TOP options so the
|
|
110
|
+
// recommended/first option stays visible; leading blank separators are
|
|
111
|
+
// dropped first when room is short. Never exceeds maxDialogLines.
|
|
112
|
+
const headLines = lines.slice(0, keepHead);
|
|
113
|
+
const firstContent = rest.findIndex((l) => l.trim().length > 0);
|
|
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];
|
|
123
|
+
}
|
|
124
|
+
// Context-heavy / input / submit dialogs: keep the head and the tail
|
|
125
|
+
// (options, footer, bottom border) exactly as the pre-fix tail-slice did.
|
|
126
|
+
return [...lines.slice(0, keepHead), ...rest.slice(rest.length - budget)];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Proposal confirmation fit: keep the protected head, the tasks section, and
|
|
131
|
+
* the options/footer/bottom border; the objective-box middle is sacrificed
|
|
132
|
+
* in-frame (it stays fully readable in the scrollback presentation). Interior
|
|
133
|
+
* blank spacing lines are dropped first when room is short; task lines are
|
|
134
|
+
* only dropped after that, from the end, when the bound is exhausted (those
|
|
135
|
+
* lines remain in the scrollback presentation). Never exceeds maxDialogLines.
|
|
136
|
+
*/
|
|
137
|
+
function fitProposalPresentation(
|
|
138
|
+
lines: string[],
|
|
139
|
+
maxDialogLines: number,
|
|
140
|
+
keepHead: number,
|
|
141
|
+
proposal: ProposalPresentationSegments,
|
|
142
|
+
): string[] {
|
|
143
|
+
const { tasksStart, tasksEnd, tailStart } = proposal;
|
|
144
|
+
const head = lines.slice(0, keepHead);
|
|
145
|
+
const tasks = lines.slice(Math.max(tasksStart, keepHead), Math.min(tasksEnd, tailStart) + 1);
|
|
146
|
+
const tail = lines.slice(Math.max(tailStart, keepHead));
|
|
147
|
+
const candidate = [...head, ...tasks, ...tail];
|
|
148
|
+
if (candidate.length <= maxDialogLines) return candidate;
|
|
149
|
+
// Tight: drop blank spacing lines below the head (e.g. the blank between
|
|
150
|
+
// the options and the footer hint) before touching any content line.
|
|
151
|
+
const stripped = candidate.filter((l, i) => i < keepHead || l.trim() !== "");
|
|
152
|
+
if (stripped.length <= maxDialogLines) return stripped;
|
|
153
|
+
// Bound still exhausted: keep the head, then spend the room on the tail
|
|
154
|
+
// first (options/footer/bottom border — the actionable decision surface —
|
|
155
|
+
// kept from its end so the border and footer never drop), then on the
|
|
156
|
+
// tasks from the start. The dropped task lines remain fully readable in
|
|
157
|
+
// the scrollback presentation. Never exceeds maxDialogLines.
|
|
158
|
+
const tailNoBlanks = tail.filter((l) => l.trim() !== "");
|
|
159
|
+
const keepTail = Math.min(tailNoBlanks.length, Math.max(0, maxDialogLines - keepHead));
|
|
160
|
+
const tailKept = tailNoBlanks.slice(tailNoBlanks.length - keepTail);
|
|
161
|
+
const keepTasks = Math.min(tasks.length, Math.max(0, maxDialogLines - keepHead - keepTail));
|
|
162
|
+
return [...head, ...tasks.slice(0, keepTasks), ...tailKept];
|
|
163
|
+
}
|
|
164
|
+
|
|
32
165
|
export function normalizeQuestionnaireQuestions(rawQuestions: GoalQuestionnaireQuestion[]): GoalQuestionnaireQuestion[] {
|
|
33
166
|
const seenIds = new Set<string>();
|
|
34
167
|
return rawQuestions.map((q, i) => {
|
|
@@ -108,18 +241,20 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
108
241
|
// the frame never exceeds the terminal height — without this, closing a dialog
|
|
109
242
|
// taller than the terminal triggers pi-tui's generic shrink full-render
|
|
110
243
|
// (\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
|
-
//
|
|
244
|
+
// so the window takes ~10s to scroll back to the bottom. The slice keeps
|
|
245
|
+
// the question and the actionable options/footer in view (see
|
|
246
|
+
// fitDialogLines); content that fits renders exactly as the
|
|
247
|
+
// pre-regression (383ae52) UI. Only applies with real TUI dimensions.
|
|
114
248
|
const tuiInfo = tui as unknown as { terminal?: { rows?: number }; previousLines?: string[] };
|
|
115
249
|
const terminalRows = tuiInfo.terminal?.rows;
|
|
116
250
|
const baseFrame = tuiInfo.previousLines?.length;
|
|
117
|
-
const maxDialogLines =
|
|
251
|
+
const maxDialogLines = computeDialogLineLimit({ terminalRows, baseFrameLines: baseFrame });
|
|
118
252
|
let currentTab = 0;
|
|
119
253
|
let optionIndex = 0;
|
|
120
254
|
let inputMode = false;
|
|
121
255
|
let inputQuestionId: string | null = null;
|
|
122
256
|
let cachedLines: string[] | undefined;
|
|
257
|
+
let optionsStartIndex = -1;
|
|
123
258
|
let auditorEnabled = auditorToggleInit?.defaultEnabled ?? true;
|
|
124
259
|
const answers = new Map<string, GoalQuestionnaireAnswer>();
|
|
125
260
|
const drafts = new Map<string, string>();
|
|
@@ -439,6 +574,9 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
439
574
|
};
|
|
440
575
|
|
|
441
576
|
add(theme.fg("accent", "─".repeat(safeWidth)));
|
|
577
|
+
// Lines up to the question line (top border, tabs, question incl.
|
|
578
|
+
// wraps) are the protected head — the slice below must never hide them.
|
|
579
|
+
let protectedCount = lines.length;
|
|
442
580
|
if (isMulti) {
|
|
443
581
|
const tabs: string[] = ["← "];
|
|
444
582
|
for (let i = 0; i < questions.length; i++) {
|
|
@@ -453,9 +591,11 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
453
591
|
tabs.push(" →");
|
|
454
592
|
add(` ${tabs.join("")}`);
|
|
455
593
|
lines.push("");
|
|
594
|
+
protectedCount = lines.length;
|
|
456
595
|
}
|
|
457
596
|
|
|
458
597
|
function renderOptions() {
|
|
598
|
+
optionsStartIndex = lines.length;
|
|
459
599
|
for (let i = 0; i < opts.length; i++) {
|
|
460
600
|
const opt = opts[i];
|
|
461
601
|
const selected = i === optionIndex;
|
|
@@ -467,6 +607,7 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
467
607
|
|
|
468
608
|
if (inputMode && q) {
|
|
469
609
|
addWrapped(theme.fg("text", ` ${q.question}`));
|
|
610
|
+
protectedCount = lines.length;
|
|
470
611
|
if (q.context) renderContextLines(q.context);
|
|
471
612
|
lines.push("");
|
|
472
613
|
if (q.options.length > 0) {
|
|
@@ -479,6 +620,7 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
479
620
|
add(theme.fg("dim", " Enter to submit • Esc to cancel"));
|
|
480
621
|
} else if (currentTab === questions.length) {
|
|
481
622
|
add(theme.fg("accent", theme.bold(" Ready to submit")));
|
|
623
|
+
protectedCount = lines.length;
|
|
482
624
|
lines.push("");
|
|
483
625
|
for (const question of questions) {
|
|
484
626
|
const answer = answers.get(question.id);
|
|
@@ -488,6 +630,7 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
488
630
|
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
631
|
} else if (q) {
|
|
490
632
|
addWrapped(theme.fg("text", ` ${q.question}`));
|
|
633
|
+
protectedCount = lines.length;
|
|
491
634
|
if (q.context) renderContextLines(q.context);
|
|
492
635
|
// Auditor toggle line between context and options
|
|
493
636
|
if (auditorToggleInit) {
|
|
@@ -516,9 +659,21 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
516
659
|
lines[i] = truncateToWidth(lines[i], safeWidth);
|
|
517
660
|
}
|
|
518
661
|
}
|
|
519
|
-
// Churn guard:
|
|
662
|
+
// Churn guard: bound to the terminal height (see factory top) so the
|
|
663
|
+
// opened frame never exceeds the screen. Never slice the question:
|
|
664
|
+
// keep the protected head, then spend the rest of the budget on the
|
|
665
|
+
// tail (options/footer/bottom border) — or, for plain select-mode
|
|
666
|
+
// questions where the options start right after the head, keep the
|
|
667
|
+
// top options so the recommended one stays visible. Proposal
|
|
668
|
+
// confirmations additionally keep the tasks section in frame; the
|
|
669
|
+
// objective-box middle is sacrificed there because the full objective
|
|
670
|
+
// is always in the scrollable transcript presentation (renderCall).
|
|
520
671
|
if (maxDialogLines !== undefined && lines.length > maxDialogLines) {
|
|
521
|
-
|
|
672
|
+
const optionsImmediatelyAfterHead = !inputMode && currentTab !== questions.length && !!q && !q.context && opts.length > 0;
|
|
673
|
+
const proposalSegments = !inputMode && currentTab !== questions.length && !!q && q.context
|
|
674
|
+
? findProposalPresentationSegments(lines, optionsStartIndex)
|
|
675
|
+
: null;
|
|
676
|
+
lines = fitDialogLines(lines, maxDialogLines, protectedCount, optionsImmediatelyAfterHead, proposalSegments);
|
|
522
677
|
}
|
|
523
678
|
cachedLines = lines;
|
|
524
679
|
return lines;
|
|
@@ -317,7 +317,8 @@ export function renderCompactDashboard(
|
|
|
317
317
|
|
|
318
318
|
// §auditor-toggle: the focused goal's independent-auditor status is the
|
|
319
319
|
// bottom-right border dot (green on / muted gray off, see the footer
|
|
320
|
-
// below);
|
|
320
|
+
// below); wide/medium footers right-align a note next to it explaining
|
|
321
|
+
// that Ctrl+Shift+A turns the auditor on and off.
|
|
321
322
|
|
|
322
323
|
// §9.1 compact task counts + progress bars live in the Tasks header row
|
|
323
324
|
// (below); the standalone progress lines were removed in favor of it.
|
|
@@ -396,21 +397,26 @@ export function renderCompactDashboard(
|
|
|
396
397
|
lines.push(boxLine(theme, safeWidth, `${dim(theme, `File ${model.filePath}`)}`));
|
|
397
398
|
}
|
|
398
399
|
|
|
399
|
-
const footerHint =
|
|
400
|
+
const footerHint = opts.footerHint
|
|
400
401
|
?? (listOverflows
|
|
401
402
|
? mode === "minimal"
|
|
402
403
|
? "↑↓: scroll"
|
|
403
404
|
: mode === "narrow"
|
|
404
405
|
? "Ctrl+Shift+↑↓: scroll"
|
|
405
406
|
: "Ctrl+Shift+T: expand · Ctrl+Shift+↑↓: scroll"
|
|
406
|
-
: spec.footerHint)
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
//
|
|
411
|
-
//
|
|
407
|
+
: spec.footerHint);
|
|
408
|
+
// The auditor dot sits at the bottom-right of the border: green when the
|
|
409
|
+
// focused goal's independent auditor is on, muted gray when off. In
|
|
410
|
+
// wide/medium a right-aligned note (same frame tone as the hint) explains
|
|
411
|
+
// that Ctrl+Shift+A turns the auditor on and off; narrow/minimal keep
|
|
412
|
+
// just the dot. The boxFooter right slot pushes both to the right edge,
|
|
413
|
+
// so the chord and its note are right-aligned by construction.
|
|
412
414
|
const auditorDot = model.auditorEnabled ? success(theme, "●") : muted(theme, "●");
|
|
413
|
-
|
|
415
|
+
const right =
|
|
416
|
+
mode === "wide" || mode === "medium"
|
|
417
|
+
? `${frame(theme, "Ctrl+Shift+A: toggle auditor")} ${auditorDot}`
|
|
418
|
+
: auditorDot;
|
|
419
|
+
lines.push(boxFooter(theme, safeWidth, footerHint, right));
|
|
414
420
|
return lines;
|
|
415
421
|
}
|
|
416
422
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-x",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
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
|
},
|