pi-better-goal 0.1.18 → 0.1.19

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 CHANGED
@@ -13,6 +13,7 @@ Use `pi-better-goal` when a Pi session should keep an explicit objective visible
13
13
  ## Core Features
14
14
 
15
15
  - `/goal` runtime for starting, pausing, resuming, completing, and clearing the current objective.
16
+ - `escape` pauses the active goal; paused goals are never poked.
16
17
  - A compact goal widget that does not replace Pi's footer.
17
18
  - Background activity tracking for subagents and other registered providers.
18
19
  - A progress-aware follow-up loop that holds after repeated identical outcomes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-goal",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "Pi extension for goal tracking with background-aware continuation.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/goal-state.ts CHANGED
@@ -23,7 +23,7 @@ interface GoalLike {
23
23
  completedAt?: number | null;
24
24
  }
25
25
 
26
- export type GoalEntrySource = "command" | "tool" | "runtime";
26
+ export type GoalEntrySource = "command" | "tool" | "runtime" | "shortcut";
27
27
 
28
28
  export interface ContinuationState {
29
29
  goalId: string;
package/src/index.ts CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  goalWithStatus,
20
20
  validateObjective,
21
21
  validateTokenBudget,
22
+ type GoalEntrySource,
22
23
  } from "./goal-state.js";
23
24
  import { continuationEvidence, type ContinuationEvidence } from "./continuation.js";
24
25
  import { observeGoalStall } from "./stall.js";
@@ -148,6 +149,9 @@ export default function (pi: ExtensionAPI): void {
148
149
 
149
150
  const getGoal = (ctx: ExtensionContext): GoalSnapshot | null => currentGoalSnapshot(ctx);
150
151
 
152
+ /** Only active goals receive autonomous pokes; paused, complete, and budget-limited goals never do. */
153
+ const isPokeable = (goal: GoalSnapshot | null): goal is GoalSnapshot => goal?.status === "active";
154
+
151
155
  const appendContinuationState = (state: ReturnType<typeof createContinuationState>): void => {
152
156
  pi.appendEntry(EXTENSION_NAME, continuationStateEntry(state));
153
157
  };
@@ -156,7 +160,7 @@ export default function (pi: ExtensionAPI): void {
156
160
  appendContinuationState(createContinuationState(goal.goalId));
157
161
  };
158
162
 
159
- const setGoal = (goal: GoalSnapshot, ctx: ExtensionContext, source: "command" | "tool" | "runtime"): void => {
163
+ const setGoal = (goal: GoalSnapshot, ctx: ExtensionContext, source: GoalEntrySource): void => {
160
164
  pi.appendEntry(EXTENSION_NAME, goalSetEntry(goal, source));
161
165
  continuationQueuedFor = null;
162
166
  backgroundDrainTracker = null;
@@ -164,7 +168,7 @@ export default function (pi: ExtensionAPI): void {
164
168
  refreshGoalWidget?.();
165
169
  };
166
170
 
167
- const clearGoal = (ctx: ExtensionContext, source: "command" | "tool" | "runtime"): void => {
171
+ const clearGoal = (ctx: ExtensionContext, source: GoalEntrySource): void => {
168
172
  const current = getGoal(ctx);
169
173
  pi.appendEntry(EXTENSION_NAME, goalClearEntry(current?.goalId ?? null, source));
170
174
  continuationQueuedFor = null;
@@ -174,7 +178,7 @@ export default function (pi: ExtensionAPI): void {
174
178
  };
175
179
 
176
180
  const queueGoalContinuation = (goal: GoalSnapshot): void => {
177
- if (goal.status !== "active" || continuationQueuedFor === goal.goalId) {
181
+ if (!isPokeable(goal) || continuationQueuedFor === goal.goalId) {
178
182
  return;
179
183
  }
180
184
  clearIdleContinuation();
@@ -205,7 +209,7 @@ export default function (pi: ExtensionAPI): void {
205
209
  kind: "continuation" | "background-drained",
206
210
  snapshot?: ActivitySnapshot,
207
211
  ): void => {
208
- if (WAKE_DISABLED || goal.status !== "active" || continuationQueuedFor === goal.goalId) {
212
+ if (WAKE_DISABLED || !isPokeable(goal) || continuationQueuedFor === goal.goalId) {
209
213
  return;
210
214
  }
211
215
  if (kind === "continuation" && currentContinuationState(ctx, goal.goalId)?.blocked) {
@@ -232,7 +236,7 @@ export default function (pi: ExtensionAPI): void {
232
236
  priorSnapshot?: ActivitySnapshot,
233
237
  ): Promise<void> => {
234
238
  const goal = currentGoalSnapshot(ctx);
235
- if (goal?.status !== "active" || goal.goalId !== goalId || continuationQueuedFor === goal.goalId || foregroundRunning) {
239
+ if (!isPokeable(goal) || goal.goalId !== goalId || continuationQueuedFor === goal.goalId || foregroundRunning) {
236
240
  return;
237
241
  }
238
242
  const snapshot = await collectActivitySnapshot(ctx, providers.values(), foregroundRunning);
@@ -299,7 +303,7 @@ export default function (pi: ExtensionAPI): void {
299
303
  const goal = currentGoalSnapshot(ctx);
300
304
  const wakePlan = planBackgroundDrainWake(backgroundDrainTracker, goal, snapshot);
301
305
  backgroundDrainTracker = wakePlan.nextTracker;
302
- if (goal?.status === "active" && wakePlan.wakeSignature) {
306
+ if (isPokeable(goal) && wakePlan.wakeSignature) {
303
307
  const wakeSignature = wakePlan.wakeSignature;
304
308
  if (wakeSignature !== lastWakeSignature) {
305
309
  lastWakeSignature = wakeSignature;
@@ -539,6 +543,24 @@ export default function (pi: ExtensionAPI): void {
539
543
  },
540
544
  });
541
545
 
546
+ pi.registerShortcut("escape", {
547
+ description: "Pause the active goal (also interrupts a running agent turn)",
548
+ handler: (ctx) => {
549
+ const goal = getGoal(ctx);
550
+ const running = !ctx.isIdle();
551
+ if (isPokeable(goal)) {
552
+ setGoal(goalWithStatus(goal, "paused"), ctx, "shortcut");
553
+ if (ctx.hasUI) {
554
+ ctx.ui.notify("Goal paused.", "info");
555
+ }
556
+ }
557
+ if (running) {
558
+ // ESC keeps its built-in interrupt meaning while the agent is streaming.
559
+ ctx.abort();
560
+ }
561
+ },
562
+ });
563
+
542
564
  pi.registerCommand("better-activity", {
543
565
  description: "Show foreground/background activity known to pi-better-goal",
544
566
  handler: async (_args, ctx) => {
@@ -586,7 +608,7 @@ export default function (pi: ExtensionAPI): void {
586
608
  currentCtx = ctx;
587
609
  const goal = currentGoalSnapshot(ctx);
588
610
  const snapshot = await publishSnapshot(ctx);
589
- if (goal?.status !== "active") {
611
+ if (!isPokeable(goal)) {
590
612
  return;
591
613
  }
592
614
 
@@ -618,37 +640,38 @@ export default function (pi: ExtensionAPI): void {
618
640
  foregroundRunning = false;
619
641
  const snapshot = await publishSnapshot(ctx);
620
642
  const goal = getGoal(ctx);
621
- if (goal?.status === "active" && !snapshot.backgroundRunning) {
622
- const previous = currentContinuationState(ctx, goal.goalId) ?? createContinuationState(goal.goalId);
623
- const evidence = lastAgentEvidence ?? continuationEvidence([]);
624
- const repeated = previous.lastEvidenceSignature === evidence.signature;
625
- const noProgressRetries = repeated ? previous.noProgressRetries + 1 : 0;
626
- const blocked = repeated && noProgressRetries >= MAX_NO_PROGRESS_RETRIES;
627
- const now = Date.now();
628
- appendContinuationState({
629
- goalId: goal.goalId,
630
- lastEvidenceSignature: evidence.signature,
631
- lastEvidenceSummary: evidence.summary,
632
- ...(repeated
633
- ? (previous.lastProgressAt !== undefined ? { lastProgressAt: previous.lastProgressAt } : {})
634
- : { lastProgressAt: now }),
635
- noProgressRetries,
636
- blocked,
637
- updatedAt: Math.floor(now / 1000),
638
- });
639
- if (blocked) {
640
- refreshGoalWidget?.();
641
- if (ctx.hasUI) {
642
- ctx.ui.setStatus(EXTENSION_NAME, "waiting: no progress");
643
- ctx.ui.notify(
644
- `Goal automatic continuation is waiting after ${noProgressRetries} identical retries (${evidence.summary}). New input or background activity will resume it.`,
645
- "warning",
646
- );
647
- }
648
- return;
643
+ if (!isPokeable(goal) || snapshot.backgroundRunning) {
644
+ return;
645
+ }
646
+ const previous = currentContinuationState(ctx, goal.goalId) ?? createContinuationState(goal.goalId);
647
+ const evidence = lastAgentEvidence ?? continuationEvidence([]);
648
+ const repeated = previous.lastEvidenceSignature === evidence.signature;
649
+ const noProgressRetries = repeated ? previous.noProgressRetries + 1 : 0;
650
+ const blocked = repeated && noProgressRetries >= MAX_NO_PROGRESS_RETRIES;
651
+ const now = Date.now();
652
+ appendContinuationState({
653
+ goalId: goal.goalId,
654
+ lastEvidenceSignature: evidence.signature,
655
+ lastEvidenceSummary: evidence.summary,
656
+ ...(repeated
657
+ ? (previous.lastProgressAt !== undefined ? { lastProgressAt: previous.lastProgressAt } : {})
658
+ : { lastProgressAt: now }),
659
+ noProgressRetries,
660
+ blocked,
661
+ updatedAt: Math.floor(now / 1000),
662
+ });
663
+ if (blocked) {
664
+ refreshGoalWidget?.();
665
+ if (ctx.hasUI) {
666
+ ctx.ui.setStatus(EXTENSION_NAME, "waiting: no progress");
667
+ ctx.ui.notify(
668
+ `Goal automatic continuation is waiting after ${noProgressRetries} identical retries (${evidence.summary}). New input or background activity will resume it.`,
669
+ "warning",
670
+ );
649
671
  }
650
- scheduleIdleContinuation(goal, ctx, "continuation", snapshot);
672
+ return;
651
673
  }
674
+ scheduleIdleContinuation(goal, ctx, "continuation", snapshot);
652
675
  });
653
676
 
654
677
  pi.on("session_shutdown", async (_event, ctx) => {