killeros 3.0.4 → 3.0.6
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 +39 -0
- package/Killeros.ts +4 -2
- package/README.md +263 -94
- package/killeros/auto-compaction.ts +13 -1
- package/killeros/footer.ts +59 -23
- package/killeros/goal-settlement.ts +13 -1
- package/killeros/notifications.ts +2 -2
- package/killeros/question-ui.ts +173 -494
- package/killeros/question.ts +27 -20
- package/killeros/worked-for.ts +2 -2
- package/package.json +11 -9
|
@@ -35,6 +35,7 @@ export interface AutoCompactionDependencies {
|
|
|
35
35
|
loadPreference?: (ctx: ExtensionContext) => AutoCompactionPreference;
|
|
36
36
|
getCompactionSettings?: (ctx: ExtensionContext) => CompactionSettings;
|
|
37
37
|
settingsStore?: KillerosSettingsStore;
|
|
38
|
+
onPreferenceChange?: (preference: AutoCompactionPreference | undefined) => void;
|
|
38
39
|
goal?: AutoCompactionGoalHandlers;
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -142,10 +143,12 @@ export function registerAutoCompaction(
|
|
|
142
143
|
if (!argument || argument === "status") {
|
|
143
144
|
try {
|
|
144
145
|
const preference = readAutoCompactionPreference(settingsStore.load());
|
|
146
|
+
dependencies.onPreferenceChange?.(preference);
|
|
145
147
|
ctx.ui.notify(preference.enabled
|
|
146
148
|
? `Automatic compaction: on at ${preference.percentRemaining}% remaining`
|
|
147
149
|
: `Automatic compaction: off (threshold ${preference.percentRemaining}%)`, "info");
|
|
148
150
|
} catch (error) {
|
|
151
|
+
dependencies.onPreferenceChange?.(undefined);
|
|
149
152
|
ctx.ui.notify(`Automatic compaction setting could not be read: ${errorMessage(error)}`, "error");
|
|
150
153
|
}
|
|
151
154
|
return;
|
|
@@ -167,6 +170,7 @@ export function registerAutoCompaction(
|
|
|
167
170
|
return { autoCompaction: updated };
|
|
168
171
|
});
|
|
169
172
|
if (!updated) throw new Error("Settings update did not apply the preference");
|
|
173
|
+
dependencies.onPreferenceChange?.(updated);
|
|
170
174
|
ctx.ui.notify(percent === undefined
|
|
171
175
|
? updated.enabled
|
|
172
176
|
? `Automatic compaction: on at ${updated.percentRemaining}% remaining`
|
|
@@ -269,7 +273,9 @@ export function registerAutoCompaction(
|
|
|
269
273
|
try {
|
|
270
274
|
preference = loadPreference(ctx);
|
|
271
275
|
compactionSettings = getCompactionSettings(ctx);
|
|
276
|
+
dependencies.onPreferenceChange?.(preference);
|
|
272
277
|
} catch (error) {
|
|
278
|
+
dependencies.onPreferenceChange?.(undefined);
|
|
273
279
|
if (!settingsErrorReported) {
|
|
274
280
|
settingsErrorReported = true;
|
|
275
281
|
ctx.ui.notify(`Automatic compaction settings could not be read: ${errorMessage(error)}`, "error");
|
|
@@ -343,8 +349,14 @@ export function registerAutoCompaction(
|
|
|
343
349
|
}
|
|
344
350
|
});
|
|
345
351
|
|
|
346
|
-
pi.on("agent_settled", (
|
|
352
|
+
pi.on("agent_settled", (event, ctx) => {
|
|
347
353
|
goalRequestOwned = false;
|
|
354
|
+
// Pi aborts the agent when our pending compaction starts. Its callback owns
|
|
355
|
+
// recovery, including cancellation; the settlement flag alone is ambiguous.
|
|
356
|
+
if ("aborted" in event && event.aborted === true && request?.phase !== "compacting") {
|
|
357
|
+
request = undefined;
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
348
360
|
if (request?.phase === "continuation-dispatched") {
|
|
349
361
|
request = undefined;
|
|
350
362
|
notifyFailure(ctx, new Error("continuation was not accepted"));
|
package/killeros/footer.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { watch } from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { type ExtensionAPI, type ExtensionContext, type Theme, type ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { truncateToWidth, visibleWidth, type TUI } from "@earendil-works/pi-tui";
|
|
6
|
+
import { readAutoCompactionPreference, type AutoCompactionPreference } from "./auto-compaction.ts";
|
|
7
|
+
import { createKillerosSettingsStore, type KillerosSettingsStore } from "./settings.ts";
|
|
6
8
|
import { isCodexFastEnabled, isCodexFastModel, subscribeCodexFast } from "./codex-fast-state.ts";
|
|
7
9
|
import { formatCwd, formatTime, modelDisplayName, padRight } from "./display.ts";
|
|
8
10
|
import { inspectWorktreeWithoutFilters, passiveGitCommand, passiveGitEnv, type PassiveGitRunner } from "./passive-git-status.ts";
|
|
@@ -255,14 +257,33 @@ export function contextPercentRemaining(ctx: ExtensionContext): number | null {
|
|
|
255
257
|
return Math.round(Math.max(0, Math.min(100, percentRemaining)));
|
|
256
258
|
}
|
|
257
259
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
260
|
+
function validCapacity(value: number | undefined): value is number {
|
|
261
|
+
return value !== undefined && Number.isFinite(value) && value > 0;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function formatContextTokens(tokens: number): string {
|
|
265
|
+
const unit = tokens >= 1_000_000 ? 1_000_000 : tokens >= 1_000 ? 1_000 : 1;
|
|
266
|
+
// Floor labels so rounding never promises tokens that are not available.
|
|
267
|
+
const count = unit === 1 ? Math.floor(tokens) : Math.floor(tokens / (unit / 100)) / 100;
|
|
268
|
+
return `${count}${unit === 1_000_000 ? "M" : unit === 1_000 ? "K" : ""}`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function formatContextProgress(
|
|
272
|
+
tokensUsed: number | null,
|
|
273
|
+
contextWindow: number,
|
|
274
|
+
theme: Theme,
|
|
275
|
+
showManualHint = false,
|
|
276
|
+
detail: "full" | "capacity" | "percentage" = "full",
|
|
277
|
+
): string {
|
|
278
|
+
if (!validCapacity(contextWindow)) return theme.fg("dim", "ctx —%");
|
|
279
|
+
const capacity = detail === "percentage" ? "" : ` (${formatContextTokens(contextWindow)})`;
|
|
280
|
+
if (tokensUsed === null || !Number.isFinite(tokensUsed)) return theme.fg("dim", `ctx —%${capacity}`);
|
|
281
|
+
const remaining = Math.max(0, contextWindow - Math.max(0, tokensUsed));
|
|
282
|
+
const percentRemaining = Math.round(remaining / contextWindow * 100);
|
|
263
283
|
const color: ThemeColor = percentRemaining > 50 ? "success" : percentRemaining >= 20 ? "warning" : "error";
|
|
264
|
-
const
|
|
265
|
-
|
|
284
|
+
const counts = detail === "full" ? ` (${formatContextTokens(remaining)}/${formatContextTokens(contextWindow)})` : capacity;
|
|
285
|
+
const action = showManualHint && percentRemaining <= 15 ? " · /compact" : "";
|
|
286
|
+
return theme.fg(color, `ctx ${percentRemaining}%${counts}${action}`);
|
|
266
287
|
}
|
|
267
288
|
|
|
268
289
|
function sumSessionCost(ctx: ExtensionContext): number {
|
|
@@ -367,7 +388,16 @@ function formatGitFileChanges(changes: GitFileChanges, theme: Theme): string {
|
|
|
367
388
|
return `${theme.fg("dim", `±${total} [`)}${theme.fg("warning", `~${changes.modified}`)} ${theme.fg("success", `+${changes.added}`)} ${theme.fg("error", `−${changes.deleted}`)}${theme.fg("dim", "]")}`;
|
|
368
389
|
}
|
|
369
390
|
|
|
370
|
-
export function registerFooter(
|
|
391
|
+
export function registerFooter(
|
|
392
|
+
pi: ExtensionAPI,
|
|
393
|
+
goalRuntime: GoalRuntime,
|
|
394
|
+
settingsStore: KillerosSettingsStore = createKillerosSettingsStore(),
|
|
395
|
+
): (preference: AutoCompactionPreference | undefined) => void {
|
|
396
|
+
let autoCompactionEnabled: boolean | undefined;
|
|
397
|
+
const updatePreference = (preference: AutoCompactionPreference | undefined): void => {
|
|
398
|
+
autoCompactionEnabled = preference?.enabled;
|
|
399
|
+
activeTui?.requestRender();
|
|
400
|
+
};
|
|
371
401
|
let currentModel: ExtensionContext["model"];
|
|
372
402
|
let thinkingLevel: ThinkingLevel = "off";
|
|
373
403
|
let activeTui: TUI | undefined;
|
|
@@ -394,6 +424,11 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
|
|
|
394
424
|
pi.on("session_start", (_event, ctx) => {
|
|
395
425
|
resetSessionCost();
|
|
396
426
|
if (ctx.mode !== "tui") return;
|
|
427
|
+
try {
|
|
428
|
+
autoCompactionEnabled = readAutoCompactionPreference(settingsStore.load()).enabled;
|
|
429
|
+
} catch {
|
|
430
|
+
autoCompactionEnabled = undefined;
|
|
431
|
+
}
|
|
397
432
|
unsubscribeCodexFast?.();
|
|
398
433
|
unsubscribeCodexFast = subscribeCodexFast(() => activeTui?.requestRender());
|
|
399
434
|
const sessionStart = Date.now();
|
|
@@ -437,31 +472,31 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
|
|
|
437
472
|
} catch {
|
|
438
473
|
usage = undefined;
|
|
439
474
|
}
|
|
440
|
-
const contextWindow = usage?.contextWindow
|
|
441
|
-
|
|
475
|
+
const contextWindow = validCapacity(usage?.contextWindow)
|
|
476
|
+
? usage.contextWindow : validCapacity(model?.contextWindow) ? model.contextWindow : Number.NaN;
|
|
477
|
+
const contexts = (["full", "capacity", "percentage"] as const).map((detail) =>
|
|
478
|
+
formatContextProgress(usage?.tokens ?? null, contextWindow, theme, autoCompactionEnabled === false, detail));
|
|
442
479
|
const branch = footerData.getGitBranch();
|
|
443
480
|
const signature = formatModel(model, theme, true, isCodexFastEnabled());
|
|
444
481
|
const fullDirectory = colorDirectory(cwd);
|
|
445
482
|
const focusedDirectory = colorDirectory(compactDirectory(cwd));
|
|
446
483
|
const goal = formatGoalFooter(goalRuntime.state, theme);
|
|
447
|
-
const primary = joinFooterParts([
|
|
448
|
-
|
|
449
|
-
level,
|
|
450
|
-
context,
|
|
451
|
-
], theme);
|
|
452
|
-
const primaryFocused = joinFooterParts([signature, context], theme);
|
|
484
|
+
const primary = contexts.map((context) => joinFooterParts([signature, level, context], theme));
|
|
485
|
+
const primaryFocused = contexts.map((context) => joinFooterParts([signature, context], theme));
|
|
453
486
|
const session = joinFooterParts([
|
|
454
487
|
theme.fg("dim", formatTime(Date.now() - sessionStart)),
|
|
455
488
|
theme.fg("dim", formatCost(getSessionCost(ctx))),
|
|
456
489
|
], theme);
|
|
457
490
|
const essentialModel = formatModel(model, theme, false, isCodexFastEnabled());
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
491
|
+
const candidates = [
|
|
492
|
+
{ left: primary[0] ?? "", right: session },
|
|
493
|
+
...primary.map((left) => ({ left, right: "" })),
|
|
494
|
+
...primaryFocused.map((left) => ({ left, right: "" })),
|
|
495
|
+
...contexts.map((right) => ({ left: essentialModel, right })),
|
|
496
|
+
];
|
|
497
|
+
const selected = candidates.find(({ left, right }) => footerRowFits(left, right, width))
|
|
498
|
+
?? { left: essentialModel, right: contexts[2] ?? "" };
|
|
499
|
+
const primaryRow = renderFooterRow(selected.left, selected.right, width);
|
|
465
500
|
const changes = gitFileChanges ? formatGitFileChanges(gitFileChanges, theme) : "";
|
|
466
501
|
const branchLabel = branch
|
|
467
502
|
? `${theme.fg("dim", branch)}${changes ? `${theme.fg("dim", " · ")}${changes}` : ""}`
|
|
@@ -501,4 +536,5 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
|
|
|
501
536
|
activeTui = undefined;
|
|
502
537
|
goalRuntime.requestRender = undefined;
|
|
503
538
|
});
|
|
539
|
+
return updatePreference;
|
|
504
540
|
}
|
|
@@ -219,7 +219,7 @@ export function registerGoalSettlement(
|
|
|
219
219
|
pi: ExtensionAPI,
|
|
220
220
|
runtime: GoalRuntime,
|
|
221
221
|
): AutoCompactionGoalHandlers {
|
|
222
|
-
pi.on("agent_settled", (
|
|
222
|
+
pi.on("agent_settled", (event, ctx) => {
|
|
223
223
|
runtime.blockedCompletion = undefined;
|
|
224
224
|
syncGoalUpdateTool(pi, runtime);
|
|
225
225
|
const wasGoalTurn = runtime.goalTurnInFlight;
|
|
@@ -230,6 +230,18 @@ export function registerGoalSettlement(
|
|
|
230
230
|
runtime.agentEndObserved = false;
|
|
231
231
|
runtime.continuationScheduled = false;
|
|
232
232
|
|
|
233
|
+
// ctx.compact() also aborts the request. An owned compaction must settle through
|
|
234
|
+
// its success/error callback; its cancellation cannot authorize recovery.
|
|
235
|
+
if ("aborted" in event && event.aborted === true && !runtime.automaticCompaction
|
|
236
|
+
&& runtime.lastStopReason !== "aborted") {
|
|
237
|
+
runtime.lastStopReason = undefined;
|
|
238
|
+
runtime.lastError = undefined;
|
|
239
|
+
if (runtime.state?.status === "active") {
|
|
240
|
+
pauseGoalAfterFailure(pi, runtime, ctx, "the agent request was cancelled");
|
|
241
|
+
}
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
233
245
|
if (runtime.automaticCompaction) {
|
|
234
246
|
const stopReason = runtime.lastStopReason;
|
|
235
247
|
const error = safeTerminalText(runtime.lastError ?? "");
|
|
@@ -116,11 +116,11 @@ export function registerCompletionNotifications(
|
|
|
116
116
|
}
|
|
117
117
|
});
|
|
118
118
|
|
|
119
|
-
pi.on("agent_settled", (
|
|
119
|
+
pi.on("agent_settled", (event, ctx) => {
|
|
120
120
|
if (!requestPending || ctx.mode !== "tui") return;
|
|
121
121
|
if (!ctx.isIdle() || ctx.hasPendingMessages()) return;
|
|
122
122
|
requestPending = false;
|
|
123
|
-
if (!enabled || lastStopReason === "aborted") return;
|
|
123
|
+
if (!enabled || "aborted" in event && event.aborted === true || lastStopReason === "aborted") return;
|
|
124
124
|
try {
|
|
125
125
|
runtime.ring();
|
|
126
126
|
} catch (error) {
|