@pinet/agent-goal 0.2.8 → 0.2.9

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
@@ -25,11 +25,11 @@ pi -e ./agent-goal/index.ts
25
25
  /goal resume Resume and immediately continue
26
26
  /goal complete Mark complete manually
27
27
  /goal clear Delete the goal
28
- /goal hide Hide the persistent goal dashboard
29
- /goal show Show the persistent goal dashboard
28
+ /goal hide Hide the compact persistent goal status
29
+ /goal show Show the compact persistent goal status
30
30
  ```
31
31
 
32
- Pi's footer shows the status and budget usage. A compact widget below the editor shows passive progress. In interactive mode, `/goal` opens a centered, keyboard-dismissable window with the objective, lifecycle state, budget bars, latest evaluator guidance, and continuation state. Press Escape, Enter, `q`, or Ctrl+C to close it. `/goal` remains a textual fallback in headless sessions.
32
+ Pi's footer is the only persistent goal UI and shows compact status and budget usage without duplicating the detailed dashboard. In interactive mode, `/goal` opens the detailed control window with the objective, lifecycle state, budget bars, latest evaluator guidance, and continuation state. Its visible keyboard actions pause or resume the goal, mark it complete, clear it, or close the window; complete and clear require a second keypress, and the window refreshes after state changes. Press Escape, `q`, or Ctrl+C to close it. `/goal` retains the detailed textual dashboard in headless sessions.
33
33
 
34
34
  Only one goal may exist per Pi session. Clear the existing goal before creating another.
35
35
 
@@ -1,13 +1,16 @@
1
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
2
  import { type Component } from "@earendil-works/pi-tui";
3
3
  import type { AgentGoal, GoalContinuationClaim } from "./domain.js";
4
+ export type GoalWindowAction = "pause" | "resume" | "complete" | "clear" | "close";
4
5
  export declare class GoalWindow implements Component {
5
6
  private readonly goal;
6
7
  private readonly claim;
7
8
  private readonly theme;
8
- private readonly onClose;
9
+ private readonly onAction;
10
+ private readonly requestRender;
9
11
  private readonly now;
10
- constructor(goal: AgentGoal | undefined, claim: GoalContinuationClaim | undefined, theme: Theme, onClose: () => void, now?: () => number);
12
+ private pendingConfirmation;
13
+ constructor(goal: AgentGoal | undefined, claim: GoalContinuationClaim | undefined, theme: Theme, onAction: (action: GoalWindowAction) => void, requestRender?: () => void, now?: () => number);
11
14
  handleInput(data: string): void;
12
15
  render(width: number): string[];
13
16
  invalidate(): void;
@@ -17,21 +17,60 @@ export class GoalWindow {
17
17
  goal;
18
18
  claim;
19
19
  theme;
20
- onClose;
20
+ onAction;
21
+ requestRender;
21
22
  now;
22
- constructor(goal, claim, theme, onClose, now = Date.now) {
23
+ pendingConfirmation;
24
+ constructor(goal, claim, theme, onAction, requestRender = () => undefined, now = Date.now) {
23
25
  this.goal = goal;
24
26
  this.claim = claim;
25
27
  this.theme = theme;
26
- this.onClose = onClose;
28
+ this.onAction = onAction;
29
+ this.requestRender = requestRender;
27
30
  this.now = now;
28
31
  }
29
32
  handleInput(data) {
30
- if (matchesKey(data, "escape") ||
31
- matchesKey(data, "ctrl+c") ||
32
- matchesKey(data, "return") ||
33
- data.toLowerCase() === "q") {
34
- this.onClose();
33
+ const key = data.toLowerCase();
34
+ if (matchesKey(data, "ctrl+c") || key === "q") {
35
+ this.onAction("close");
36
+ return;
37
+ }
38
+ if (matchesKey(data, "escape")) {
39
+ if (this.pendingConfirmation) {
40
+ this.pendingConfirmation = undefined;
41
+ this.requestRender();
42
+ }
43
+ else {
44
+ this.onAction("close");
45
+ }
46
+ return;
47
+ }
48
+ if (!this.goal)
49
+ return;
50
+ if (this.pendingConfirmation) {
51
+ const confirmationKey = this.pendingConfirmation === "complete" ? "c" : "x";
52
+ if (key === confirmationKey) {
53
+ this.onAction(this.pendingConfirmation);
54
+ }
55
+ else {
56
+ this.pendingConfirmation = undefined;
57
+ this.requestRender();
58
+ }
59
+ return;
60
+ }
61
+ if (key === "p" && this.goal.status === "active") {
62
+ this.onAction("pause");
63
+ }
64
+ else if (key === "r" && (this.goal.status === "paused" || this.goal.status === "blocked")) {
65
+ this.onAction("resume");
66
+ }
67
+ else if (key === "c" && this.goal.status !== "complete") {
68
+ this.pendingConfirmation = "complete";
69
+ this.requestRender();
70
+ }
71
+ else if (key === "x") {
72
+ this.pendingConfirmation = "clear";
73
+ this.requestRender();
35
74
  }
36
75
  }
37
76
  render(width) {
@@ -53,7 +92,7 @@ export class GoalWindow {
53
92
  if (!this.goal) {
54
93
  lines.push(row(), row(` ${this.theme.fg("muted", "No goal for this session.")}`));
55
94
  lines.push(row(` ${this.theme.fg("dim", "/goal <objective> to begin")}`), row());
56
- lines.push(row(` ${this.theme.fg("dim", "esc · enter · q close")}`));
95
+ lines.push(row(` ${this.theme.fg("dim", "esc · q close")}`));
57
96
  lines.push(border(`╰${"─".repeat(innerWidth)}╯`));
58
97
  return lines;
59
98
  }
@@ -93,7 +132,17 @@ export class GoalWindow {
93
132
  if (this.claim) {
94
133
  lines.push(row(` ${this.theme.fg("muted", "Continuation")} ${this.claim.state} · attempt ${this.claim.attempt}`));
95
134
  }
96
- lines.push(row(), row(` ${this.theme.fg("dim", "esc · enter · q close")}`));
135
+ const actions = [
136
+ this.goal.status === "active" ? "p pause" : undefined,
137
+ this.goal.status === "paused" || this.goal.status === "blocked" ? "r resume" : undefined,
138
+ this.goal.status !== "complete" ? "c complete" : undefined,
139
+ "x clear",
140
+ "q close",
141
+ ].filter((action) => action !== undefined);
142
+ const footer = this.pendingConfirmation
143
+ ? `${this.pendingConfirmation === "complete" ? "c" : "x"} again to confirm ${this.pendingConfirmation} · esc cancel`
144
+ : actions.join(" · ");
145
+ lines.push(row(), row(` ${this.theme.fg("dim", footer)}`));
97
146
  lines.push(border(`╰${"─".repeat(innerWidth)}╯`));
98
147
  return lines;
99
148
  }
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import type { GoalBudget, GoalContinuation, GoalEvaluator, GoalEventSink, GoalRetryPolicy, GoalStorage, GoalWakeScheduler } from "./domain.js";
3
3
  export type { AgentGoal, GoalBudget, GoalContinuation, GoalContinuationClaim, GoalContinuationRequest, GoalContinuationResult, GoalEvaluation, GoalEvaluationRecord, GoalEvaluator, GoalEvent, GoalEventSink, GoalPendingEvaluation, GoalProgress, GoalRetryPolicy, GoalStatus, GoalStorage, GoalTerminalCandidate, GoalTerminalCandidateRecord, GoalUsage, GoalWakeScheduler, } from "./domain.js";
4
4
  export { displayGoalText, formatGoalDashboard, formatGoalStatus } from "./dashboard.js";
5
- export { GoalWindow } from "./goal-window.js";
5
+ export { GoalWindow, type GoalWindowAction } from "./goal-window.js";
6
6
  export { MemoryGoalStorage } from "./memory-storage.js";
7
7
  export { parseGoalEvaluation, PiGoalEvaluator } from "./pi-evaluator.js";
8
8
  export { countGoalProgressTokens, formatGoalProgress, type GoalProgressMessage, } from "./progress.js";
package/dist/index.js CHANGED
@@ -73,13 +73,27 @@ export function registerAgentGoal(pi, options = {}) {
73
73
  const refreshUi = async (ctx) => {
74
74
  const scopeId = ctx.sessionManager.getSessionId();
75
75
  const goal = await runtime.get(scopeId);
76
- ctx.ui.setStatus(STATUS_KEY, goal ? formatGoalStatus(goal) : undefined);
77
- if (!goal || hiddenScopes.has(scopeId)) {
78
- ctx.ui.setWidget(WIDGET_KEY, undefined);
79
- return;
76
+ const hidden = hiddenScopes.has(scopeId);
77
+ ctx.ui.setStatus(STATUS_KEY, goal && !hidden ? formatGoalStatus(goal) : undefined);
78
+ ctx.ui.setWidget(WIDGET_KEY, undefined);
79
+ };
80
+ const applyGoalAction = async (scopeId, action) => {
81
+ switch (action) {
82
+ case "pause":
83
+ await runtime.setStatus(scopeId, "paused");
84
+ break;
85
+ case "resume":
86
+ await runtime.setStatus(scopeId, "active");
87
+ await runtime.start(scopeId, "Resume the goal from current state.");
88
+ break;
89
+ case "complete":
90
+ await runtime.setStatus(scopeId, "complete");
91
+ break;
92
+ case "clear":
93
+ if (!(await runtime.clear(scopeId)))
94
+ throw new Error("This session has no goal");
95
+ break;
80
96
  }
81
- const claim = await runtime.getContinuationClaim(scopeId);
82
- ctx.ui.setWidget(WIDGET_KEY, formatGoalDashboard(goal, claim), { placement: "belowEditor" });
83
97
  };
84
98
  pi.on("session_start", async (_event, rawCtx) => {
85
99
  const ctx = rawCtx;
@@ -277,47 +291,46 @@ export function registerAgentGoal(pi, options = {}) {
277
291
  const input = args.trim();
278
292
  try {
279
293
  if (!input) {
280
- const goal = await runtime.get(scopeId);
281
- const claim = await runtime.getContinuationClaim(scopeId);
282
294
  let openedWindow = false;
283
- await ctx.ui.custom((_tui, theme, _keybindings, done) => {
284
- openedWindow = true;
285
- return new GoalWindow(goal, claim, theme, () => done(undefined));
286
- }, {
287
- overlay: true,
288
- overlayOptions: {
289
- anchor: "center",
290
- width: 66,
291
- minWidth: 36,
292
- maxHeight: "80%",
293
- margin: 1,
294
- },
295
- });
296
- if (openedWindow)
297
- return;
298
- api.sendMessage({
299
- customType: "agent-goal.status",
300
- content: goal
301
- ? formatGoalDashboard(goal, claim).join("\n")
302
- : "This session has no goal.",
303
- display: true,
304
- }, { triggerTurn: false });
305
- return;
295
+ while (true) {
296
+ const goal = await runtime.get(scopeId);
297
+ const claim = await runtime.getContinuationClaim(scopeId);
298
+ const action = await ctx.ui.custom((tui, theme, _keybindings, done) => {
299
+ openedWindow = true;
300
+ return new GoalWindow(goal, claim, theme, done, () => tui.requestRender());
301
+ }, {
302
+ overlay: true,
303
+ overlayOptions: {
304
+ anchor: "center",
305
+ width: 66,
306
+ minWidth: 36,
307
+ maxHeight: "80%",
308
+ margin: 1,
309
+ },
310
+ });
311
+ if (!openedWindow) {
312
+ api.sendMessage({
313
+ customType: "agent-goal.status",
314
+ content: goal
315
+ ? formatGoalDashboard(goal, claim).join("\n")
316
+ : "This session has no goal.",
317
+ display: true,
318
+ }, { triggerTurn: false });
319
+ return;
320
+ }
321
+ if (!action || action === "close")
322
+ return;
323
+ await applyGoalAction(scopeId, action);
324
+ await refreshUi(ctx);
325
+ }
306
326
  }
307
- switch (input.toLowerCase()) {
327
+ const command = input.toLowerCase();
328
+ switch (command) {
308
329
  case "pause":
309
- await runtime.setStatus(scopeId, "paused");
310
- break;
311
330
  case "resume":
312
- await runtime.setStatus(scopeId, "active");
313
- await runtime.start(scopeId, "Resume the goal from current state.");
314
- break;
315
331
  case "complete":
316
- await runtime.setStatus(scopeId, "complete");
317
- break;
318
332
  case "clear":
319
- if (!(await runtime.clear(scopeId)))
320
- throw new Error("This session has no goal");
333
+ await applyGoalAction(scopeId, command);
321
334
  break;
322
335
  case "hide":
323
336
  hiddenScopes.add(scopeId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pinet/agent-goal",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "type": "module",
5
5
  "description": "Standalone single-agent durable goal loop for Pi",
6
6
  "author": "Will Porcellini <5994936+gugu91@users.noreply.github.com>",