killeros 2.0.5 → 2.0.7

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.
@@ -5,10 +5,12 @@ export type InitOutcome =
5
5
  | { kind: "pending" }
6
6
  | { kind: "written" }
7
7
  | { kind: "policy-conflict"; reason: string }
8
+ | { kind: "cancelled" }
8
9
  | { kind: "no-outcome" };
9
10
 
10
11
  export interface InitRuntime {
11
12
  active: boolean;
13
+ starting?: symbol;
12
14
  targetPath?: string;
13
15
  projectRoot?: string;
14
16
  activeTools?: string[];
@@ -26,6 +28,11 @@ export interface GoalBlockerAudit {
26
28
  lastTurn: number;
27
29
  }
28
30
 
31
+ export interface GoalFileVerification {
32
+ kind: "file";
33
+ path: string;
34
+ }
35
+
29
36
  export interface GoalState {
30
37
  version: 1;
31
38
  revision: number;
@@ -41,6 +48,7 @@ export interface GoalState {
41
48
  result?: string;
42
49
  resumeAfterManualCompaction?: true;
43
50
  blockerAudit?: GoalBlockerAudit;
51
+ verification?: GoalFileVerification;
44
52
  }
45
53
 
46
54
  export interface GoalRuntime {
@@ -71,6 +79,7 @@ export function createGoalRuntime(): GoalRuntime {
71
79
 
72
80
  export function resetInitRuntime(state: InitRuntime): void {
73
81
  state.active = false;
82
+ state.starting = undefined;
74
83
  state.targetPath = undefined;
75
84
  state.projectRoot = undefined;
76
85
  state.activeTools = undefined;
@@ -2,7 +2,6 @@ import { execFile } from "node:child_process";
2
2
  import { readFileSync } from "node:fs";
3
3
  import {
4
4
  CustomEditor,
5
- DynamicBorder,
6
5
  VERSION,
7
6
  type ExtensionAPI,
8
7
  type ExtensionContext,
@@ -10,9 +9,7 @@ import {
10
9
  type Theme,
11
10
  } from "@earendil-works/pi-coding-agent";
12
11
  import {
13
- Container,
14
12
  CURSOR_MARKER,
15
- Text,
16
13
  truncateToWidth,
17
14
  visibleWidth,
18
15
  wrapTextWithAnsi,
@@ -182,7 +179,7 @@ function isBorderLine(line: string): boolean {
182
179
 
183
180
  function isScrolledTopBorder(line: string): boolean {
184
181
  const unstyled = stripAnsi(line);
185
- return unstyled.includes("↑") || unstyled.includes(".");
182
+ return unstyled.includes("↑");
186
183
  }
187
184
 
188
185
  class PiCodeEditor extends CustomEditor {
@@ -219,10 +216,9 @@ class PiCodeEditor extends CustomEditor {
219
216
 
220
217
  override render(width: number): string[] {
221
218
  if (width <= 0) return [];
222
- if (width < 4) return super.render(width).map((line) => truncateToWidth(line, width, ""));
223
- const innerWidth = width - 2;
219
+ const innerWidth = Math.max(1, width - 2);
224
220
  const lines = super.render(innerWidth);
225
- if (lines.length < 2) return lines.map((line) => truncateToWidth(line, width, ""));
221
+ if (lines.length < 2) return ["", ...lines.map((line) => truncateToWidth(line, width, ""))];
226
222
  let bottomBorderIndex = lines.length - 1;
227
223
  for (let index = lines.length - 1; index >= 1; index -= 1) {
228
224
  if (isBorderLine(lines[index] ?? "")) {
@@ -231,44 +227,40 @@ class PiCodeEditor extends CustomEditor {
231
227
  }
232
228
  }
233
229
 
234
- const gray = (text: string): string => this.runtimeTheme.fg("dim", text);
235
- const framed: string[] = [];
230
+ const dim = (text: string): string => this.runtimeTheme.fg("dim", text);
231
+ const rendered: string[] = [];
236
232
  const top = stripAnsi(lines[0] ?? "");
237
233
  const isScrolledHeader = isScrolledTopBorder(lines[0] ?? "");
238
234
  if (isScrolledHeader) {
239
235
  const count = top.match(/↑\s*(\d+)/)?.[1] ?? "";
240
- const indicator = `${gray("─── ")}${count}${gray(" more ")}${gray("─".repeat(Math.max(0, width - 12 - count.length)))}`;
241
- framed.push(truncateToWidth(indicator, width, ""));
242
- } else {
243
- framed.push(gray("─".repeat(width)));
236
+ rendered.push(truncateToWidth(dim(` ↑ ${count} more`), width, ""));
244
237
  }
245
238
 
246
239
  for (let index = 1; index < bottomBorderIndex; index += 1) {
247
240
  const isPromptLine = index === 1 && !isScrolledHeader;
248
- const prefix = isPromptLine ? gray("❯\u00A0") : " ";
241
+ const prefix = isPromptLine
242
+ ? this.runtimeTheme.fg(this.focused ? "accent" : "dim", "❯\u00A0")
243
+ : " ";
249
244
  let content = lines[index] ?? "";
250
245
  if (isPromptLine && this.getText() === "") {
251
246
  const first = this.suggestion.slice(0, 1);
252
247
  const rest = this.suggestion.slice(1);
253
248
  const cursorMarker = this.focused ? CURSOR_MARKER : "";
254
- content = `${cursorMarker}\x1B[7m${gray(first)}\x1B[27m${gray(rest)}`;
249
+ content = `${cursorMarker}\x1B[7m${dim(first)}\x1B[27m${dim(rest)}`;
255
250
  }
256
- framed.push(`${prefix}${padRight(content, innerWidth)}`);
251
+ rendered.push(`${prefix}${padRight(content, innerWidth)}`);
257
252
  }
258
253
 
259
254
  const bottom = stripAnsi(lines[bottomBorderIndex] ?? "");
260
255
  if (bottom.includes("↓")) {
261
256
  const count = bottom.match(/↓\s*(\d+)/)?.[1] ?? "";
262
- const indicator = `${gray("─── ")}${count}${gray(" more ")}${gray("─".repeat(Math.max(0, width - 12 - count.length)))}`;
263
- framed.push(truncateToWidth(indicator, width, ""));
264
- } else {
265
- framed.push(gray("─".repeat(width)));
257
+ rendered.push(truncateToWidth(dim(` ↓ ${count} more`), width, ""));
266
258
  }
267
259
 
268
260
  for (let index = bottomBorderIndex + 1; index < lines.length; index += 1) {
269
- framed.push(` ${padRight(lines[index] ?? "", innerWidth)}`);
261
+ rendered.push(` ${padRight(lines[index] ?? "", innerWidth)}`);
270
262
  }
271
- return framed.map((line) => truncateToWidth(line, width, ""));
263
+ return ["", ...rendered.map((line) => truncateToWidth(line, width, ""))];
272
264
  }
273
265
  }
274
266
 
@@ -277,39 +269,11 @@ const ACTIVITY_FRAMES = [
277
269
  "✽", "✻", "✶", "✱", "✢", "·",
278
270
  ] as const;
279
271
  const ACTIVITY_FRAME_INTERVAL_MS = 120;
280
- const ACTIVITY_WORDS = ["Brewing", "Pondering", "Tinkering", "Wrangling", "Noodling", "Cooking"] as const;
281
-
282
- function formatActivityMessage(word: string, theme: Theme): string {
283
- return `${theme.fg("accent", `${word}…`)} ${theme.fg("dim", `(${theme.bold("esc")} to interrupt · thinking)`)}`;
284
- }
285
272
 
286
273
  let killerosEditorFactory: ReturnType<ExtensionContext["ui"]["getEditorComponent"]>;
287
274
 
288
275
  export function registerShellUi(pi: ExtensionAPI): void {
289
276
  let activeHeader: PiStartupHeader | undefined;
290
- let activityDeck: string[] = [];
291
- let lastActivityWord: string | undefined;
292
- let activityTimer: ReturnType<typeof setInterval> | undefined;
293
- const refillActivityDeck = (): void => {
294
- activityDeck = [...ACTIVITY_WORDS];
295
- for (let index = activityDeck.length - 1; index > 0; index -= 1) {
296
- const swapIndex = Math.floor(Math.random() * (index + 1));
297
- [activityDeck[index], activityDeck[swapIndex]] = [activityDeck[swapIndex]!, activityDeck[index]!];
298
- }
299
- if (activityDeck.length > 1 && activityDeck.at(-1) === lastActivityWord) {
300
- [activityDeck[0], activityDeck[activityDeck.length - 1]] = [activityDeck.at(-1)!, activityDeck[0]!];
301
- }
302
- };
303
- const nextActivityWord = (): string => {
304
- if (activityDeck.length === 0) refillActivityDeck();
305
- const word = activityDeck.pop() ?? ACTIVITY_WORDS[0];
306
- lastActivityWord = word;
307
- return word;
308
- };
309
- const clearActivityTimer = (): void => {
310
- if (activityTimer) clearInterval(activityTimer);
311
- activityTimer = undefined;
312
- };
313
277
 
314
278
  pi.on("session_start", (_event, ctx) => {
315
279
  if (ctx.mode !== "tui") return;
@@ -321,7 +285,6 @@ export function registerShellUi(pi: ExtensionAPI): void {
321
285
  activeHeader = new PiStartupHeader(pi, ctx, startupTip, tui);
322
286
  return activeHeader;
323
287
  });
324
- clearActivityTimer();
325
288
  ctx.ui.setWorkingIndicator({
326
289
  frames: ACTIVITY_FRAMES.map((frame) => ctx.ui.theme.fg("accent", frame)),
327
290
  intervalMs: ACTIVITY_FRAME_INTERVAL_MS,
@@ -339,25 +302,8 @@ export function registerShellUi(pi: ExtensionAPI): void {
339
302
  }
340
303
  });
341
304
 
342
- pi.on("agent_start", (_event, ctx) => {
343
- if (ctx.mode !== "tui") return;
344
- clearActivityTimer();
345
- const updateWorkingWord = (): void => ctx.ui.setWorkingMessage(formatActivityMessage(nextActivityWord(), ctx.ui.theme));
346
- updateWorkingWord();
347
- activityTimer = setInterval(updateWorkingWord, 2_500);
348
- activityTimer.unref?.();
349
- });
350
-
351
- pi.on("agent_end", (_event, ctx) => {
352
- clearActivityTimer();
353
- if (ctx.mode === "tui") ctx.ui.setWorkingMessage();
354
- });
355
-
356
305
  pi.on("session_shutdown", () => {
357
- clearActivityTimer();
358
306
  activeHeader?.dispose();
359
307
  activeHeader = undefined;
360
- activityDeck = [];
361
- lastActivityWord = undefined;
362
308
  });
363
309
  }
@@ -1,9 +1,9 @@
1
- import { DynamicBorder, keyHint, type ExtensionAPI, type ExtensionContext, type ThemeColor } from "@earendil-works/pi-coding-agent";
2
- import { Container, SelectList, Text, truncateToWidth } from "@earendil-works/pi-tui";
1
+ import { DynamicBorder, type ExtensionAPI, type ExtensionContext, type ThemeColor } from "@earendil-works/pi-coding-agent";
2
+ import { SelectList, truncateToWidth } from "@earendil-works/pi-tui";
3
3
 
4
- export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
4
+ export type ThinkingLevel = ReturnType<ExtensionAPI["getThinkingLevel"]>;
5
5
 
6
- const ALL_LEVELS: readonly ThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
6
+ const ALL_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const satisfies readonly ThinkingLevel[];
7
7
  const LEVEL_LABELS: Readonly<Record<ThinkingLevel, string>> = {
8
8
  off: "Off",
9
9
  minimal: "Minimal",
@@ -42,7 +42,7 @@ const LEVEL_ALIASES: Readonly<Record<string, ThinkingLevel>> = {
42
42
  };
43
43
 
44
44
  function isThinkingLevel(value: string): value is ThinkingLevel {
45
- return (ALL_LEVELS as readonly string[]).includes(value);
45
+ return ALL_LEVELS.some((level) => level === value);
46
46
  }
47
47
 
48
48
  function resolveThinkingLevel(input: string): ThinkingLevel | undefined {
@@ -96,40 +96,82 @@ export function registerVariants(pi: ExtensionAPI): void {
96
96
  ctx.ui.notify(`${modelLabel(ctx.model)} does not support extended reasoning`, "info");
97
97
  return;
98
98
  }
99
- const current = pi.getThinkingLevel() as ThinkingLevel;
99
+ const current = pi.getThinkingLevel();
100
100
  const items = supported.map((level) => ({
101
101
  value: level,
102
102
  label: level === current ? `${LEVEL_LABELS[level]} ← current` : LEVEL_LABELS[level],
103
103
  description: LEVEL_DESCRIPTIONS[level],
104
104
  }));
105
- const selected = await ctx.ui.custom<ThinkingLevel | null>((tui, theme, _keybindings, done) => {
106
- const container = new Container();
107
- container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
108
- container.addChild(new Text(theme.fg("accent", theme.bold("Thinking variants")), 1, 0));
109
- container.addChild(new Text(theme.fg("dim", `Model: ${modelLabel(ctx.model)}`), 1, 0));
110
- container.addChild(new Text("", 0, 0));
111
- const selectList = new SelectList(items, Math.min(items.length, 10), {
112
- selectedPrefix: (text) => theme.fg("accent", text),
113
- selectedText: (text) => theme.fg("accent", text),
114
- description: (text) => theme.fg("muted", text),
115
- scrollInfo: (text) => theme.fg("dim", text),
116
- noMatch: (text) => theme.fg("warning", text),
117
- });
118
- selectList.onSelect = (item) => done(isThinkingLevel(item.value) ? item.value : null);
119
- selectList.onCancel = () => done(null);
120
- container.addChild(selectList);
121
- container.addChild(new Text("", 0, 0));
122
- container.addChild(new Text(
123
- theme.fg("dim", `${keyHint("tui.select.up", "up")} • ${keyHint("tui.select.down", "down")} • ${keyHint("tui.select.confirm", "select")} • ${keyHint("tui.select.cancel", "cancel")}`),
124
- 1,
125
- 0,
126
- ));
127
- container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
105
+ const selected = await ctx.ui.custom<ThinkingLevel | null>((tui, theme, keybindings, done) => {
106
+ const listTheme = {
107
+ selectedPrefix: (text: string) => theme.fg("accent", text),
108
+ selectedText: (text: string) => theme.fg("accent", text),
109
+ description: (text: string) => theme.fg("muted", text),
110
+ scrollInfo: (text: string) => theme.fg("dim", text),
111
+ noMatch: (text: string) => theme.fg("warning", text),
112
+ };
113
+ let selectList: SelectList | undefined;
114
+ let visibleOptionRows = 0;
115
+
116
+ const chromeFor = (rowBudget: number): "full" | "compact" | "none" => (
117
+ rowBudget >= 8 ? "full" : rowBudget >= 4 ? "compact" : "none"
118
+ );
119
+ const visibleRowsFor = (rowBudget: number): number => {
120
+ const chrome = chromeFor(rowBudget);
121
+ const chromeRows = chrome === "full" ? 5 : chrome === "compact" ? 2 : 0;
122
+ const availableListRows = Math.max(1, rowBudget - chromeRows);
123
+ return availableListRows >= items.length
124
+ ? items.length
125
+ : Math.max(1, availableListRows - 1);
126
+ };
127
+ const ensureSelectList = (nextVisibleOptionRows: number): SelectList => {
128
+ if (selectList && visibleOptionRows === nextVisibleOptionRows) return selectList;
129
+ const selectedValue = selectList?.getSelectedItem()?.value ?? current;
130
+ const nextSelectList = new SelectList(items, nextVisibleOptionRows, listTheme);
131
+ const selectedIndex = items.findIndex((item) => item.value === selectedValue);
132
+ nextSelectList.setSelectedIndex(Math.max(0, selectedIndex));
133
+ nextSelectList.onSelect = (item) => done(isThinkingLevel(item.value) ? item.value : null);
134
+ nextSelectList.onCancel = () => done(null);
135
+ selectList = nextSelectList;
136
+ visibleOptionRows = nextVisibleOptionRows;
137
+ return nextSelectList;
138
+ };
139
+
140
+ const border = new DynamicBorder((text: string) => theme.fg("accent", text));
141
+ const title = ` ${theme.fg("accent", theme.bold("Thinking variants"))}`;
142
+ const model = ` ${theme.fg("dim", `Model: ${modelLabel(ctx.model)}`)}`;
143
+ const keyHint = (keybinding: Parameters<typeof keybindings.getKeys>[0], description: string): string => {
144
+ const keyText = keybindings.getKeys(keybinding)
145
+ .join("/")
146
+ .split("/")
147
+ .map((key) => key.split("+").map((part) => process.platform === "darwin" && part.toLocaleLowerCase() === "alt" ? "option" : part).join("+"))
148
+ .join("/");
149
+ return theme.fg("dim", keyText) + theme.fg("muted", ` ${description}`);
150
+ };
151
+ const controls = ` ${theme.fg("dim", `${keyHint("tui.select.up", "up")} • ${keyHint("tui.select.down", "down")} • ${keyHint("tui.select.confirm", "select")} • ${keyHint("tui.select.cancel", "cancel")}`)}`;
152
+
153
+ const render = (width: number): string[] => {
154
+ const rowBudget = Math.max(0, tui.terminal.rows);
155
+ if (width <= 0 || rowBudget === 0) return [];
156
+ const chrome = chromeFor(rowBudget);
157
+ const list = ensureSelectList(visibleRowsFor(rowBudget));
158
+ const lines: string[] = [];
159
+
160
+ if (chrome === "full") lines.push(...border.render(width));
161
+ if (chrome !== "none") lines.push(title);
162
+ if (chrome === "full") lines.push(model);
163
+ lines.push(...list.render(width));
164
+ if (chrome !== "none") lines.push(controls);
165
+ if (chrome === "full") lines.push(...border.render(width));
166
+
167
+ return lines.slice(0, rowBudget).map((line) => truncateToWidth(line, width, ""));
168
+ };
169
+
128
170
  return {
129
- render: (width) => container.render(width).map((line) => truncateToWidth(line, width, "")),
130
- invalidate: () => container.invalidate(),
171
+ render,
172
+ invalidate: () => selectList?.invalidate(),
131
173
  handleInput: (data) => {
132
- selectList.handleInput(data);
174
+ ensureSelectList(visibleRowsFor(Math.max(1, tui.terminal.rows))).handleInput(data);
133
175
  tui.requestRender();
134
176
  },
135
177
  };
@@ -0,0 +1,135 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { StopReason } from "@earendil-works/pi-ai";
3
+ import { Text } from "@earendil-works/pi-tui";
4
+
5
+ const WORKED_FOR_ENTRY_TYPE = "killeros-worked-for";
6
+
7
+ interface WorkedForEntryDataV1 {
8
+ version: 1;
9
+ milliseconds: number;
10
+ }
11
+
12
+ export type WorkedForOutcome = "done" | "stopped" | "failed";
13
+
14
+ interface WorkedForEntryDataV2 {
15
+ version: 2;
16
+ milliseconds: number;
17
+ outcome: WorkedForOutcome;
18
+ }
19
+
20
+ type WorkedForEntryData = WorkedForEntryDataV1 | WorkedForEntryDataV2;
21
+
22
+ const OUTCOMES = {
23
+ done: { marker: "✓", label: "Done", color: "success" },
24
+ stopped: { marker: "■", label: "Stopped", color: "warning" },
25
+ failed: { marker: "×", label: "Failed", color: "error" },
26
+ } as const satisfies Record<WorkedForOutcome, { marker: string; label: string; color: string }>;
27
+
28
+ function isWorkedForOutcome(value: unknown): value is WorkedForOutcome {
29
+ return value === "done" || value === "stopped" || value === "failed";
30
+ }
31
+
32
+ export function formatWorkedForDuration(milliseconds: number): string {
33
+ const boundedMilliseconds = Number.isFinite(milliseconds) ? Math.max(0, milliseconds) : 0;
34
+ const totalSeconds = Math.max(1, Math.floor(boundedMilliseconds / 1_000));
35
+ if (totalSeconds < 60) return `${totalSeconds}s`;
36
+
37
+ const totalMinutes = Math.floor(totalSeconds / 60);
38
+ if (totalMinutes < 60) {
39
+ return `${totalMinutes}m ${(totalSeconds % 60).toString().padStart(2, "0")}s`;
40
+ }
41
+
42
+ return `${Math.floor(totalMinutes / 60)}h ${(totalMinutes % 60).toString().padStart(2, "0")}m`;
43
+ }
44
+
45
+ function parseWorkedForEntryData(data: unknown): WorkedForEntryData | undefined {
46
+ if (!data || typeof data !== "object" || Array.isArray(data)) return undefined;
47
+ if (!("version" in data) || !("milliseconds" in data)) return undefined;
48
+ if (typeof data.milliseconds !== "number" || !Number.isFinite(data.milliseconds) || data.milliseconds < 0) {
49
+ return undefined;
50
+ }
51
+ if (data.version === 1) return { version: 1, milliseconds: data.milliseconds };
52
+ if (data.version !== 2 || !("outcome" in data) || !isWorkedForOutcome(data.outcome)) {
53
+ return undefined;
54
+ }
55
+ return { version: 2, milliseconds: data.milliseconds, outcome: data.outcome };
56
+ }
57
+
58
+ export function workedForOutcome(stopReason: StopReason | undefined): WorkedForOutcome {
59
+ if (stopReason === "stop") return "done";
60
+ if (stopReason === "aborted") return "stopped";
61
+ return "failed";
62
+ }
63
+
64
+ function errorMessage(error: unknown): string {
65
+ return error instanceof Error ? error.message : String(error);
66
+ }
67
+
68
+ export function registerWorkedFor(
69
+ pi: ExtensionAPI,
70
+ now: () => number = Date.now,
71
+ ): void {
72
+ let startedAt: number | undefined;
73
+ let stopReason: StopReason | undefined;
74
+
75
+ pi.registerEntryRenderer<WorkedForEntryData>(WORKED_FOR_ENTRY_TYPE, (entry, _options, theme) => {
76
+ const data = parseWorkedForEntryData(entry.data);
77
+ if (!data) return undefined;
78
+ if (data.version === 1) {
79
+ return new Text(
80
+ theme.fg("dim", `✻ Worked for ${formatWorkedForDuration(data.milliseconds)}`),
81
+ 0,
82
+ 0,
83
+ );
84
+ }
85
+ const outcome = OUTCOMES[data.outcome];
86
+ return new Text(
87
+ `${theme.fg(outcome.color, `${outcome.marker} ${outcome.label}`)}${theme.fg("dim", ` · ${formatWorkedForDuration(data.milliseconds)}`)}`,
88
+ 0,
89
+ 0,
90
+ );
91
+ });
92
+
93
+ pi.on("session_start", () => {
94
+ startedAt = undefined;
95
+ stopReason = undefined;
96
+ });
97
+
98
+ pi.on("agent_start", (_event, ctx) => {
99
+ if (ctx.mode !== "tui" || startedAt !== undefined) return;
100
+ startedAt = now();
101
+ });
102
+
103
+ pi.on("agent_end", (event, ctx) => {
104
+ if (ctx.mode !== "tui" || startedAt === undefined) return;
105
+ for (let index = event.messages.length - 1; index >= 0; index -= 1) {
106
+ const message = event.messages[index];
107
+ if (message?.role !== "assistant") continue;
108
+ stopReason = message.stopReason;
109
+ break;
110
+ }
111
+ });
112
+
113
+ pi.on("agent_settled", (_event, ctx) => {
114
+ if (ctx.mode !== "tui" || startedAt === undefined) return;
115
+ if (!ctx.isIdle() || ctx.hasPendingMessages()) return;
116
+ const milliseconds = Math.max(0, now() - startedAt);
117
+ const outcome = workedForOutcome(stopReason);
118
+ startedAt = undefined;
119
+ stopReason = undefined;
120
+ try {
121
+ pi.appendEntry<WorkedForEntryDataV2>(WORKED_FOR_ENTRY_TYPE, {
122
+ version: 2,
123
+ milliseconds,
124
+ outcome,
125
+ });
126
+ } catch (error) {
127
+ ctx.ui.notify(`Worked-for timing could not be saved: ${errorMessage(error)}`, "error");
128
+ }
129
+ });
130
+
131
+ pi.on("session_shutdown", () => {
132
+ startedAt = undefined;
133
+ stopReason = undefined;
134
+ });
135
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "killeros",
3
- "version": "2.0.5",
3
+ "version": "2.0.7",
4
4
  "description": "TUI, goals, and workflow automation for the Pi coding agent",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -42,14 +42,15 @@
42
42
  ]
43
43
  },
44
44
  "peerDependencies": {
45
- "@earendil-works/pi-ai": ">=0.82.1",
46
- "@earendil-works/pi-coding-agent": ">=0.82.1",
47
- "@earendil-works/pi-tui": ">=0.82.1",
45
+ "@earendil-works/pi-ai": ">=0.84.1",
46
+ "@earendil-works/pi-coding-agent": ">=0.84.1",
47
+ "@earendil-works/pi-tui": ">=0.84.1",
48
48
  "typebox": ">=1.1.38 <2"
49
49
  },
50
50
  "devDependencies": {
51
- "@earendil-works/pi-coding-agent": "0.82.1",
52
- "@earendil-works/pi-tui": "0.82.1",
51
+ "@earendil-works/pi-ai": "0.84.1",
52
+ "@earendil-works/pi-coding-agent": "0.84.1",
53
+ "@earendil-works/pi-tui": "0.84.1",
53
54
  "@types/node": "24.12.4",
54
55
  "typebox": "1.1.38",
55
56
  "typescript": "5.9.3"