pi-better-goal 0.1.20 → 0.1.22
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/package.json +1 -1
- package/src/goal-clock.ts +18 -2
- package/src/index.ts +69 -16
package/package.json
CHANGED
package/src/goal-clock.ts
CHANGED
|
@@ -62,7 +62,7 @@ export function formatGoalClock(
|
|
|
62
62
|
): string {
|
|
63
63
|
const timing = goalTiming(goal, now);
|
|
64
64
|
return (
|
|
65
|
-
`Goal [${goal.status}]: ${goal.objective}` +
|
|
65
|
+
`Goal [${goal.status}]: ${flattenObjectiveForRail(goal.objective)}` +
|
|
66
66
|
` | active ${formatClockDuration(timing.activeSeconds)}` +
|
|
67
67
|
` | elapsed ${formatClockDuration(timing.elapsedSeconds)}`
|
|
68
68
|
);
|
|
@@ -85,10 +85,26 @@ export function renderGoalClockLine(
|
|
|
85
85
|
if (fixedWidth >= width) {
|
|
86
86
|
return truncateToWidth(`${heading} ${active}`, width);
|
|
87
87
|
}
|
|
88
|
-
const objective = truncateToWidth(
|
|
88
|
+
const objective = truncateToWidth(
|
|
89
|
+
flattenObjectiveForRail(goal.objective),
|
|
90
|
+
width - fixedWidth,
|
|
91
|
+
);
|
|
89
92
|
return styleGoalRailLine(heading, `${active} ${objective}`, fg);
|
|
90
93
|
}
|
|
91
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Goal rail / clock lines must stay one terminal row. Raw objectives can keep
|
|
97
|
+
* newlines for prompts and `/goal` inspection, but embedding them here makes
|
|
98
|
+
* the widget taller than the dock budget and desyncs main-screen differential
|
|
99
|
+
* paints (Working... / Elapsed stacking into scrollback).
|
|
100
|
+
*/
|
|
101
|
+
export function flattenObjectiveForRail(objective: string): string {
|
|
102
|
+
return objective
|
|
103
|
+
.replace(/[\r\n\t]+/g, " ")
|
|
104
|
+
.replace(/ {2,}/g, " ")
|
|
105
|
+
.trim();
|
|
106
|
+
}
|
|
107
|
+
|
|
92
108
|
function styleGoalRailLine(
|
|
93
109
|
heading: string,
|
|
94
110
|
tail: string,
|
package/src/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
import { continuationEvidence, type ContinuationEvidence } from "./continuation.js";
|
|
25
25
|
import { observeGoalStall } from "./stall.js";
|
|
26
26
|
import {
|
|
27
|
+
flattenObjectiveForRail,
|
|
27
28
|
goalClockRefreshDelayMs,
|
|
28
29
|
goalTiming,
|
|
29
30
|
isGoalClockVisible,
|
|
@@ -164,7 +165,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
164
165
|
let continuationQueuedFor: string | null = null;
|
|
165
166
|
let idleContinuationTimer: ReturnType<typeof setTimeout> | undefined;
|
|
166
167
|
let idleContinuationSignature = "";
|
|
167
|
-
let refreshGoalWidget: (() => void) | undefined;
|
|
168
|
+
let refreshGoalWidget: ((force?: boolean) => void) | undefined;
|
|
168
169
|
let lastAgentEvidence: ContinuationEvidence | null = null;
|
|
169
170
|
|
|
170
171
|
const getGoal = (ctx: ExtensionContext): GoalSnapshot | null => currentGoalSnapshot(ctx);
|
|
@@ -180,21 +181,53 @@ export default function (pi: ExtensionAPI): void {
|
|
|
180
181
|
appendContinuationState(createContinuationState(goal.goalId));
|
|
181
182
|
};
|
|
182
183
|
|
|
184
|
+
const isForegroundBusy = (ctx: ExtensionContext): boolean =>
|
|
185
|
+
foregroundRunning || !ctx.isIdle();
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Chat `notify` appends lines above the Working/bash status region. While the
|
|
189
|
+
* agent is streaming, that height change desyncs Pi main-screen differential
|
|
190
|
+
* rendering and stacks "Working..." / "Elapsed Xs" into scrollback. Keep
|
|
191
|
+
* mid-stream feedback out of the transcript; use the footer status instead.
|
|
192
|
+
*/
|
|
193
|
+
const notifyGoal = (
|
|
194
|
+
ctx: ExtensionContext,
|
|
195
|
+
message: string,
|
|
196
|
+
type: "info" | "warning" | "error" = "info",
|
|
197
|
+
): void => {
|
|
198
|
+
if (!ctx.hasUI) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (isForegroundBusy(ctx) && type !== "error") {
|
|
202
|
+
try {
|
|
203
|
+
ctx.ui.setStatus(EXTENSION_NAME, message);
|
|
204
|
+
} catch {
|
|
205
|
+
// Footer status is best-effort only.
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
ctx.ui.notify(message, type);
|
|
210
|
+
};
|
|
211
|
+
|
|
183
212
|
const setGoal = (goal: GoalSnapshot, ctx: ExtensionContext, source: GoalEntrySource): void => {
|
|
213
|
+
const previous = getGoal(ctx);
|
|
214
|
+
const wasVisible = previous !== null && isGoalClockVisible(previous);
|
|
184
215
|
pi.appendEntry(EXTENSION_NAME, goalSetEntry(goal, source));
|
|
185
216
|
continuationQueuedFor = null;
|
|
186
217
|
backgroundDrainTracker = null;
|
|
187
218
|
clearIdleContinuation();
|
|
188
|
-
|
|
219
|
+
// Force a full redraw when the dock height changes (absent ↔ visible clock).
|
|
220
|
+
refreshGoalWidget?.(!wasVisible || !isGoalClockVisible(goal));
|
|
189
221
|
};
|
|
190
222
|
|
|
191
223
|
const clearGoal = (ctx: ExtensionContext, source: GoalEntrySource): void => {
|
|
192
224
|
const current = getGoal(ctx);
|
|
225
|
+
const wasVisible = current !== null && isGoalClockVisible(current);
|
|
193
226
|
pi.appendEntry(EXTENSION_NAME, goalClearEntry(current?.goalId ?? null, source));
|
|
194
227
|
continuationQueuedFor = null;
|
|
195
228
|
backgroundDrainTracker = null;
|
|
196
229
|
clearIdleContinuation();
|
|
197
|
-
refreshGoalWidget?.();
|
|
230
|
+
refreshGoalWidget?.(wasVisible);
|
|
198
231
|
};
|
|
199
232
|
|
|
200
233
|
const queueGoalContinuation = (goal: GoalSnapshot): void => {
|
|
@@ -395,8 +428,24 @@ export default function (pi: ExtensionAPI): void {
|
|
|
395
428
|
ctx.ui.setWidget(
|
|
396
429
|
EXTENSION_NAME,
|
|
397
430
|
(tui, theme) => {
|
|
398
|
-
const
|
|
399
|
-
|
|
431
|
+
const requestRender = (force = false): void => {
|
|
432
|
+
try {
|
|
433
|
+
tui.requestRender(force);
|
|
434
|
+
} catch {
|
|
435
|
+
// UI paint is best-effort only.
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
const scheduler = createRenderScheduler(() => requestRender(false));
|
|
439
|
+
const refresh = (force = false): void => {
|
|
440
|
+
if (force) {
|
|
441
|
+
scheduler.cancel();
|
|
442
|
+
// Height transitions must reset main-screen differential state so
|
|
443
|
+
// Working/Elapsed lines keep rewriting in place instead of stacking.
|
|
444
|
+
requestRender(true);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
scheduler.request();
|
|
448
|
+
};
|
|
400
449
|
refreshGoalWidget = refresh;
|
|
401
450
|
|
|
402
451
|
return {
|
|
@@ -444,52 +493,56 @@ export default function (pi: ExtensionAPI): void {
|
|
|
444
493
|
|
|
445
494
|
if (!trimmed) {
|
|
446
495
|
const continuation = current ? currentContinuationState(ctx, current.goalId) : null;
|
|
496
|
+
// Inspection is always explicit user intent; show the full summary even while busy.
|
|
447
497
|
ctx.ui.notify(formatGoal(current, continuation, observeGoalStall(current, continuation, { foregroundRunning })), "info");
|
|
448
498
|
return;
|
|
449
499
|
}
|
|
450
500
|
|
|
451
501
|
if (trimmed === "pause") {
|
|
452
502
|
if (!current || current.status !== "active") {
|
|
453
|
-
ctx
|
|
503
|
+
notifyGoal(ctx, "Only active goals can be paused.", "warning");
|
|
454
504
|
return;
|
|
455
505
|
}
|
|
456
506
|
setGoal(goalWithStatus(current, "paused"), ctx, "command");
|
|
457
|
-
ctx
|
|
507
|
+
notifyGoal(ctx, "Goal paused.");
|
|
458
508
|
return;
|
|
459
509
|
}
|
|
460
510
|
|
|
461
511
|
if (trimmed === "resume") {
|
|
462
512
|
if (!current || current.status !== "paused") {
|
|
463
|
-
ctx
|
|
513
|
+
notifyGoal(ctx, "Only paused goals can be resumed.", "warning");
|
|
464
514
|
return;
|
|
465
515
|
}
|
|
466
516
|
const goal = goalWithStatus(current, "active");
|
|
467
517
|
setGoal(goal, ctx, "command");
|
|
468
518
|
queueGoalContinuation(goal);
|
|
469
|
-
ctx
|
|
519
|
+
notifyGoal(ctx, "Goal resumed.");
|
|
470
520
|
return;
|
|
471
521
|
}
|
|
472
522
|
|
|
473
523
|
if (trimmed === "clear") {
|
|
474
524
|
clearGoal(ctx, "command");
|
|
475
|
-
ctx
|
|
525
|
+
notifyGoal(ctx, "Goal cleared.");
|
|
476
526
|
return;
|
|
477
527
|
}
|
|
478
528
|
|
|
479
529
|
if (trimmed === "complete") {
|
|
480
530
|
if (!current) {
|
|
481
|
-
ctx
|
|
531
|
+
notifyGoal(ctx, "No goal is set.", "warning");
|
|
482
532
|
return;
|
|
483
533
|
}
|
|
484
534
|
setGoal(goalWithStatus(current, "complete"), ctx, "command");
|
|
485
|
-
ctx
|
|
535
|
+
notifyGoal(ctx, "Goal marked complete.");
|
|
486
536
|
return;
|
|
487
537
|
}
|
|
488
538
|
|
|
489
|
-
|
|
539
|
+
// Confirm dialogs swap the editor and reflow the dock. While the agent is
|
|
540
|
+
// streaming that desyncs main-screen differential paints, so replace
|
|
541
|
+
// silently mid-turn and only confirm when idle.
|
|
542
|
+
if (current && current.status !== "complete" && ctx.hasUI && !isForegroundBusy(ctx)) {
|
|
490
543
|
const replace = await ctx.ui.confirm(
|
|
491
544
|
"Replace active goal?",
|
|
492
|
-
`Current goal: ${current.objective}`,
|
|
545
|
+
`Current goal: ${flattenObjectiveForRail(current.objective)}`,
|
|
493
546
|
);
|
|
494
547
|
if (!replace) {
|
|
495
548
|
return;
|
|
@@ -498,9 +551,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
498
551
|
|
|
499
552
|
try {
|
|
500
553
|
const goal = startOrReplaceGoal(trimmed, null, ctx, "command");
|
|
501
|
-
ctx
|
|
554
|
+
notifyGoal(ctx, `Goal set: ${flattenObjectiveForRail(goal.objective)}`);
|
|
502
555
|
} catch (error) {
|
|
503
|
-
ctx
|
|
556
|
+
notifyGoal(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
504
557
|
}
|
|
505
558
|
},
|
|
506
559
|
});
|