@pinet/agent-goal 0.2.13 → 0.2.14

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.
@@ -1,18 +1,5 @@
1
1
  import { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
2
- import { displayGoalText } from "./dashboard.js";
3
- function progressBar(value, maximum, width) {
4
- const ratio = Math.min(1, Math.max(0, value / maximum));
5
- const filled = Math.round(ratio * width);
6
- return `${"━".repeat(filled)}${"─".repeat(width - filled)}`;
7
- }
8
- function compactNumber(value) {
9
- if (value < 1_000)
10
- return String(value);
11
- const divisor = value < 1_000_000 ? 1_000 : 1_000_000;
12
- const suffix = value < 1_000_000 ? "k" : "m";
13
- const scaled = value / divisor;
14
- return `${scaled.toFixed(scaled < 100 ? 1 : 0).replace(/\.0$/, "")}${suffix}`;
15
- }
2
+ import { displayGoalText, formatElapsed, goalDisplayName } from "./dashboard.js";
16
3
  export class GoalWindow {
17
4
  goal;
18
5
  claim;
@@ -21,13 +8,22 @@ export class GoalWindow {
21
8
  requestRender;
22
9
  now;
23
10
  actionError;
24
- pendingConfirmation;
25
- budgetField;
11
+ checkpoints;
12
+ mode = "details";
13
+ textField = "name";
14
+ name = "";
15
+ objective = "";
16
+ initialName = "";
17
+ initialObjective = "";
18
+ budgetField = "turns";
26
19
  budgetTurns = "";
27
- budgetTokens = "";
28
- replaceBudgetValue = false;
29
- budgetInputError;
30
- constructor(goal, claim, theme, onAction, requestRender = () => undefined, now = Date.now, actionError) {
20
+ budgetRuntime = "";
21
+ snoozeDuration = "30m";
22
+ inputError;
23
+ confirmClose = false;
24
+ showAllCheckpoints = false;
25
+ checkpointOffset = 0;
26
+ constructor(goal, claim, theme, onAction, requestRender = () => undefined, now = Date.now, actionError, checkpoints = []) {
31
27
  this.goal = goal;
32
28
  this.claim = claim;
33
29
  this.theme = theme;
@@ -35,191 +31,343 @@ export class GoalWindow {
35
31
  this.requestRender = requestRender;
36
32
  this.now = now;
37
33
  this.actionError = actionError;
34
+ this.checkpoints = checkpoints;
38
35
  }
39
36
  handleInput(data) {
40
37
  const key = data.toLowerCase();
41
- if (matchesKey(data, "ctrl+c") || key === "q") {
38
+ if (matchesKey(data, "ctrl+c") || (this.mode === "details" && key === "q")) {
42
39
  this.onAction("close");
43
40
  return;
44
41
  }
45
42
  if (matchesKey(data, "escape")) {
46
- if (this.pendingConfirmation) {
47
- this.pendingConfirmation = undefined;
43
+ if (this.mode !== "details") {
44
+ this.mode = "details";
45
+ this.inputError = undefined;
48
46
  this.requestRender();
49
47
  }
50
- else if (this.budgetField) {
51
- this.budgetField = undefined;
52
- this.budgetInputError = undefined;
48
+ else if (this.confirmClose) {
49
+ this.confirmClose = false;
53
50
  this.requestRender();
54
51
  }
55
- else {
52
+ else
56
53
  this.onAction("close");
57
- }
58
54
  return;
59
55
  }
60
- if (!this.goal)
56
+ if (this.mode === "create" || this.mode === "edit") {
57
+ this.handleTextForm(data);
61
58
  return;
62
- if (this.budgetField) {
63
- if (matchesKey(data, "tab") || matchesKey(data, "up") || matchesKey(data, "down")) {
64
- this.budgetField = this.budgetField === "turns" ? "tokens" : "turns";
65
- this.replaceBudgetValue = true;
66
- this.budgetInputError = undefined;
67
- }
68
- else if (matchesKey(data, "backspace")) {
69
- const value = this.budgetField === "turns" ? this.budgetTurns : this.budgetTokens;
70
- if (this.budgetField === "turns")
71
- this.budgetTurns = value.slice(0, -1);
72
- else
73
- this.budgetTokens = value.slice(0, -1);
74
- this.replaceBudgetValue = false;
75
- this.budgetInputError = undefined;
76
- }
77
- else if (matchesKey(data, "enter")) {
78
- const maxIterations = Number(this.budgetTurns);
79
- if (!Number.isInteger(maxIterations) || maxIterations <= 0) {
80
- this.budgetInputError = "Turns must be a positive integer";
81
- }
82
- else {
83
- const maxTokens = this.budgetTokens ? Number(this.budgetTokens) : undefined;
84
- this.onAction({ type: "budget", maxIterations, maxTokens });
85
- return;
86
- }
87
- }
88
- else if (/^\d+$/.test(data)) {
89
- const value = this.replaceBudgetValue
90
- ? data
91
- : `${this.budgetField === "turns" ? this.budgetTurns : this.budgetTokens}${data}`;
92
- if (this.budgetField === "turns")
93
- this.budgetTurns = value;
94
- else
95
- this.budgetTokens = value;
96
- this.replaceBudgetValue = false;
97
- this.budgetInputError = undefined;
98
- }
99
- else {
100
- return;
101
- }
59
+ }
60
+ if (this.mode === "budget") {
61
+ this.handleBudgetForm(data);
62
+ return;
63
+ }
64
+ if (this.mode === "snooze") {
65
+ this.handleSnoozeForm(data);
66
+ return;
67
+ }
68
+ if (!this.goal) {
69
+ if (key === "n" || matchesKey(data, "enter"))
70
+ this.openTextForm("create");
71
+ return;
72
+ }
73
+ if (this.showAllCheckpoints && matchesKey(data, "down")) {
74
+ this.checkpointOffset = Math.min(Math.max(0, this.checkpoints.length - 1), this.checkpointOffset + 1);
102
75
  this.requestRender();
103
76
  return;
104
77
  }
105
- if (this.pendingConfirmation) {
106
- const confirmationKey = this.pendingConfirmation === "complete" ? "c" : "x";
107
- if (key === confirmationKey) {
108
- this.onAction(this.pendingConfirmation);
109
- }
78
+ if (this.showAllCheckpoints && matchesKey(data, "up")) {
79
+ this.checkpointOffset = Math.max(0, this.checkpointOffset - 1);
80
+ this.requestRender();
81
+ return;
82
+ }
83
+ if (this.confirmClose) {
84
+ if (key === "x")
85
+ this.onAction("closeGoal");
110
86
  else {
111
- this.pendingConfirmation = undefined;
87
+ this.confirmClose = false;
112
88
  this.requestRender();
113
89
  }
114
90
  return;
115
91
  }
116
- if (key === "b" && this.goal.status !== "complete") {
117
- this.budgetField = "turns";
118
- this.budgetTurns = String(this.goal.budget.maxIterations);
119
- this.budgetTokens =
120
- this.goal.budget.maxTokens === undefined ? "" : String(this.goal.budget.maxTokens);
121
- this.replaceBudgetValue = true;
122
- this.budgetInputError = undefined;
92
+ if (key === "e")
93
+ this.openTextForm("edit");
94
+ else if (key === "b" && this.goal.status !== "complete")
95
+ this.openBudgetForm();
96
+ else if (key === "s" && this.goal.status !== "complete") {
97
+ this.mode = "snooze";
98
+ this.inputError = undefined;
99
+ this.requestRender();
100
+ }
101
+ else if (key === "h" && this.checkpoints.length > 3) {
102
+ this.showAllCheckpoints = !this.showAllCheckpoints;
103
+ this.checkpointOffset = 0;
104
+ this.requestRender();
105
+ }
106
+ else if (key === "x") {
107
+ this.confirmClose = true;
123
108
  this.requestRender();
124
109
  }
125
- else if (key === "p" && this.goal.status === "active") {
126
- this.onAction("pause");
110
+ }
111
+ openTextForm(mode) {
112
+ this.mode = mode;
113
+ this.textField = "name";
114
+ this.name = mode === "edit" && this.goal ? (this.goal.name ?? this.goal.objective) : "";
115
+ this.objective = mode === "edit" && this.goal ? this.goal.objective : "";
116
+ this.initialName = this.name;
117
+ this.initialObjective = this.objective;
118
+ if (mode === "create") {
119
+ this.budgetTurns = "";
120
+ this.budgetRuntime = "";
121
+ }
122
+ this.inputError = undefined;
123
+ this.requestRender();
124
+ }
125
+ handleTextForm(data) {
126
+ if (matchesKey(data, "tab") || matchesKey(data, "down")) {
127
+ const fields = this.mode === "create" ? ["name", "objective", "turns", "runtime"] : ["name", "objective"];
128
+ this.textField = fields[(fields.indexOf(this.textField) + 1) % fields.length];
127
129
  }
128
- else if (key === "r" && (this.goal.status === "paused" || this.goal.status === "blocked")) {
129
- this.onAction("resume");
130
+ else if (matchesKey(data, "up")) {
131
+ const fields = this.mode === "create" ? ["name", "objective", "turns", "runtime"] : ["name", "objective"];
132
+ this.textField =
133
+ fields[(fields.indexOf(this.textField) + fields.length - 1) % fields.length];
130
134
  }
131
- else if (key === "c" && this.goal.status !== "complete") {
132
- this.pendingConfirmation = "complete";
135
+ else if (matchesKey(data, "backspace")) {
136
+ if (this.textField === "name")
137
+ this.name = this.name.slice(0, -1);
138
+ else if (this.textField === "objective")
139
+ this.objective = this.objective.slice(0, -1);
140
+ else if (this.textField === "turns")
141
+ this.budgetTurns = this.budgetTurns.slice(0, -1);
142
+ else
143
+ this.budgetRuntime = this.budgetRuntime.slice(0, -1);
144
+ }
145
+ else if (matchesKey(data, "enter")) {
146
+ const name = this.name.trim();
147
+ const objective = this.objective.trim();
148
+ const maxIterations = this.budgetTurns ? Number(this.budgetTurns) : undefined;
149
+ const maxRuntimeMs = this.budgetRuntime ? parseDuration(this.budgetRuntime) : undefined;
150
+ if (!name || !objective)
151
+ this.inputError = "Name and objective are required";
152
+ else if (maxIterations !== undefined &&
153
+ (!Number.isInteger(maxIterations) || maxIterations <= 0))
154
+ this.inputError = "Turns must be a positive integer";
155
+ else if (this.budgetRuntime && maxRuntimeMs === undefined)
156
+ this.inputError = "Runtime must use m, h, or d";
157
+ else if (this.mode === "create")
158
+ this.onAction({ type: "create", name, objective, maxIterations, maxRuntimeMs });
159
+ else {
160
+ const update = {
161
+ ...(this.name === this.initialName ? {} : { name }),
162
+ ...(this.objective === this.initialObjective ? {} : { objective }),
163
+ };
164
+ if (update.name === undefined && update.objective === undefined)
165
+ this.inputError = "Change the name or objective before saving";
166
+ else
167
+ this.onAction({ type: "edit", ...update });
168
+ }
133
169
  this.requestRender();
170
+ return;
134
171
  }
135
- else if (key === "x") {
136
- this.pendingConfirmation = "clear";
172
+ else if (data.length > 0 &&
173
+ Array.from(data).every((character) => {
174
+ const code = character.codePointAt(0) ?? 0;
175
+ return code > 31 && code !== 127;
176
+ })) {
177
+ if (this.textField === "name")
178
+ this.name += data;
179
+ else if (this.textField === "objective")
180
+ this.objective += data;
181
+ else if (this.textField === "turns" && /^\d+$/.test(data))
182
+ this.budgetTurns += data;
183
+ else if (this.textField === "runtime" && /^[0-9mhd]+$/i.test(data))
184
+ this.budgetRuntime += data;
185
+ else
186
+ return;
187
+ }
188
+ else
189
+ return;
190
+ this.inputError = undefined;
191
+ this.requestRender();
192
+ }
193
+ openBudgetForm() {
194
+ this.mode = "budget";
195
+ this.budgetField = "turns";
196
+ this.budgetTurns = this.goal?.budget.maxIterations?.toString() ?? "";
197
+ this.budgetRuntime = this.goal?.budget.maxRuntimeMs
198
+ ? `${Math.round(this.goal.budget.maxRuntimeMs / 60_000)}m`
199
+ : "";
200
+ this.inputError = undefined;
201
+ this.requestRender();
202
+ }
203
+ handleBudgetForm(data) {
204
+ if (data.toLowerCase() === "o") {
205
+ this.onAction({ type: "budget", disabled: true });
206
+ return;
207
+ }
208
+ if (matchesKey(data, "tab") || matchesKey(data, "up") || matchesKey(data, "down")) {
209
+ this.budgetField = this.budgetField === "turns" ? "runtime" : "turns";
210
+ }
211
+ else if (matchesKey(data, "backspace")) {
212
+ if (this.budgetField === "turns")
213
+ this.budgetTurns = this.budgetTurns.slice(0, -1);
214
+ else
215
+ this.budgetRuntime = this.budgetRuntime.slice(0, -1);
216
+ }
217
+ else if (matchesKey(data, "enter")) {
218
+ const maxIterations = this.budgetTurns ? Number(this.budgetTurns) : undefined;
219
+ const maxRuntimeMs = this.budgetRuntime ? parseDuration(this.budgetRuntime) : undefined;
220
+ if (maxIterations !== undefined && (!Number.isInteger(maxIterations) || maxIterations <= 0))
221
+ this.inputError = "Turns must be a positive integer";
222
+ else if (this.budgetRuntime && maxRuntimeMs === undefined)
223
+ this.inputError = "Runtime must use m, h, or d (for example 2h)";
224
+ else if (maxIterations === undefined && maxRuntimeMs === undefined)
225
+ this.inputError = "Set a limit or press o to turn limits off";
226
+ else
227
+ this.onAction({ type: "budget", maxIterations, maxRuntimeMs });
228
+ this.requestRender();
229
+ return;
230
+ }
231
+ else if (/^[0-9mhd]+$/i.test(data)) {
232
+ if (this.budgetField === "turns" && /^\d+$/.test(data))
233
+ this.budgetTurns += data;
234
+ else if (this.budgetField === "runtime")
235
+ this.budgetRuntime += data;
236
+ else
237
+ return;
238
+ }
239
+ else
240
+ return;
241
+ this.inputError = undefined;
242
+ this.requestRender();
243
+ }
244
+ handleSnoozeForm(data) {
245
+ if (matchesKey(data, "backspace"))
246
+ this.snoozeDuration = this.snoozeDuration.slice(0, -1);
247
+ else if (matchesKey(data, "enter")) {
248
+ const durationMs = parseDuration(this.snoozeDuration);
249
+ if (durationMs === undefined)
250
+ this.inputError = "Duration must use m, h, or d";
251
+ else
252
+ this.onAction({ type: "snooze", durationMs });
137
253
  this.requestRender();
254
+ return;
138
255
  }
256
+ else if (/^[0-9mhd]+$/i.test(data))
257
+ this.snoozeDuration += data;
258
+ else
259
+ return;
260
+ this.inputError = undefined;
261
+ this.requestRender();
139
262
  }
140
263
  render(width) {
141
264
  if (width < 8)
142
265
  return [truncateToWidth("Goal", Math.max(0, width), "")];
143
266
  const innerWidth = width - 2;
144
267
  const contentWidth = Math.max(1, innerWidth - 2);
145
- const borderColor = this.goal?.status === "complete" ? "success" : "borderAccent";
146
- const border = (text) => this.theme.fg(borderColor, text);
268
+ const border = (text) => this.theme.fg("borderAccent", text);
147
269
  const row = (content = "") => {
148
270
  const truncated = truncateToWidth(content, innerWidth, "", true);
149
271
  return `${border("│")}${truncated}${" ".repeat(Math.max(0, innerWidth - visibleWidth(truncated)))}${border("│")}`;
150
272
  };
151
- const title = this.theme.fg("accent", this.theme.bold(" Goal "));
152
- const titleWidth = visibleWidth(title);
273
+ const title = this.theme.fg("accent", this.theme.bold(` Goal${this.mode === "details" ? "" : ` · ${this.mode}`} `));
153
274
  const lines = [
154
- `${border("╭")}${title}${border(`${"─".repeat(Math.max(0, innerWidth - titleWidth))}╮`)}`,
275
+ `${border("╭")}${title}${border(`${"─".repeat(Math.max(0, innerWidth - visibleWidth(title)))}╮`)}`,
155
276
  ];
156
- if (!this.goal) {
277
+ if (!this.goal && this.mode === "details") {
157
278
  lines.push(row(), row(` ${this.theme.fg("muted", "No goal for this session.")}`));
158
- lines.push(row(` ${this.theme.fg("dim", "/goal <objective> to begin")}`), row());
159
- lines.push(row(` ${this.theme.fg("dim", "esc · q close")}`));
279
+ lines.push(row(` ${this.theme.fg("dim", "n · enter create · q close")}`));
160
280
  lines.push(border(`╰${"─".repeat(innerWidth)}╯`));
161
281
  return lines;
162
282
  }
163
- const statusColor = this.goal.status === "complete"
164
- ? "success"
165
- : this.goal.status === "blocked"
166
- ? "error"
167
- : this.goal.status === "active"
168
- ? "accent"
169
- : "warning";
170
- lines.push(row(` ${this.theme.fg(statusColor, `● ${this.goal.status.toUpperCase()}`)}`));
171
- const objectiveLines = wrapTextWithAnsi(displayGoalText(this.goal.objective, 500), contentWidth).slice(0, 3);
172
- for (const objectiveLine of objectiveLines)
173
- lines.push(row(` ${objectiveLine}`));
174
- lines.push(row());
175
- const turnBar = progressBar(this.goal.usage.iterations, this.goal.budget.maxIterations, Math.min(12, Math.max(4, contentWidth - 23)));
176
- lines.push(row(` Turns ${this.theme.fg("accent", turnBar)} ${this.goal.usage.iterations}/${this.goal.budget.maxIterations}`));
177
- if (this.goal.budget.maxTokens === undefined) {
178
- lines.push(row(` Tokens ${compactNumber(this.goal.usage.tokens)}`));
179
- }
180
- else {
181
- const tokenBar = progressBar(this.goal.usage.tokens, this.goal.budget.maxTokens, Math.min(12, Math.max(4, contentWidth - 23)));
182
- lines.push(row(` Tokens ${this.theme.fg("accent", tokenBar)} ${compactNumber(this.goal.usage.tokens)}/${compactNumber(this.goal.budget.maxTokens)}`));
183
- }
184
- if (this.goal.budget.maxRuntimeMs !== undefined) {
185
- const end = this.goal.status === "active" ? this.now() : Date.parse(this.goal.updatedAt);
186
- const elapsed = Math.max(0, end - Date.parse(this.goal.createdAt));
187
- const runtimeBar = progressBar(elapsed, this.goal.budget.maxRuntimeMs, Math.min(12, Math.max(4, contentWidth - 23)));
188
- lines.push(row(` Time ${this.theme.fg("accent", runtimeBar)} ${Math.floor(elapsed / 60_000)}m/${Math.ceil(this.goal.budget.maxRuntimeMs / 60_000)}m`));
189
- }
190
- if (this.goal.lastEvaluation) {
191
- lines.push(row(), row(` ${this.theme.fg("muted", "Latest")} ${displayGoalText(this.goal.lastEvaluation.reason, Math.max(20, contentWidth - 8))}`));
283
+ if (this.mode === "create" || this.mode === "edit") {
284
+ const displayedName = displayGoalText(this.name, 500);
285
+ const displayedObjective = displayGoalText(this.objective, 500);
286
+ lines.push(row(` ${this.textField === "name" ? "" : " "} Name ${displayedName || "_"}`), row(` ${this.textField === "objective" ? "›" : " "} Objective ${displayedObjective || "_"}`), ...(this.mode === "create"
287
+ ? [
288
+ row(` ${this.textField === "turns" ? "" : " "} Turns ${this.budgetTurns || "off"}`),
289
+ row(` ${this.textField === "runtime" ? "›" : " "} Runtime ${this.budgetRuntime || "off"}`),
290
+ ]
291
+ : []), row(), row(` ${this.theme.fg("dim", "tab field · enter save · esc cancel")}`));
292
+ if (this.mode === "edit")
293
+ lines.push(row(` ${this.theme.fg("warning", "Objective applies on the next continuation.")}`));
294
+ this.finish(lines, row, border, innerWidth);
295
+ return lines;
192
296
  }
193
- else if (this.goal.blockedReason) {
194
- lines.push(row(), row(` ${this.theme.fg("muted", "Reason")} ${displayGoalText(this.goal.blockedReason, Math.max(20, contentWidth - 8))}`));
297
+ if (this.mode === "budget") {
298
+ lines.push(row(` › Limits are opt-in and stop continuation, not in-flight work.`), row(` ${this.budgetField === "turns" ? "›" : " "} Turns ${this.budgetTurns || "off"}`), row(` ${this.budgetField === "runtime" ? "›" : " "} Runtime ${this.budgetRuntime || "off"}`), row(), row(` ${this.theme.fg("dim", "tab field · enter save · o off · esc cancel")}`));
299
+ this.finish(lines, row, border, innerWidth);
300
+ return lines;
195
301
  }
196
- if (this.claim) {
197
- lines.push(row(` ${this.theme.fg("muted", "Continuation")} ${this.claim.state} · attempt ${this.claim.attempt}`));
302
+ if (this.mode === "snooze") {
303
+ lines.push(row(` Snooze for ${this.snoozeDuration || "_"}`), row(` ${this.theme.fg("dim", "Automatically continues when due · enter save · esc cancel")}`));
304
+ this.finish(lines, row, border, innerWidth);
305
+ return lines;
198
306
  }
199
- if (this.budgetField) {
200
- const turns = `${this.budgetField === "turns" ? "›" : " "} Turns ${this.budgetTurns || "_"}`;
201
- const tokens = `${this.budgetField === "tokens" ? "›" : " "} Tokens ${this.budgetTokens || "unchanged"}`;
202
- lines.push(row(), row(` ${this.theme.fg("accent", "Edit budget")}`), row(` ${turns}`), row(` ${tokens}`));
307
+ const goal = this.goal;
308
+ const status = goal.snoozedUntil
309
+ ? `SNOOZED UNTIL ${goal.snoozedUntil}`
310
+ : goal.status.toUpperCase();
311
+ lines.push(row(` ${this.theme.fg(goal.status === "complete" ? "success" : "accent", `● ${status}`)}`));
312
+ lines.push(row(` ${this.theme.bold(goalDisplayName(goal))}`));
313
+ for (const objectiveLine of wrapTextWithAnsi(displayGoalText(goal.objective, 500), contentWidth).slice(0, 3))
314
+ lines.push(row(` ${objectiveLine}`));
315
+ const elapsedEnd = goal.status === "active" ? this.now() : Date.parse(goal.updatedAt);
316
+ lines.push(row(` ${this.theme.fg("muted", `Elapsed ${formatElapsed(elapsedEnd - Date.parse(goal.createdAt))}`)}`));
317
+ const limits = [
318
+ goal.budget.maxIterations === undefined
319
+ ? undefined
320
+ : `${goal.usage.iterations}/${goal.budget.maxIterations} turns`,
321
+ goal.budget.maxRuntimeMs === undefined
322
+ ? undefined
323
+ : `${Math.round(goal.budget.maxRuntimeMs / 60_000)}m runtime`,
324
+ ].filter((value) => value !== undefined);
325
+ lines.push(row(` ${this.theme.fg("muted", `Limits ${limits.length ? limits.join(" · ") : "off"}`)}`));
326
+ if (this.claim)
327
+ lines.push(row(` ${this.theme.fg("muted", `Continuation ${this.claim.state}`)}`));
328
+ if (this.checkpoints.length) {
329
+ lines.push(row(), row(` ${this.theme.fg("accent", "Checkpoints · agent-reported")}`));
330
+ const shown = this.showAllCheckpoints
331
+ ? this.checkpoints.slice(this.checkpointOffset, this.checkpointOffset + 3)
332
+ : this.checkpoints.slice(0, 3);
333
+ for (const checkpoint of shown) {
334
+ lines.push(row(` ${checkpoint.createdAt.slice(11, 16)} · ${displayGoalText(checkpoint.summary, contentWidth - 10)}`));
335
+ if (this.showAllCheckpoints && checkpoint.evidence)
336
+ lines.push(row(` evidence · ${displayGoalText(checkpoint.evidence, contentWidth - 14)}`));
337
+ if (this.showAllCheckpoints && (checkpoint.blocker || checkpoint.nextStep))
338
+ lines.push(row(` ${checkpoint.blocker ? "blocker" : "next"} · ${displayGoalText(checkpoint.blocker ?? checkpoint.nextStep ?? "", contentWidth - 11)}`));
339
+ }
340
+ if (!this.showAllCheckpoints && this.checkpoints.length > 3)
341
+ lines.push(row(` ${this.theme.fg("dim", `… and ${this.checkpoints.length - 3} more · h show all`)}`));
342
+ else if (this.showAllCheckpoints && this.checkpoints.length > 3)
343
+ lines.push(row(` ${this.theme.fg("dim", `history ${this.checkpointOffset + 1}-${Math.min(this.checkpointOffset + shown.length, this.checkpoints.length)}/${this.checkpoints.length} · ↑↓ scroll · h newest 3`)}`));
203
344
  }
204
- const error = this.budgetInputError ?? this.actionError;
205
- if (error)
206
- lines.push(row(` ${this.theme.fg("error", displayGoalText(error, contentWidth))}`));
207
- const actions = [
208
- this.goal.status === "active" ? "p pause" : undefined,
209
- this.goal.status === "paused" || this.goal.status === "blocked" ? "r resume" : undefined,
210
- this.goal.status !== "complete" ? "b budget" : undefined,
211
- this.goal.status !== "complete" ? "c complete" : undefined,
212
- "x clear",
213
- "q close",
214
- ].filter((action) => action !== undefined);
215
- const footer = this.pendingConfirmation
216
- ? `${this.pendingConfirmation === "complete" ? "c" : "x"} again to confirm ${this.pendingConfirmation} · esc cancel`
217
- : this.budgetField
218
- ? "digits edit · tab field · enter save · esc cancel"
219
- : actions.join(" · ");
345
+ const footer = this.confirmClose
346
+ ? "x again to close goal · esc cancel"
347
+ : goal.status === "complete"
348
+ ? "x close goal · q close overlay"
349
+ : "e edit · b limits · s snooze · x close goal · q overlay";
220
350
  lines.push(row(), row(` ${this.theme.fg("dim", footer)}`));
221
- lines.push(border(`╰${"─".repeat(innerWidth)}╯`));
351
+ this.finish(lines, row, border, innerWidth);
222
352
  return lines;
223
353
  }
354
+ finish(lines, row, border, innerWidth) {
355
+ const error = this.inputError ?? this.actionError;
356
+ if (error)
357
+ lines.push(row(` ${this.theme.fg("error", displayGoalText(error, innerWidth - 2))}`));
358
+ lines.push(border(`╰${"─".repeat(innerWidth)}╯`));
359
+ }
224
360
  invalidate() { }
225
361
  }
362
+ export function parseDuration(value) {
363
+ const match = value
364
+ .trim()
365
+ .toLowerCase()
366
+ .match(/^(\d+)(m|h|d)$/);
367
+ if (!match)
368
+ return undefined;
369
+ const amount = Number(match[1]);
370
+ if (!Number.isSafeInteger(amount) || amount <= 0)
371
+ return undefined;
372
+ return amount * (match[2] === "m" ? 60_000 : match[2] === "h" ? 3_600_000 : 86_400_000);
373
+ }
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, type GoalWindowAction, type GoalWindowLifecycleAction, } from "./goal-window.js";
5
+ export { GoalWindow, parseDuration, 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";