killeros 2.1.23 → 2.1.24

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 CHANGED
@@ -4,6 +4,17 @@ All notable changes to KillerOS are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.1.24] - 2026-09-05
8
+
9
+ ### Added
10
+
11
+ - Appended the settled model display name to version 4 worked-for receipts for single-model TUI runs.
12
+
13
+ ### Changed
14
+
15
+ - Rendered settled `Done` receipts without the `✓` marker, keeping `■ Stopped`, `× Failed`, and version 1 history unchanged.
16
+ - Raised the locked Pi development packages and minimum supported Pi peer version to 0.85.0, including `@earendil-works/pi-server`.
17
+
7
18
  ## [2.1.23] - 2026-09-04
8
19
 
9
20
  ### Changed
package/README.md CHANGED
@@ -19,7 +19,7 @@ A TypeScript extension for the [Pi coding agent](https://github.com/earendil-wor
19
19
  ## Requirements
20
20
 
21
21
  - Node.js 22.19.0+
22
- - Pi 0.84.3 or later within the 0.x release line
22
+ - Pi 0.85.0 or later within the 0.x release line
23
23
  - An interactive TUI session for the custom header, editor, footer, `question`, and `/init`
24
24
 
25
25
  ## Install
@@ -34,7 +34,7 @@ Or from GitHub:
34
34
  pi install git:github.com/KyrosHendrix/pi-KillerOS
35
35
  ```
36
36
 
37
- Pin a release by appending its tag, for example `@v2.1.23`. Add `-l` to install only for the current project. Restart Pi after installing.
37
+ Pin a release by appending its tag, for example `@v2.1.24`. Add `-l` to install only for the current project. Restart Pi after installing.
38
38
 
39
39
  ## Commands
40
40
 
@@ -51,3 +51,10 @@ export function formatTokens(value: number): string {
51
51
  if (inK >= 1_000) return `${Number((rounded / 1_000_000).toFixed(1))}M`;
52
52
  return `${inK}k`;
53
53
  }
54
+
55
+ /** Resolves a terminal-safe model display name, preferring the name over the id. */
56
+ export function modelDisplayName(model: { name?: string; id?: string }): string {
57
+ const name = safeTerminalText(model.name ?? "").replaceAll("\n", "").trim();
58
+ if (name) return name;
59
+ return safeTerminalText(model.id ?? "").replaceAll("\n", "").trim();
60
+ }
@@ -3,7 +3,7 @@ import { watch } from "node:fs";
3
3
  import { type ExtensionAPI, type ExtensionContext, type Theme, type ThemeColor } from "@earendil-works/pi-coding-agent";
4
4
  import { truncateToWidth, visibleWidth, type TUI } from "@earendil-works/pi-tui";
5
5
  import { isCodexFastEnabled, subscribeCodexFast } from "./codex-fast-state.ts";
6
- import { formatCwd, formatTime, formatTokens, padRight } from "./display.ts";
6
+ import { formatCwd, formatTime, formatTokens, modelDisplayName, padRight } from "./display.ts";
7
7
  import { goalElapsedMilliseconds } from "./goal-state.ts";
8
8
  import type { GoalRuntime, GoalState } from "./runtime.ts";
9
9
  import { safeTerminalText } from "./safe-terminal-text.ts";
@@ -296,11 +296,6 @@ function formatProviderName(provider: string): string {
296
296
  .join(" ") || "Unknown provider";
297
297
  }
298
298
 
299
- function modelDisplayName(model: NonNullable<ExtensionContext["model"]>): string {
300
- const name = safeTerminalText(model.name ?? "").replaceAll("\n", "").trim();
301
- return name || safeTerminalText(model.id).replaceAll("\n", "").trim() || "Unknown model";
302
- }
303
-
304
299
  export function formatModel(
305
300
  model: ExtensionContext["model"],
306
301
  theme: Theme,
@@ -308,7 +303,7 @@ export function formatModel(
308
303
  showCodexFast = false,
309
304
  ): string {
310
305
  if (!model) return theme.fg("dim", "No model");
311
- const name = theme.fg("text", theme.bold(modelDisplayName(model)));
306
+ const name = theme.fg("text", theme.bold(modelDisplayName(model) || "Unknown model"));
312
307
  const fast = showCodexFast && model.provider === CODEX_PROVIDER
313
308
  ? theme.fg("accent", theme.bold("Fast"))
314
309
  : "";
@@ -12,7 +12,7 @@ import {
12
12
  type ChangedFile,
13
13
  type CheckAttempt,
14
14
  } from "./change-receipt.ts";
15
- import { formatTokens } from "./display.ts";
15
+ import { formatTokens, modelDisplayName } from "./display.ts";
16
16
  import { errorMessage } from "./errors.ts";
17
17
  import { safeTerminalText } from "./safe-terminal-text.ts";
18
18
 
@@ -47,15 +47,16 @@ export interface WorkedForEntryDataV4 {
47
47
  changes: ChangeSummary;
48
48
  checks: CheckAttempt[];
49
49
  omittedChecks: { passed: number; failed: number };
50
+ model?: string;
50
51
  }
51
52
 
52
53
  type WorkedForEntryData = WorkedForEntryDataV1 | WorkedForEntryDataV2 | WorkedForEntryDataV3 | WorkedForEntryDataV4;
53
54
 
54
55
  const OUTCOMES = {
55
- done: { marker: "✓", label: "Done", color: "success" },
56
- stopped: { marker: "■", label: "Stopped", color: "warning" },
57
- failed: { marker: "×", label: "Failed", color: "error" },
58
- } as const satisfies Record<WorkedForOutcome, { marker: string; label: string; color: string }>;
56
+ done: { label: "Done", color: "success" },
57
+ stopped: { label: "■ Stopped", color: "warning" },
58
+ failed: { label: "× Failed", color: "error" },
59
+ } as const satisfies Record<WorkedForOutcome, { label: string; color: string }>;
59
60
 
60
61
  function isWorkedForOutcome(value: unknown): value is WorkedForOutcome {
61
62
  return value === "done" || value === "stopped" || value === "failed";
@@ -121,6 +122,12 @@ function parseChanges(value: unknown): ChangeSummary | undefined {
121
122
  };
122
123
  }
123
124
 
125
+ function parseModelName(value: unknown): string | undefined {
126
+ if (typeof value !== "string" || value.length > 200) return undefined;
127
+ const sanitized = safeTerminalText(value).replaceAll("\n", "").trim();
128
+ return sanitized || undefined;
129
+ }
130
+
124
131
  function parseV4(data: Record<string, unknown>): WorkedForEntryDataV4 | undefined {
125
132
  try {
126
133
  if (Buffer.byteLength(JSON.stringify(data), "utf8") > MAX_PAYLOAD_BYTES) return undefined;
@@ -139,6 +146,7 @@ function parseV4(data: Record<string, unknown>): WorkedForEntryDataV4 | undefine
139
146
  if (!label) return undefined;
140
147
  checks.push({ label, outcome: check.outcome });
141
148
  }
149
+ const model = parseModelName(data.model);
142
150
  return {
143
151
  version: 4,
144
152
  milliseconds: data.milliseconds,
@@ -147,6 +155,7 @@ function parseV4(data: Record<string, unknown>): WorkedForEntryDataV4 | undefine
147
155
  changes,
148
156
  checks,
149
157
  omittedChecks: { passed: data.omittedChecks.passed, failed: data.omittedChecks.failed },
158
+ ...(model ? { model } : {}),
150
159
  };
151
160
  }
152
161
 
@@ -210,8 +219,10 @@ class WorkedForV4Component implements Component {
210
219
  if (width <= 0) return [];
211
220
  const { data, theme } = this;
212
221
  const outcome = OUTCOMES[data.outcome];
222
+ const headline = theme.fg(outcome.color, outcome.label);
223
+ const modelSuffix = data.model ? ` · ${data.model}` : "";
213
224
  const lines = [
214
- `${theme.fg(outcome.color, `${outcome.marker} ${outcome.label}`)}${theme.fg("dim", ` · ${formatWorkedForDuration(data.milliseconds)} · ↑ ${formatTokens(data.tokens)} tokens`)}`,
225
+ `${headline}${theme.fg("dim", ` · ${formatWorkedForDuration(data.milliseconds)} · ↑ ${formatTokens(data.tokens)} tokens${modelSuffix}`)}`,
215
226
  ];
216
227
  if (data.changes.state === "unavailable") lines.push(theme.fg("dim", " Changes unavailable"));
217
228
  else if (data.changes.totalFiles === 0) lines.push(theme.fg("dim", " No files changed"));
@@ -272,8 +283,22 @@ type ActiveReceipt = {
272
283
  collection: Promise<ChangeReceiptCollection>;
273
284
  checks: CheckAttempt[];
274
285
  omittedChecks: { passed: number; failed: number };
286
+ modelProvider: string | undefined;
287
+ modelId: string | undefined;
288
+ modelMismatch: boolean;
275
289
  };
276
290
 
291
+ function receiptModelName(
292
+ settled: ActiveReceipt,
293
+ model: ExtensionContext["model"],
294
+ ): string | undefined {
295
+ if (settled.modelMismatch || settled.modelProvider === undefined || settled.modelId === undefined) return undefined;
296
+ if (!model || model.provider !== settled.modelProvider || model.id !== settled.modelId) return undefined;
297
+ const resolved = modelDisplayName(model);
298
+ if (!resolved || resolved.length > 200) return undefined;
299
+ return resolved;
300
+ }
301
+
277
302
  function fitPayload(data: WorkedForEntryDataV4): WorkedForEntryDataV4 {
278
303
  if (data.changes.state === "unavailable") return data;
279
304
  const changes = { ...data.changes, files: [...data.changes.files] };
@@ -300,7 +325,8 @@ export function registerWorkedFor(
300
325
  if (data.version === 4) return new WorkedForV4Component(data, options.expanded, theme);
301
326
  const outcome = OUTCOMES[data.outcome];
302
327
  const tokens = data.version === 3 ? ` · ↑ ${formatTokens(data.tokens)} tokens` : "";
303
- return new Text(`${theme.fg(outcome.color, `${outcome.marker} ${outcome.label}`)}${theme.fg("dim", ` · ${formatWorkedForDuration(data.milliseconds)}${tokens}`)}`, 0, 0);
328
+ const headline = theme.fg(outcome.color, outcome.label);
329
+ return new Text(`${headline}${theme.fg("dim", ` · ${formatWorkedForDuration(data.milliseconds)}${tokens}`)}`, 0, 0);
304
330
  });
305
331
 
306
332
  pi.on("session_start", async () => {
@@ -319,12 +345,35 @@ export function registerWorkedFor(
319
345
  collection: collect(ctx.cwd),
320
346
  checks: [],
321
347
  omittedChecks: { passed: 0, failed: 0 },
348
+ modelProvider: undefined,
349
+ modelId: undefined,
350
+ modelMismatch: false,
322
351
  };
323
352
  active = state;
324
353
  const collection = await state.collection;
325
354
  if (active !== state) await collection.dispose();
326
355
  });
327
356
 
357
+ pi.on("message_end", (event, ctx) => {
358
+ if (ctx.mode !== "tui" || !active) return;
359
+ if (event.message.role !== "assistant") return;
360
+ const provider: unknown = event.message.provider;
361
+ const modelId: unknown = event.message.model;
362
+ if (typeof provider !== "string" || typeof modelId !== "string") {
363
+ active.modelMismatch = true;
364
+ return;
365
+ }
366
+ if (active.modelProvider === undefined || active.modelId === undefined) {
367
+ active.modelProvider = provider;
368
+ active.modelId = modelId;
369
+ } else if (active.modelProvider !== provider || active.modelId !== modelId) {
370
+ active.modelMismatch = true;
371
+ }
372
+ if (event.message.responseModel !== undefined && event.message.responseModel !== modelId) {
373
+ active.modelMismatch = true;
374
+ }
375
+ });
376
+
328
377
  pi.on("tool_result", (event: ToolResultEvent, ctx) => {
329
378
  if (ctx.mode !== "tui" || !active || event.toolName !== "bash" && event.toolName !== "powershell") return;
330
379
  const check = recognizedCheck(event.input.command, event.isError);
@@ -347,6 +396,7 @@ export function registerWorkedFor(
347
396
  if (ctx.mode !== "tui" || !active) return;
348
397
  const settled = active;
349
398
  active = undefined;
399
+ const model = receiptModelName(settled, ctx.model);
350
400
  const changes = await (await settled.collection).finish();
351
401
  if (changes.state === "unavailable" && changes.reason !== "not-git" && changes.reason !== "timeout" && !collectionNoticeShown) {
352
402
  collectionNoticeShown = true;
@@ -361,6 +411,7 @@ export function registerWorkedFor(
361
411
  changes,
362
412
  checks: settled.checks,
363
413
  omittedChecks: settled.omittedChecks,
414
+ ...(model ? { model } : {}),
364
415
  });
365
416
  try {
366
417
  pi.appendEntry<WorkedForEntryDataV4>(WORKED_FOR_ENTRY_TYPE, data);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "killeros",
3
- "version": "2.1.23",
3
+ "version": "2.1.24",
4
4
  "description": "TUI, goals, and workflow automation for the Pi coding agent",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -44,15 +44,16 @@
44
44
  ]
45
45
  },
46
46
  "peerDependencies": {
47
- "@earendil-works/pi-ai": ">=0.84.3 <1",
48
- "@earendil-works/pi-coding-agent": ">=0.84.3 <1",
49
- "@earendil-works/pi-tui": ">=0.84.3 <1",
47
+ "@earendil-works/pi-ai": ">=0.85.0 <1",
48
+ "@earendil-works/pi-coding-agent": ">=0.85.0 <1",
49
+ "@earendil-works/pi-tui": ">=0.85.0 <1",
50
50
  "typebox": ">=1.1.38 <2"
51
51
  },
52
52
  "devDependencies": {
53
- "@earendil-works/pi-ai": "0.84.3",
54
- "@earendil-works/pi-coding-agent": "0.84.3",
55
- "@earendil-works/pi-tui": "0.84.3",
53
+ "@earendil-works/pi-ai": "0.85.0",
54
+ "@earendil-works/pi-coding-agent": "0.85.0",
55
+ "@earendil-works/pi-server": "0.85.0",
56
+ "@earendil-works/pi-tui": "0.85.0",
56
57
  "@types/node": "24.12.4",
57
58
  "eslint": "^10.9.1",
58
59
  "typebox": "1.3.20",