killeros 1.5.8 → 2.0.1
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 +17 -130
- package/Killeros.ts +1 -25
- package/README.md +130 -209
- package/killeros/bounded-text.ts +25 -0
- package/killeros/commands.ts +8 -238
- package/killeros/display.ts +6 -2
- package/killeros/footer.ts +35 -5
- package/killeros/goals.ts +53 -25
- package/killeros/hooks.ts +1 -1
- package/killeros/limits.ts +1 -0
- package/killeros/personal-instructions.ts +3 -1
- package/killeros/question.ts +144 -97
- package/killeros/shell-ui.ts +230 -54
- package/killeros/variants.ts +6 -2
- package/package.json +1 -6
- package/themes/killeros.json +4 -3
- package/killeros/subagent-lifecycle.ts +0 -761
- package/killeros/subagent-persistence.ts +0 -572
- package/killeros/subagent-process.ts +0 -626
- package/killeros/subagent-ui.ts +0 -245
- package/killeros/subagents.ts +0 -3257
- package/subagent-lifecycle.ts +0 -1
- package/subagent-process.ts +0 -1
- package/subagent-ui.ts +0 -1
- package/subagents.ts +0 -1
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Text, truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
export class BoundedText implements Component {
|
|
4
|
+
private readonly text: string;
|
|
5
|
+
private readonly maxRows?: number;
|
|
6
|
+
|
|
7
|
+
constructor(text: string, maxRows?: number) {
|
|
8
|
+
this.text = text;
|
|
9
|
+
this.maxRows = maxRows;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
render(width: number): string[] {
|
|
13
|
+
if (width <= 0) return [];
|
|
14
|
+
const lines = new Text(this.text, 0, 0).render(width);
|
|
15
|
+
if (this.maxRows === undefined || lines.length <= this.maxRows) return lines;
|
|
16
|
+
const rowLimit = Math.max(1, this.maxRows);
|
|
17
|
+
const visible = lines.slice(0, rowLimit);
|
|
18
|
+
const suffix = " …";
|
|
19
|
+
const last = visible.at(-1) ?? "";
|
|
20
|
+
visible[rowLimit - 1] = `${truncateToWidth(last, Math.max(0, width - visibleWidth(suffix)), "")}${suffix}`;
|
|
21
|
+
return visible.map((line) => truncateToWidth(line, width, ""));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
invalidate(): void {}
|
|
25
|
+
}
|
package/killeros/commands.ts
CHANGED
|
@@ -1,76 +1,5 @@
|
|
|
1
|
-
import { type ExtensionAPI, type ExtensionCommandContext
|
|
1
|
+
import { type ExtensionAPI, type ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
3
|
-
import { formatThreadControls, type ThreadStatus } from "./subagent-ui.ts";
|
|
4
|
-
|
|
5
|
-
export type SubagentControlAction = "list" | "inspect" | "wait" | "steer" | "interrupt" | "collect" | "resume" | "close";
|
|
6
|
-
|
|
7
|
-
export interface SubagentControlRequest {
|
|
8
|
-
action: SubagentControlAction;
|
|
9
|
-
threadId?: string;
|
|
10
|
-
all?: true;
|
|
11
|
-
message?: string;
|
|
12
|
-
task?: string;
|
|
13
|
-
timeoutMs?: number;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface SubagentControlThread {
|
|
17
|
-
id: string;
|
|
18
|
-
displayName?: string;
|
|
19
|
-
name?: string;
|
|
20
|
-
agent?: string;
|
|
21
|
-
role?: string;
|
|
22
|
-
task?: string;
|
|
23
|
-
prompt?: string;
|
|
24
|
-
status?: string;
|
|
25
|
-
state?: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface SubagentControlDetails {
|
|
29
|
-
results?: readonly SubagentControlThread[];
|
|
30
|
-
threads?: readonly SubagentControlThread[];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface SubagentControlResult {
|
|
34
|
-
text: string;
|
|
35
|
-
details?: SubagentControlDetails;
|
|
36
|
-
usage?: unknown;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface SubagentControlApi {
|
|
40
|
-
execute(request: SubagentControlRequest, ctx: ExtensionContext): Promise<SubagentControlResult>;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface SubagentToolLike {
|
|
44
|
-
name: string;
|
|
45
|
-
execute(
|
|
46
|
-
toolCallId: string,
|
|
47
|
-
params: unknown,
|
|
48
|
-
signal: AbortSignal | undefined,
|
|
49
|
-
onUpdate: undefined,
|
|
50
|
-
ctx: ExtensionContext,
|
|
51
|
-
): Promise<{
|
|
52
|
-
content?: readonly { type: string; text?: string }[];
|
|
53
|
-
details?: unknown;
|
|
54
|
-
usage?: unknown;
|
|
55
|
-
}>;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function createSubagentControlApi(tool: SubagentToolLike): SubagentControlApi {
|
|
59
|
-
return {
|
|
60
|
-
async execute(request, ctx) {
|
|
61
|
-
const toolRequest = request.action === "interrupt" && request.threadId === "all"
|
|
62
|
-
? { action: "interrupt", all: true }
|
|
63
|
-
: request;
|
|
64
|
-
const result = await tool.execute("subagents-command", toolRequest, ctx.signal, undefined, ctx);
|
|
65
|
-
const text = result.content?.find((item) => item.type === "text")?.text ?? "";
|
|
66
|
-
return {
|
|
67
|
-
text,
|
|
68
|
-
details: result.details as SubagentControlDetails | undefined,
|
|
69
|
-
usage: result.usage,
|
|
70
|
-
};
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
3
|
|
|
75
4
|
async function confirmNewSession(ctx: ExtensionCommandContext): Promise<boolean> {
|
|
76
5
|
if (!ctx.hasUI) return true;
|
|
@@ -125,10 +54,16 @@ const BUILTIN_COMMANDS: ReadonlyArray<{ name: string; description: string }> = [
|
|
|
125
54
|
{ name: "quit", description: "Quit Pi" },
|
|
126
55
|
];
|
|
127
56
|
|
|
57
|
+
export function availableCommandNames(pi: ExtensionAPI): ReadonlySet<string> {
|
|
58
|
+
return new Set([
|
|
59
|
+
...BUILTIN_COMMANDS.map((command) => command.name),
|
|
60
|
+
...pi.getCommands().map((command) => command.name),
|
|
61
|
+
]);
|
|
62
|
+
}
|
|
63
|
+
|
|
128
64
|
const COMMAND_SYNTAX_HINTS: Readonly<Record<string, string>> = {
|
|
129
65
|
goal: "/goal [objective|clear|edit|pause|resume]",
|
|
130
66
|
variants: "/variants [level]",
|
|
131
|
-
subagents: "/subagents [list|inspect|wait|steer|interrupt|collect|resume|close] [thread]",
|
|
132
67
|
model: "/model [provider/model]",
|
|
133
68
|
"scoped-models": "/scoped-models",
|
|
134
69
|
login: "/login [provider]",
|
|
@@ -151,162 +86,6 @@ function scoreCommandMatch(name: string, prefix: string): number {
|
|
|
151
86
|
return 0;
|
|
152
87
|
}
|
|
153
88
|
|
|
154
|
-
const SUBAGENT_COMMAND_USAGE = "/subagents [list|inspect|wait|steer|interrupt|collect|resume|close] [thread]";
|
|
155
|
-
|
|
156
|
-
function subagentCommandError(message: string): Error {
|
|
157
|
-
return new Error(`${message} Usage: ${SUBAGENT_COMMAND_USAGE}`);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function parseThreadReference(action: SubagentControlAction, tail: string): string {
|
|
161
|
-
const reference = tail.match(/^(\S+)(?:\s+([\s\S]*))?$/u)?.[1];
|
|
162
|
-
if (!reference) throw subagentCommandError(`/subagents ${action} requires a thread reference.`);
|
|
163
|
-
return reference;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function parseExplicitSubagentCommand(args: string): SubagentControlRequest {
|
|
167
|
-
const trimmed = args.trim();
|
|
168
|
-
const match = trimmed.match(/^(\S+)(?:\s+([\s\S]*))?$/u);
|
|
169
|
-
if (!match) throw subagentCommandError("/subagents requires an explicit verb outside TUI.");
|
|
170
|
-
const action = match[1]!.toLocaleLowerCase() as SubagentControlAction;
|
|
171
|
-
const tail = match[2]?.trim() ?? "";
|
|
172
|
-
|
|
173
|
-
if (action === "list") {
|
|
174
|
-
if (tail) throw subagentCommandError("/subagents list does not accept arguments.");
|
|
175
|
-
return { action };
|
|
176
|
-
}
|
|
177
|
-
if (action === "wait") {
|
|
178
|
-
if (!tail) return { action };
|
|
179
|
-
const parts = tail.split(/\s+/u);
|
|
180
|
-
if (parts.length > 2) throw subagentCommandError("/subagents wait accepts one thread reference and one timeout-ms value.");
|
|
181
|
-
if (parts.length === 1 && /^\d+$/u.test(parts[0]!)) {
|
|
182
|
-
return { action, timeoutMs: parseTimeout(parts[0]!) };
|
|
183
|
-
}
|
|
184
|
-
const request: SubagentControlRequest = { action, threadId: parts[0] };
|
|
185
|
-
if (parts[1] !== undefined) request.timeoutMs = parseTimeout(parts[1]);
|
|
186
|
-
return request;
|
|
187
|
-
}
|
|
188
|
-
if (action === "steer") {
|
|
189
|
-
const referenceAndMessage = tail.match(/^(\S+)(?:\s+([\s\S]+))?$/u);
|
|
190
|
-
if (!referenceAndMessage?.[1]) throw subagentCommandError("/subagents steer requires a thread reference.");
|
|
191
|
-
if (!referenceAndMessage[2]?.trim()) throw subagentCommandError("/subagents steer requires a message.");
|
|
192
|
-
return { action, threadId: referenceAndMessage[1], message: referenceAndMessage[2] };
|
|
193
|
-
}
|
|
194
|
-
if (action === "resume") {
|
|
195
|
-
const referenceAndTask = tail.match(/^(\S+)(?:\s+([\s\S]+))?$/u);
|
|
196
|
-
if (!referenceAndTask?.[1]) throw subagentCommandError("/subagents resume requires a thread reference.");
|
|
197
|
-
return {
|
|
198
|
-
action,
|
|
199
|
-
threadId: referenceAndTask[1],
|
|
200
|
-
...(referenceAndTask[2] ? { task: referenceAndTask[2] } : {}),
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
if (action === "inspect" || action === "interrupt" || action === "collect" || action === "close") {
|
|
204
|
-
const threadId = parseThreadReference(action, tail);
|
|
205
|
-
if (tail.slice(threadId.length).trim()) throw subagentCommandError(`/subagents ${action} accepts one thread reference.`);
|
|
206
|
-
return { action, threadId };
|
|
207
|
-
}
|
|
208
|
-
throw subagentCommandError(`Unknown /subagents action ${JSON.stringify(match[1])}.`);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function parseTimeout(value: string): number {
|
|
212
|
-
const timeoutMs = Number(value);
|
|
213
|
-
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 0) {
|
|
214
|
-
throw subagentCommandError("/subagents wait timeout-ms must be a non-negative integer.");
|
|
215
|
-
}
|
|
216
|
-
return timeoutMs;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function controlThreads(result: SubagentControlResult): SubagentControlThread[] {
|
|
220
|
-
const details = result.details;
|
|
221
|
-
if (!details) return [];
|
|
222
|
-
const results = [...(details.results ?? [])];
|
|
223
|
-
const threads = [...(details.threads ?? [])];
|
|
224
|
-
const candidates = results.length ? results : threads;
|
|
225
|
-
return candidates.filter((thread) => thread && typeof thread.id === "string" && thread.state !== "closed" && thread.status !== "closed");
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function threadStatus(thread: SubagentControlThread): ThreadStatus {
|
|
229
|
-
const status = (thread.status ?? thread.state ?? "queued").toLocaleLowerCase();
|
|
230
|
-
if (status === "active" || status === "running") return "running";
|
|
231
|
-
if (status === "done" || status === "complete" || status === "closed") return "complete";
|
|
232
|
-
if (status === "stopped" || status === "cancelled") return "cancelled";
|
|
233
|
-
if (status === "limited") return "limited";
|
|
234
|
-
if (status === "orphaned") return "orphaned";
|
|
235
|
-
if (status === "failed") return "failed";
|
|
236
|
-
return "queued";
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function threadLabel(thread: SubagentControlThread): string {
|
|
240
|
-
const name = thread.displayName ?? thread.name ?? thread.agent ?? thread.role ?? thread.id;
|
|
241
|
-
return `${name} · ${thread.id} · ${threadStatus(thread)}`;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function selectedThread(threads: readonly SubagentControlThread[], labels: readonly string[], choice: string): SubagentControlThread | undefined {
|
|
245
|
-
const index = labels.indexOf(choice);
|
|
246
|
-
if (index >= 0) return threads[index];
|
|
247
|
-
return threads.find((thread) => thread.id === choice || thread.displayName === choice || thread.name === choice);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
async function executeSubagentControl(
|
|
251
|
-
control: SubagentControlApi | undefined,
|
|
252
|
-
request: SubagentControlRequest,
|
|
253
|
-
ctx: ExtensionCommandContext,
|
|
254
|
-
): Promise<void> {
|
|
255
|
-
if (!control) throw new Error("Subagent control API is not available.");
|
|
256
|
-
const result = await control.execute(request, ctx);
|
|
257
|
-
if (result?.text) ctx.ui.notify(result.text, "info");
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
async function runTuiSubagentCommand(control: SubagentControlApi | undefined, ctx: ExtensionCommandContext): Promise<void> {
|
|
261
|
-
if (!control) throw new Error("Subagent control API is not available.");
|
|
262
|
-
const listed = await control.execute({ action: "list" }, ctx);
|
|
263
|
-
const threads = controlThreads(listed);
|
|
264
|
-
if (!threads.length) {
|
|
265
|
-
ctx.ui.notify("No child threads.", "info");
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const labels = threads.map(threadLabel);
|
|
270
|
-
const selected = await ctx.ui.select("Select a thread", labels);
|
|
271
|
-
if (selected === undefined) return;
|
|
272
|
-
const thread = selectedThread(threads, labels, selected);
|
|
273
|
-
if (!thread) return;
|
|
274
|
-
|
|
275
|
-
const controls = formatThreadControls(threadStatus(thread)).filter((item) => item.enabled);
|
|
276
|
-
const controlLabels = controls.map((item) => item.label);
|
|
277
|
-
const selectedControl = await ctx.ui.select("Select a control", controlLabels);
|
|
278
|
-
if (selectedControl === undefined) return;
|
|
279
|
-
const chosen = controls.find((item) => item.label === selectedControl || item.id === selectedControl);
|
|
280
|
-
if (!chosen) return;
|
|
281
|
-
|
|
282
|
-
const request: SubagentControlRequest = { action: chosen.id, threadId: thread.id };
|
|
283
|
-
if (chosen.id === "steer") {
|
|
284
|
-
const message = await ctx.ui.input("Steer child thread", "Message");
|
|
285
|
-
if (message === undefined || !message.trim()) return;
|
|
286
|
-
request.message = message;
|
|
287
|
-
} else if (chosen.id === "resume") {
|
|
288
|
-
const task = await ctx.ui.input("Resume child thread", "Optional task");
|
|
289
|
-
if (task === undefined) return;
|
|
290
|
-
if (task) request.task = task;
|
|
291
|
-
}
|
|
292
|
-
await executeSubagentControl(control, request, ctx);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
export function registerSubagentCommand(pi: ExtensionAPI, control?: SubagentControlApi | void): void {
|
|
296
|
-
const api = control && typeof control.execute === "function" ? control : undefined;
|
|
297
|
-
pi.registerCommand("subagents", {
|
|
298
|
-
description: "Inspect and control child threads",
|
|
299
|
-
handler: async (args, ctx) => {
|
|
300
|
-
if (!args.trim()) {
|
|
301
|
-
if (ctx.mode !== "tui") throw subagentCommandError("/subagents requires an explicit verb outside TUI.");
|
|
302
|
-
await runTuiSubagentCommand(api, ctx);
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
await executeSubagentControl(api, parseExplicitSubagentCommand(args), ctx);
|
|
306
|
-
},
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
|
|
310
89
|
export function registerSlashAutocomplete(pi: ExtensionAPI): void {
|
|
311
90
|
const usage = new Map<string, number>();
|
|
312
91
|
pi.on("session_start", (_event, ctx) => {
|
|
@@ -349,15 +128,6 @@ export function registerSlashAutocomplete(pi: ExtensionAPI): void {
|
|
|
349
128
|
}
|
|
350
129
|
}
|
|
351
130
|
|
|
352
|
-
if (!commands.has("subagents")) {
|
|
353
|
-
commands.set("subagents", {
|
|
354
|
-
name: "subagents",
|
|
355
|
-
description: "Inspect and control child threads",
|
|
356
|
-
category: "Extension",
|
|
357
|
-
syntaxHint: COMMAND_SYNTAX_HINTS.subagents,
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
|
|
361
131
|
const ranked = [...commands.values()]
|
|
362
132
|
.map((command) => ({
|
|
363
133
|
command,
|
package/killeros/display.ts
CHANGED
|
@@ -6,9 +6,11 @@ export function formatCwd(cwd: string): string {
|
|
|
6
6
|
if (!home) return cwd;
|
|
7
7
|
const normalizedHome = home.replace(/[\\/]+$/, "");
|
|
8
8
|
const normalizedCwd = cwd.replace(/[\\/]+$/, "");
|
|
9
|
-
|
|
9
|
+
const comparedHome = process.platform === "win32" ? normalizedHome.toLocaleLowerCase() : normalizedHome;
|
|
10
|
+
const comparedCwd = process.platform === "win32" ? normalizedCwd.toLocaleLowerCase() : normalizedCwd;
|
|
11
|
+
if (comparedCwd === comparedHome) return "~";
|
|
10
12
|
const separator = normalizedCwd.slice(normalizedHome.length, normalizedHome.length + 1);
|
|
11
|
-
return
|
|
13
|
+
return comparedCwd.startsWith(comparedHome) && (separator === "/" || separator === "\\")
|
|
12
14
|
? `~${normalizedCwd.slice(normalizedHome.length)}`
|
|
13
15
|
: cwd;
|
|
14
16
|
}
|
|
@@ -20,6 +22,7 @@ export function padRight(text: string, width: number): string {
|
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export function formatTime(milliseconds: number): string {
|
|
25
|
+
if (!Number.isFinite(milliseconds)) return "0s";
|
|
23
26
|
const totalSeconds = Math.max(0, Math.floor(milliseconds / 1_000));
|
|
24
27
|
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
25
28
|
const minutes = Math.floor(totalSeconds / 60);
|
|
@@ -28,6 +31,7 @@ export function formatTime(milliseconds: number): string {
|
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
export function formatTokens(value: number): string {
|
|
34
|
+
if (!Number.isFinite(value)) return "0";
|
|
31
35
|
const amount = Math.max(0, value);
|
|
32
36
|
if (amount < 1_000) return `${Math.round(amount)}`;
|
|
33
37
|
if (amount >= 1_000_000) {
|
package/killeros/footer.ts
CHANGED
|
@@ -13,8 +13,8 @@ export function formatCost(usd: number): string {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export function formatContextProgress(tokensUsed: number | null, contextWindow: number, theme: Theme): string {
|
|
16
|
-
if (tokensUsed === null) return theme.fg("dim", "—% left (—)");
|
|
17
|
-
const windowSize = contextWindow > 0 ? contextWindow : 128_000;
|
|
16
|
+
if (tokensUsed === null || !Number.isFinite(tokensUsed)) return theme.fg("dim", "—% left (—)");
|
|
17
|
+
const windowSize = Number.isFinite(contextWindow) && contextWindow > 0 ? contextWindow : 128_000;
|
|
18
18
|
const remaining = Math.max(0, Math.min(windowSize, windowSize - Math.max(0, tokensUsed)));
|
|
19
19
|
const percentLeft = Math.max(0, Math.min(100, Math.round((remaining / windowSize) * 100)));
|
|
20
20
|
const color: ThemeColor = percentLeft < 20 ? "error" : percentLeft <= 50 ? "warning" : "success";
|
|
@@ -116,16 +116,34 @@ function formatGoalFooter(state: GoalState | undefined, theme: Theme): string {
|
|
|
116
116
|
if (state.status === "active") return theme.fg("accent", `✻ goal · ${formatTime(goalElapsedMilliseconds(state))}`);
|
|
117
117
|
if (state.status === "paused") return theme.fg("warning", "Ⅱ goal paused");
|
|
118
118
|
if (state.status === "blocked") return theme.fg("error", "! goal blocked");
|
|
119
|
-
return
|
|
119
|
+
return "";
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void {
|
|
123
123
|
let currentModel: ExtensionContext["model"];
|
|
124
124
|
let thinkingLevel: ThinkingLevel = "off";
|
|
125
125
|
let activeTui: TUI | undefined;
|
|
126
|
+
let cachedSessionCost = 0;
|
|
127
|
+
let sessionCostDirty = true;
|
|
128
|
+
const resetSessionCost = (): void => {
|
|
129
|
+
cachedSessionCost = 0;
|
|
130
|
+
sessionCostDirty = true;
|
|
131
|
+
};
|
|
132
|
+
const invalidateSessionCost = (): void => {
|
|
133
|
+
sessionCostDirty = true;
|
|
134
|
+
activeTui?.requestRender();
|
|
135
|
+
};
|
|
136
|
+
const getSessionCost = (ctx: ExtensionContext): number => {
|
|
137
|
+
if (sessionCostDirty) {
|
|
138
|
+
cachedSessionCost = sumSessionCost(ctx);
|
|
139
|
+
sessionCostDirty = false;
|
|
140
|
+
}
|
|
141
|
+
return cachedSessionCost;
|
|
142
|
+
};
|
|
126
143
|
goalRuntime.requestRender = () => activeTui?.requestRender();
|
|
127
144
|
|
|
128
145
|
pi.on("session_start", (_event, ctx) => {
|
|
146
|
+
resetSessionCost();
|
|
129
147
|
if (ctx.mode !== "tui") return;
|
|
130
148
|
const sessionStart = Date.now();
|
|
131
149
|
currentModel = ctx.model;
|
|
@@ -150,7 +168,12 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
|
|
|
150
168
|
const level = model?.reasoning === false
|
|
151
169
|
? theme.fg("thinkingOff", "no reasoning")
|
|
152
170
|
: theme.fg(LEVEL_COLORS[thinkingLevel], thinkingLevel);
|
|
153
|
-
|
|
171
|
+
let usage: ReturnType<ExtensionContext["getContextUsage"]>;
|
|
172
|
+
try {
|
|
173
|
+
usage = ctx.getContextUsage();
|
|
174
|
+
} catch {
|
|
175
|
+
usage = undefined;
|
|
176
|
+
}
|
|
154
177
|
const contextWindow = usage?.contextWindow ?? model?.contextWindow ?? 128_000;
|
|
155
178
|
const context = formatContextProgress(usage?.tokens ?? null, contextWindow, theme);
|
|
156
179
|
const branch = footerData.getGitBranch();
|
|
@@ -165,7 +188,7 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
|
|
|
165
188
|
goal,
|
|
166
189
|
branch ? theme.fg("dim", branch) : "",
|
|
167
190
|
theme.fg("dim", formatTime(Date.now() - sessionStart)),
|
|
168
|
-
theme.fg("dim", formatCost(
|
|
191
|
+
theme.fg("dim", formatCost(getSessionCost(ctx))),
|
|
169
192
|
], theme);
|
|
170
193
|
const focused = joinFooterParts([signature, context, goal], theme);
|
|
171
194
|
|
|
@@ -202,7 +225,14 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
|
|
|
202
225
|
thinkingLevel = event.level;
|
|
203
226
|
activeTui?.requestRender();
|
|
204
227
|
});
|
|
228
|
+
pi.on("turn_end", invalidateSessionCost);
|
|
229
|
+
pi.on("session_compact", invalidateSessionCost);
|
|
230
|
+
pi.on("session_tree", () => {
|
|
231
|
+
resetSessionCost();
|
|
232
|
+
activeTui?.requestRender();
|
|
233
|
+
});
|
|
205
234
|
pi.on("session_shutdown", () => {
|
|
235
|
+
resetSessionCost();
|
|
206
236
|
activeTui = undefined;
|
|
207
237
|
goalRuntime.requestRender = undefined;
|
|
208
238
|
});
|
package/killeros/goals.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { type ExtensionAPI, type ExtensionContext, type ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, type ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Text } from "@earendil-works/pi-tui";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
|
-
import { MAX_NODE_TIMER_MS } from "./
|
|
4
|
+
import { MAX_NODE_TIMER_MS } from "./limits.ts";
|
|
5
|
+
import { BoundedText } from "./bounded-text.ts";
|
|
5
6
|
import { CONCISE_SYSTEM_PROMPT } from "./concise.ts";
|
|
6
7
|
import { formatTime, formatTokens } from "./display.ts";
|
|
7
8
|
import { reportError } from "./errors.ts";
|
|
@@ -175,6 +176,18 @@ function goalStatusLabel(status: GoalStatus): string {
|
|
|
175
176
|
return `${status.charAt(0).toLocaleUpperCase()}${status.slice(1)}`;
|
|
176
177
|
}
|
|
177
178
|
|
|
179
|
+
function goalPanelActions(status: GoalStatus): Array<{ label: string; control: "pause" | "resume" | "edit" | "clear" }> {
|
|
180
|
+
const terminal = [
|
|
181
|
+
{ label: "Edit objective", control: "edit" as const },
|
|
182
|
+
{ label: "Clear goal", control: "clear" as const },
|
|
183
|
+
];
|
|
184
|
+
if (status === "active") return [{ label: "Pause automatic continuation", control: "pause" }, ...terminal];
|
|
185
|
+
if (status === "paused" || status === "blocked") {
|
|
186
|
+
return [{ label: "Resume automatic continuation", control: "resume" }, ...terminal];
|
|
187
|
+
}
|
|
188
|
+
return terminal;
|
|
189
|
+
}
|
|
190
|
+
|
|
178
191
|
function goalStatusSummary(state: GoalState, ctx: ExtensionContext): string {
|
|
179
192
|
const usedTokens = Math.max(0, sumGoalTokens(ctx) - state.baselineTokens);
|
|
180
193
|
const lines = [
|
|
@@ -293,7 +306,7 @@ export function registerGoal(
|
|
|
293
306
|
runtime: GoalRuntime,
|
|
294
307
|
initState: InitRuntime,
|
|
295
308
|
): void {
|
|
296
|
-
pi.registerEntryRenderer<GoalEntryData>(GOAL_ENTRY_TYPE, (entry,
|
|
309
|
+
pi.registerEntryRenderer<GoalEntryData>(GOAL_ENTRY_TYPE, (entry, options, theme) => {
|
|
297
310
|
const data = entry.data;
|
|
298
311
|
if (!data || data.version !== GOAL_VERSION || data.event === "turn" || data.event === "checkpoint") return undefined;
|
|
299
312
|
if (data.event === "clear" || data.state === null) return new Text(theme.fg("dim", "Goal cleared"), 0, 0);
|
|
@@ -301,7 +314,11 @@ export function registerGoal(
|
|
|
301
314
|
if (!state) return undefined;
|
|
302
315
|
const icon = state.status === "active" ? "✻" : state.status === "paused" ? "Ⅱ" : state.status === "blocked" ? "!" : "✓";
|
|
303
316
|
const color: ThemeColor = state.status === "active" ? "accent" : state.status === "paused" ? "warning" : state.status === "blocked" ? "error" : "success";
|
|
304
|
-
|
|
317
|
+
const status = theme.fg(color, `${icon} Goal ${state.status}`);
|
|
318
|
+
if (!options.expanded) return new BoundedText(`${status}${theme.fg("dim", ` · ${state.objective}`)}`, 3);
|
|
319
|
+
const lines = [status, theme.fg("dim", state.objective)];
|
|
320
|
+
if (state.result) lines.push(theme.fg("muted", state.result));
|
|
321
|
+
return new BoundedText(lines.join("\n"));
|
|
305
322
|
});
|
|
306
323
|
|
|
307
324
|
pi.registerTool<typeof GoalUpdateParams, GoalUpdateDetails>({
|
|
@@ -329,11 +346,11 @@ export function registerGoal(
|
|
|
329
346
|
renderCall(args, theme) {
|
|
330
347
|
return new Text(`${theme.fg("toolTitle", theme.bold("goal "))}${theme.fg("muted", args.status)}`, 0, 0);
|
|
331
348
|
},
|
|
332
|
-
renderResult(result,
|
|
349
|
+
renderResult(result, options, theme) {
|
|
333
350
|
const details = result.details;
|
|
334
|
-
return new
|
|
335
|
-
|
|
336
|
-
|
|
351
|
+
if (!details) return new BoundedText(theme.fg("dim", "Goal updated"));
|
|
352
|
+
const text = `${theme.fg(details.status === "complete" ? "success" : "warning", details.status === "complete" ? "✓ Complete" : "! Blocked")}${theme.fg("dim", ` · ${details.evidence}`)}`;
|
|
353
|
+
return new BoundedText(text, options.expanded ? undefined : 3);
|
|
337
354
|
},
|
|
338
355
|
});
|
|
339
356
|
|
|
@@ -428,22 +445,7 @@ export function registerGoal(
|
|
|
428
445
|
runtime.lastError = finalAssistant?.errorMessage;
|
|
429
446
|
});
|
|
430
447
|
|
|
431
|
-
|
|
432
|
-
description: "Set or view the goal for a long-running task",
|
|
433
|
-
getArgumentCompletions: (prefix) => {
|
|
434
|
-
const normalized = prefix.trimStart().toLocaleLowerCase();
|
|
435
|
-
if (normalized.includes(" ")) return null;
|
|
436
|
-
const actions = [
|
|
437
|
-
{ value: "clear", description: "Remove the current goal" },
|
|
438
|
-
{ value: "edit", description: "Edit and reactivate the current goal" },
|
|
439
|
-
{ value: "pause", description: "Stop automatic continuation" },
|
|
440
|
-
{ value: "resume", description: "Resume automatic continuation" },
|
|
441
|
-
];
|
|
442
|
-
return actions
|
|
443
|
-
.filter((action) => action.value.startsWith(normalized))
|
|
444
|
-
.map((action) => ({ ...action, label: action.value }));
|
|
445
|
-
},
|
|
446
|
-
handler: async (args, ctx) => {
|
|
448
|
+
const handleGoalCommand = async (args: string, ctx: ExtensionCommandContext): Promise<void> => {
|
|
447
449
|
if (ctx.mode === "print" || ctx.mode === "json") {
|
|
448
450
|
ctx.ui.notify("/goal requires TUI or RPC mode", "error");
|
|
449
451
|
return;
|
|
@@ -461,7 +463,16 @@ export function registerGoal(
|
|
|
461
463
|
ctx.ui.notify("No goal is set. Use /goal <objective> to start a long-running task.", "info");
|
|
462
464
|
return;
|
|
463
465
|
}
|
|
464
|
-
ctx.
|
|
466
|
+
if (ctx.mode !== "tui") {
|
|
467
|
+
ctx.ui.notify(goalStatusSummary(runtime.state, ctx), "info");
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
const actions = goalPanelActions(runtime.state.status);
|
|
471
|
+
const selected = await ctx.ui.select(goalStatusSummary(runtime.state, ctx), actions.map((action) => action.label));
|
|
472
|
+
const action = actions.find((candidate) => candidate.label === selected);
|
|
473
|
+
if (!action) return;
|
|
474
|
+
if (action.control === "clear" && !await ctx.ui.confirm("Clear goal?", runtime.state.objective)) return;
|
|
475
|
+
await handleGoalCommand(action.control, ctx);
|
|
465
476
|
return;
|
|
466
477
|
}
|
|
467
478
|
|
|
@@ -684,7 +695,24 @@ export function registerGoal(
|
|
|
684
695
|
reportError(ctx, "Goal could not be started", error);
|
|
685
696
|
scheduleGoalContinuation(pi, runtime, initState, ctx);
|
|
686
697
|
}
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
pi.registerCommand("goal", {
|
|
701
|
+
description: "Set or view the goal for a long-running task",
|
|
702
|
+
getArgumentCompletions: (prefix) => {
|
|
703
|
+
const normalized = prefix.trimStart().toLocaleLowerCase();
|
|
704
|
+
if (normalized.includes(" ")) return null;
|
|
705
|
+
const actions = [
|
|
706
|
+
{ value: "clear", description: "Remove the current goal" },
|
|
707
|
+
{ value: "edit", description: "Edit and reactivate the current goal" },
|
|
708
|
+
{ value: "pause", description: "Stop automatic continuation" },
|
|
709
|
+
{ value: "resume", description: "Resume automatic continuation" },
|
|
710
|
+
];
|
|
711
|
+
return actions
|
|
712
|
+
.filter((action) => action.value.startsWith(normalized))
|
|
713
|
+
.map((action) => ({ ...action, label: action.value }));
|
|
687
714
|
},
|
|
715
|
+
handler: handleGoalCommand,
|
|
688
716
|
});
|
|
689
717
|
}
|
|
690
718
|
|
package/killeros/hooks.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { CONFIG_DIR_NAME, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { reportError } from "./errors.ts";
|
|
6
|
-
import { MAX_NODE_TIMER_MS } from "./
|
|
6
|
+
import { MAX_NODE_TIMER_MS } from "./limits.ts";
|
|
7
7
|
|
|
8
8
|
type KillerosHookEvent = "tool_call" | "tool_result" | "agent_settled";
|
|
9
9
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const MAX_NODE_TIMER_MS = 2_147_483_647;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { closeSync, existsSync, openSync, readFileSync, readSync } from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { StringDecoder } from "node:string_decoder";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { CONFIG_DIR_NAME, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
7
|
import type { InitRuntime } from "./runtime.ts";
|
|
@@ -14,7 +15,8 @@ function readBoundedText(filePath: string, limit = PERSONAL_INSTRUCTIONS_LIMIT):
|
|
|
14
15
|
descriptor = openSync(filePath, "r");
|
|
15
16
|
const buffer = Buffer.alloc(limit + 1);
|
|
16
17
|
const bytesRead = readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
17
|
-
const
|
|
18
|
+
const decoder = new StringDecoder("utf8");
|
|
19
|
+
const content = decoder.write(buffer.subarray(0, Math.min(bytesRead, limit)));
|
|
18
20
|
if (!content.trim()) return undefined;
|
|
19
21
|
return bytesRead > limit
|
|
20
22
|
? `${content}\n\n[Personal instructions truncated by KillerOS]`
|