@pinet/agent-goal 0.2.13 → 0.2.15

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