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.
- package/CHANGELOG.md +38 -0
- package/Killeros.ts +4 -0
- package/README.md +19 -12
- package/killeros/activity.ts +97 -0
- package/killeros/footer.ts +31 -45
- package/killeros/goals.ts +118 -42
- package/killeros/init.ts +23 -2
- package/killeros/question.ts +382 -186
- package/killeros/runtime.ts +9 -0
- package/killeros/shell-ui.ts +14 -68
- package/killeros/variants.ts +74 -32
- package/killeros/worked-for.ts +135 -0
- package/package.json +7 -6
package/killeros/runtime.ts
CHANGED
|
@@ -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;
|
package/killeros/shell-ui.ts
CHANGED
|
@@ -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("↑")
|
|
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
|
-
|
|
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
|
|
235
|
-
const
|
|
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
|
-
|
|
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
|
|
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${
|
|
249
|
+
content = `${cursorMarker}\x1B[7m${dim(first)}\x1B[27m${dim(rest)}`;
|
|
255
250
|
}
|
|
256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
261
|
+
rendered.push(` ${padRight(lines[index] ?? "", innerWidth)}`);
|
|
270
262
|
}
|
|
271
|
-
return
|
|
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
|
}
|
package/killeros/variants.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { DynamicBorder,
|
|
2
|
-
import {
|
|
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 = "
|
|
4
|
+
export type ThinkingLevel = ReturnType<ExtensionAPI["getThinkingLevel"]>;
|
|
5
5
|
|
|
6
|
-
const ALL_LEVELS
|
|
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 (
|
|
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()
|
|
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,
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
130
|
-
invalidate: () =>
|
|
171
|
+
render,
|
|
172
|
+
invalidate: () => selectList?.invalidate(),
|
|
131
173
|
handleInput: (data) => {
|
|
132
|
-
|
|
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.
|
|
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.
|
|
46
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
47
|
-
"@earendil-works/pi-tui": ">=0.
|
|
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-
|
|
52
|
-
"@earendil-works/pi-
|
|
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"
|