pi-goal-x 0.27.2 → 0.27.3
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.
|
@@ -159,6 +159,11 @@ export const WIDGET_HEIGHT_RESERVE = 6;
|
|
|
159
159
|
* own and can never trigger pi-tui's clearOnShrink full-render
|
|
160
160
|
* (`\x1b[2J\x1b[H\x1b[3J` scrollback wipe). No-op (unbounded) when
|
|
161
161
|
* terminalRows is missing (mock TUI, headless contexts, /goal-status text).
|
|
162
|
+
*
|
|
163
|
+
* This pure head-slice is the terminal bound for direct callers. The live
|
|
164
|
+
* component additionally applies the sticky-cap bound
|
|
165
|
+
* (spec 2026-08-11-stable-widget-height) so the rendered height stays
|
|
166
|
+
* constant once the widget reaches the cap — see applyStableHeightBound.
|
|
162
167
|
*/
|
|
163
168
|
export function boundWidgetRenderLines(lines: string[], terminalRows: number | undefined): string[] {
|
|
164
169
|
if (!terminalRows || terminalRows <= 0) return lines;
|
|
@@ -167,6 +172,68 @@ export function boundWidgetRenderLines(lines: string[], terminalRows: number | u
|
|
|
167
172
|
return lines.slice(0, cap);
|
|
168
173
|
}
|
|
169
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Sticky-cap rendered height (spec 2026-08-11-stable-widget-height).
|
|
177
|
+
*
|
|
178
|
+
* The widget's natural (unbounded) height varies with goal state — activity
|
|
179
|
+
* feed growth, current-task contract/evidence wrapping, verification text,
|
|
180
|
+
* budget, task growth (measured 4..31 lines on a 24-row terminal). When the
|
|
181
|
+
* widget is taller than the terminal, every natural-height change alters the
|
|
182
|
+
* dock height → the terminal buffer's line count changes → the terminal
|
|
183
|
+
* scrolls to the bottom and the user cannot hold a scroll position to read
|
|
184
|
+
* the chat. The sticky cap makes the rendered height invariant to goal-state
|
|
185
|
+
* changes while the widget is at the cap:
|
|
186
|
+
*
|
|
187
|
+
* - once natural > cap in the current regime, render exactly `cap` lines on
|
|
188
|
+
* every later render of that regime (head slice when natural > cap, blank
|
|
189
|
+
* padding when natural dips below — the height never changes);
|
|
190
|
+
* - resizing the terminal clears the latch and re-evaluates
|
|
191
|
+
* min(natural, newCap): growing the terminal reveals more of the widget;
|
|
192
|
+
* once it fits, everything renders (the fits case stays byte-identical);
|
|
193
|
+
* - a regime change (goal id/status, widget state kind, compact↔expanded,
|
|
194
|
+
* debug mode, tasks disabled) clears the latch so the new mode starts from
|
|
195
|
+
* its own natural height.
|
|
196
|
+
*
|
|
197
|
+
* Pure given the persisted latch state: deterministic, no timers, no
|
|
198
|
+
* randomness — the same goal state on the same terminal renders the same
|
|
199
|
+
* lines on every render.
|
|
200
|
+
*/
|
|
201
|
+
export function applyStableHeightBound(
|
|
202
|
+
lines: string[],
|
|
203
|
+
terminalRows: number | undefined,
|
|
204
|
+
state: { stickyCap: number | undefined; stickyRegime: string | undefined; stickyTerminalRows: number | undefined },
|
|
205
|
+
regime: string,
|
|
206
|
+
): string[] {
|
|
207
|
+
if (!terminalRows || terminalRows <= 0) return lines;
|
|
208
|
+
const cap = Math.max(1, terminalRows - WIDGET_HEIGHT_RESERVE);
|
|
209
|
+
if (state.stickyTerminalRows !== terminalRows) {
|
|
210
|
+
// Terminal resized: adapt to the new height (grow/shrink to fit).
|
|
211
|
+
state.stickyCap = undefined;
|
|
212
|
+
state.stickyTerminalRows = terminalRows;
|
|
213
|
+
}
|
|
214
|
+
if (state.stickyRegime !== regime) {
|
|
215
|
+
// Mode/state changed: the latch belongs to one regime.
|
|
216
|
+
state.stickyCap = undefined;
|
|
217
|
+
state.stickyRegime = regime;
|
|
218
|
+
}
|
|
219
|
+
if (lines.length > cap) {
|
|
220
|
+
// At the cap: latch the rendered height and head-slice (the dashboard's
|
|
221
|
+
// content priority is top-down).
|
|
222
|
+
state.stickyCap = cap;
|
|
223
|
+
return lines.slice(0, cap);
|
|
224
|
+
}
|
|
225
|
+
if (state.stickyCap !== undefined && lines.length < state.stickyCap) {
|
|
226
|
+
// Natural dipped below the committed height: pad deterministically so
|
|
227
|
+
// the buffer line count never changes. Blank rows after the box footer
|
|
228
|
+
// — honest filler (the …/↑ N more markers are only for hidden content).
|
|
229
|
+
const padded = lines.slice();
|
|
230
|
+
while (padded.length < state.stickyCap) padded.push("");
|
|
231
|
+
return padded;
|
|
232
|
+
}
|
|
233
|
+
// Fits case (natural ≤ cap, no latch): byte-identical to today.
|
|
234
|
+
return lines;
|
|
235
|
+
}
|
|
236
|
+
|
|
170
237
|
function heading(theme: Theme, width: number, left: string, right = ""): string {
|
|
171
238
|
if (!right) return fit(left, width);
|
|
172
239
|
const rightPart = ` ${right}`;
|
|
@@ -273,6 +340,16 @@ export class GoalWidgetComponent implements Component {
|
|
|
273
340
|
private lastSeenGoalId: string | undefined;
|
|
274
341
|
private lastSeenLatestCompletedAt: string | undefined;
|
|
275
342
|
|
|
343
|
+
// Sticky-cap latch (spec 2026-08-11-stable-widget-height): once the widget
|
|
344
|
+
// reaches the terminal cap in a regime, its rendered height stays constant
|
|
345
|
+
// (cap lines) until the regime or the terminal size changes, so the buffer
|
|
346
|
+
// line count never changes and the terminal stops jumping to the bottom.
|
|
347
|
+
private stableHeightState: { stickyCap: number | undefined; stickyRegime: string | undefined; stickyTerminalRows: number | undefined } = {
|
|
348
|
+
stickyCap: undefined,
|
|
349
|
+
stickyRegime: undefined,
|
|
350
|
+
stickyTerminalRows: undefined,
|
|
351
|
+
};
|
|
352
|
+
|
|
276
353
|
constructor(options: GoalWidgetOptions) {
|
|
277
354
|
this.theme = options.theme;
|
|
278
355
|
this.tui = options.tui;
|
|
@@ -358,10 +435,23 @@ export class GoalWidgetComponent implements Component {
|
|
|
358
435
|
// pattern as the questionnaire churn guard; mock TUIs without a
|
|
359
436
|
// `terminal` render unbounded.
|
|
360
437
|
const terminalRows = (this.tui as unknown as { terminal?: { rows?: number } }).terminal?.rows;
|
|
438
|
+
// Render the current branch unbounded (natural), then apply the
|
|
439
|
+
// stable-height bound (spec 2026-08-11): the rendered height is constant
|
|
440
|
+
// while the widget is at the cap, so the dock height / buffer line count
|
|
441
|
+
// stop changing and the terminal stops jumping to the bottom.
|
|
442
|
+
const natural = this.renderNatural(width);
|
|
443
|
+
const regime = this.stableHeightRegime();
|
|
444
|
+
return applyStableHeightBound(natural, terminalRows, this.stableHeightState, regime);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** The current widget branch rendered unbounded (no terminal-height bound). */
|
|
448
|
+
private renderNatural(width: number): string[] {
|
|
449
|
+
const settings = this.getSettings();
|
|
450
|
+
const safeWidth = Math.max(1, width);
|
|
361
451
|
// §15.4: a finished audit shows its result card until cleared.
|
|
362
452
|
const auditResult = this.getAuditResult();
|
|
363
453
|
if (auditResult) {
|
|
364
|
-
return
|
|
454
|
+
return clampLinesToWidth(renderAuditResultCardView(auditResult, this.theme, width), width);
|
|
365
455
|
}
|
|
366
456
|
const goal = this.getGoal();
|
|
367
457
|
const otherCount = Math.max(0, this.getOpenGoalCount() - 1);
|
|
@@ -372,7 +462,7 @@ export class GoalWidgetComponent implements Component {
|
|
|
372
462
|
tasksDisabled: settings.disableTasks === true,
|
|
373
463
|
}) : null;
|
|
374
464
|
this.maybeReanchor(model);
|
|
375
|
-
|
|
465
|
+
const lines = renderGoalWidgetLines(this.getGoal(), this.theme, safeWidth, {
|
|
376
466
|
openGoalCount: this.getOpenGoalCount(),
|
|
377
467
|
auditorProgress: this.getAuditorProgress(),
|
|
378
468
|
disableTasks: settings.disableTasks,
|
|
@@ -385,12 +475,28 @@ export class GoalWidgetComponent implements Component {
|
|
|
385
475
|
expandedScrollOffset: this.expandedScrollOffset,
|
|
386
476
|
expandedTaskRows: expandedTaskViewportRows(this.lastRenderWidth),
|
|
387
477
|
keybindings: settings.keybindings?.dashboard,
|
|
388
|
-
terminalRows,
|
|
389
478
|
});
|
|
390
479
|
if (this.getDebugMode()) {
|
|
391
480
|
lines.push(...this.renderDebugPanel(width));
|
|
392
481
|
}
|
|
393
|
-
return
|
|
482
|
+
return clampLinesToWidth(lines, width);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Sticky-cap regime key: the latch belongs to one goal, goal status,
|
|
487
|
+
* widget state kind (focused / audit / result / unfocused / none),
|
|
488
|
+
* expansion mode, debug mode, and task setting — a change clears the latch
|
|
489
|
+
* so the new mode starts from its own natural height.
|
|
490
|
+
*/
|
|
491
|
+
private stableHeightRegime(): string {
|
|
492
|
+
const settings = this.getSettings();
|
|
493
|
+
const goal = this.getGoal();
|
|
494
|
+
let stateKind: string;
|
|
495
|
+
if (this.getAuditResult()) stateKind = "result";
|
|
496
|
+
else if (this.getAuditorProgress()) stateKind = "audit";
|
|
497
|
+
else if (goal) stateKind = "focused";
|
|
498
|
+
else stateKind = this.getOpenGoalCount() > 0 ? "unfocused" : "none";
|
|
499
|
+
return [goal?.id ?? "∅", goal?.status ?? "∅", stateKind, String(this.getExpanded()), String(this.getDebugMode()), String(settings.disableTasks === true)].join("|");
|
|
394
500
|
}
|
|
395
501
|
|
|
396
502
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-x",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.3",
|
|
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",
|