killeros 2.1.24 → 2.1.25

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,24 @@ All notable changes to KillerOS are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.1.25] - 2026-09-06
8
+
9
+ ### Fixed
10
+
11
+ - Left-aligned settled `✓ Done` receipt lines so every row shares the assistant text edge.
12
+
13
+ ### Changed
14
+
15
+ - Replaced the boxed startup card with a three-row masthead: coral fade mark, versions, model and reasoning level, directory and branch, and an italic tip.
16
+ - Swapped the working indicator to the 10-frame Braille orbit at 80 ms per frame.
17
+ - Restored the leading `❯` prompt marker in white in the chat editor.
18
+ - Rendered footer model names and the `fast` badge in non-bold white.
19
+ - Showed the current Pi version beside the KillerOS version in the startup header.
20
+
21
+ ### Removed
22
+
23
+ - Removed the `/variants` reasoning-level command in favor of Pi's native `/thinking` selector.
24
+
7
25
  ## [2.1.24] - 2026-09-05
8
26
 
9
27
  ### Added
package/Killeros.ts CHANGED
@@ -22,7 +22,6 @@ import { registerPersonalInstructions } from "./killeros/personal-instructions.t
22
22
  import { registerQuestionTool } from "./killeros/question.ts";
23
23
  import { createGoalRuntime, createInitRuntime } from "./killeros/runtime.ts";
24
24
  import { registerShellUi } from "./killeros/shell-ui.ts";
25
- import { registerVariants } from "./killeros/variants.ts";
26
25
  import { registerWorkedFor } from "./killeros/worked-for.ts";
27
26
 
28
27
  export { contextPercentRemaining, formatCost, formatContextProgress } from "./killeros/footer.ts";
@@ -49,7 +48,6 @@ export default function Killeros(pi: ExtensionAPI, options: KillerosOptions = {}
49
48
  registerHandoff(pi, goalRuntime, options.handoffMaxTokens);
50
49
  registerSlashAutocomplete(pi, commandResolver);
51
50
  registerFooter(pi, goalRuntime);
52
- registerVariants(pi);
53
51
  registerCodexFastMode(pi);
54
52
  registerInitCommand(pi, initRuntime, goalRuntime);
55
53
  registerLifecycleHooks(pi);
package/README.md CHANGED
@@ -4,10 +4,9 @@ A TypeScript extension for the [Pi coding agent](https://github.com/earendil-wor
4
4
 
5
5
  ## What you get
6
6
 
7
- - A custom TUI: startup card with version, model, provider, working directory, and Git branch; a dark theme with coral accents; a multiline editor with slash-command completion; a footer that tracks model, context, and goal state; settled task receipts with duration and token usage.
7
+ - A custom TUI: startup masthead with versions, model, working directory, and Git branch; a dark theme with coral accents; a multiline editor with slash-command completion; a footer that tracks model, context, and goal state; settled task receipts with duration and token usage.
8
8
  - `/goal`: set an objective and Pi keeps working toward it across turns, compaction, reloads, and branch navigation. New goals pause after 20 turns; `/goal resume` grants another 20.
9
9
  - `/init`: generates a root `AGENTS.md` from repository evidence, preserving compatible existing rules.
10
- - `/variants`: pick a reasoning level supported by the active model.
11
10
  - `/codex-fast`: toggles the `priority` service tier on Codex requests.
12
11
  - `/handoff`: starts a fresh linked session carrying visible continuation context.
13
12
  - Automatic context compaction when remaining tokens drop below 15% of the window (configurable).
@@ -34,7 +33,7 @@ Or from GitHub:
34
33
  pi install git:github.com/KyrosHendrix/pi-KillerOS
35
34
  ```
36
35
 
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.
36
+ Pin a release by appending its tag, for example `@v2.1.25`. Add `-l` to install only for the current project. Restart Pi after installing.
38
37
 
39
38
  ## Commands
40
39
 
@@ -45,7 +44,6 @@ Pin a release by appending its tag, for example `@v2.1.24`. Add `-l` to install
45
44
  /goal pause Stop automatic continuation
46
45
  /goal resume Resume automatic continuation
47
46
  /goal clear Remove the current goal
48
- /variants Reasoning-level selector (/variants high sets directly)
49
47
  /codex-fast Toggle Codex fast mode
50
48
  /notification Configure the completion sound
51
49
  /handoff [focus] Fresh session with continuation context
@@ -72,7 +72,6 @@ const BUILTIN_COMMANDS: ReadonlyArray<{ name: string; description: string }> = [
72
72
  const COMMAND_SYNTAX_HINTS: Readonly<Record<string, string>> = {
73
73
  goal: "/goal [objective|pause|resume|clear]",
74
74
  handoff: "/handoff [next-session focus]",
75
- variants: "/variants [level]",
76
75
  model: "/model [provider/model]",
77
76
  "scoped-models": "/scoped-models",
78
77
  login: "/login [provider]",
@@ -52,9 +52,9 @@ export function formatTokens(value: number): string {
52
52
  return `${inK}k`;
53
53
  }
54
54
 
55
- /** Resolves a terminal-safe model display name, preferring the name over the id. */
55
+ /** Resolves a lowercase model id for display, falling back to the name. */
56
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();
57
+ const id = safeTerminalText(model.id ?? "").replaceAll("\n", "").trim();
58
+ if (id) return id.toLowerCase();
59
+ return safeTerminalText(model.name ?? "").replaceAll("\n", "").trim().toLowerCase();
60
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, modelDisplayName, padRight } from "./display.ts";
6
+ import { formatCwd, formatTime, 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";
@@ -14,7 +14,7 @@ const GIT_STATUS_TIMEOUT_MS = 5_000;
14
14
  const GIT_STATUS_WATCH_DEBOUNCE_MS = 250;
15
15
  const GIT_STATUS_WATCH_INTERVAL_MS = 5_000;
16
16
  const CODEX_PROVIDER = "openai-codex";
17
- const colorDirectory = (text: string): string => `\x1B[38;2;240;248;154m${text}\x1B[39m`;
17
+ export const colorDirectory = (text: string): string => `\x1B[38;2;240;248;154m${text}\x1B[39m`;
18
18
 
19
19
  export interface GitFileChanges {
20
20
  modified: number;
@@ -239,13 +239,13 @@ export function contextPercentRemaining(ctx: ExtensionContext): number | null {
239
239
  }
240
240
 
241
241
  export function formatContextProgress(tokensUsed: number | null, contextWindow: number, theme: Theme): string {
242
- if (tokensUsed === null || !Number.isFinite(tokensUsed)) return theme.fg("dim", "—% left (—)");
242
+ if (tokensUsed === null || !Number.isFinite(tokensUsed)) return theme.fg("dim", "ctx —%");
243
243
  const windowSize = Number.isFinite(contextWindow) && contextWindow > 0 ? contextWindow : 128_000;
244
- const remaining = Math.max(0, Math.min(windowSize, windowSize - Math.max(0, tokensUsed)));
245
- const percentLeft = Math.max(0, Math.min(100, Math.round((remaining / windowSize) * 100)));
246
- const color: ThemeColor = percentLeft < 20 ? "error" : percentLeft <= 50 ? "warning" : "success";
247
- const action = percentLeft < 15 ? " · /compact" : "";
248
- return theme.fg(color, `${percentLeft}% left (${formatTokens(remaining)})${action}`);
244
+ const used = Math.max(0, Math.min(windowSize, Math.max(0, tokensUsed)));
245
+ const percentUsed = Math.max(0, Math.min(100, Math.round((used / windowSize) * 100)));
246
+ const color: ThemeColor = percentUsed > 80 ? "error" : percentUsed >= 50 ? "warning" : "success";
247
+ const action = percentUsed >= 85 ? " · /compact" : "";
248
+ return theme.fg(color, `ctx ${percentUsed}%${action}`);
249
249
  }
250
250
 
251
251
  function sumSessionCost(ctx: ExtensionContext): number {
@@ -275,25 +275,11 @@ const PROVIDER_LABELS: Readonly<Record<string, string>> = {
275
275
  openrouter: "OpenRouter",
276
276
  };
277
277
 
278
- const PROVIDER_WORDS: Readonly<Record<string, string>> = {
279
- ai: "AI",
280
- api: "API",
281
- deepseek: "DeepSeek",
282
- github: "GitHub",
283
- llm: "LLM",
284
- openai: "OpenAI",
285
- openrouter: "OpenRouter",
286
- };
287
-
288
278
  function formatProviderName(provider: string): string {
289
279
  const normalized = safeTerminalText(provider).replaceAll("\n", "").trim();
290
280
  const known = PROVIDER_LABELS[normalized.toLowerCase()];
291
- if (known) return known;
292
- return normalized
293
- .split(/[-_]+/u)
294
- .filter(Boolean)
295
- .map((word) => PROVIDER_WORDS[word.toLowerCase()] ?? `${word.charAt(0).toUpperCase()}${word.slice(1)}`)
296
- .join(" ") || "Unknown provider";
281
+ if (known) return known.toLowerCase();
282
+ return normalized.toLowerCase().split(/[-_]+/u).filter(Boolean).join(" ") || "unknown provider";
297
283
  }
298
284
 
299
285
  export function formatModel(
@@ -302,11 +288,10 @@ export function formatModel(
302
288
  includeProvider = true,
303
289
  showCodexFast = false,
304
290
  ): string {
305
- if (!model) return theme.fg("dim", "No model");
306
- const name = theme.fg("text", theme.bold(modelDisplayName(model) || "Unknown model"));
307
- const fast = showCodexFast && model.provider === CODEX_PROVIDER
308
- ? theme.fg("accent", theme.bold("Fast"))
309
- : "";
291
+ if (!model) return theme.fg("dim", "no model");
292
+ const displayName = modelDisplayName(model) || "unknown model";
293
+ const name = theme.fg("text", displayName);
294
+ const fast = showCodexFast && model.provider === CODEX_PROVIDER ? theme.fg("text", "fast") : "";
310
295
  const provider = includeProvider ? theme.fg("dim", formatProviderName(model.provider)) : "";
311
296
  return [name, fast, provider].filter(Boolean).join(" ");
312
297
  }
@@ -2,6 +2,7 @@ import { execFile } from "node:child_process";
2
2
  import { readFileSync } from "node:fs";
3
3
  import {
4
4
  CustomEditor,
5
+ VERSION as PI_VERSION,
5
6
  type ExtensionAPI,
6
7
  type ExtensionContext,
7
8
  type KeybindingsManager,
@@ -11,22 +12,18 @@ import {
11
12
  CURSOR_MARKER,
12
13
  stripTerminalSequences,
13
14
  truncateToWidth,
14
- visibleWidth,
15
15
  wrapTextWithAnsi,
16
16
  type EditorTheme,
17
17
  type TUI,
18
18
  } from "@earendil-works/pi-tui";
19
- import { formatCwd, padRight } from "./display.ts";
19
+ import { formatCwd, modelDisplayName, padRight } from "./display.ts";
20
+ import { colorDirectory } from "./footer.ts";
20
21
  import {
21
22
  createSlashCommandResolver,
22
23
  findSlashCommandTokens,
23
24
  type SlashCommandResolver,
24
25
  } from "./commands.ts";
25
26
  import { reportError } from "./errors.ts";
26
- import { formatModel } from "./footer.ts";
27
- import { LEVEL_COLORS } from "./variants.ts";
28
-
29
- const COMPACT_HEADER_MAX_WIDTH = 52;
30
27
 
31
28
  function readPackageVersion(path: string | URL): string | undefined {
32
29
  try {
@@ -46,7 +43,7 @@ const KILLEROS_VERSION = readPackageVersion(new URL("../package.json", import.me
46
43
 
47
44
  const STARTUP_TIPS = [
48
45
  "Press Shift+Enter to insert a line break without sending.",
49
- "Run /variants to tune the model's reasoning depth.",
46
+ "Run /thinking to tune the model's reasoning depth.",
50
47
  "Type / to browse every command available in this session.",
51
48
  "Run /notification to enable a terminal bell when work settles.",
52
49
  "Run /goal <objective> to keep long-running work moving across turns.",
@@ -119,10 +116,8 @@ function nextEditorSuggestion(): string {
119
116
  return editorSuggestionDeck.pop() ?? EDITOR_SUGGESTIONS[0];
120
117
  }
121
118
 
122
- function compactBoxLine(content: string, width: number, theme: Theme): string {
123
- if (width < 4) return truncateToWidth(content, width, "");
124
- return `${theme.fg("dim", "│")} ${padRight(content, width - 4)} ${theme.fg("dim", "│")}`;
125
- }
119
+ const MASTHEAD_MARK = ["██████", "▓▓▓▓▓▓", "░░░░░░"];
120
+ const MASTHEAD_GAP = " ";
126
121
 
127
122
  class PiStartupHeader {
128
123
  private readonly pi: ExtensionAPI;
@@ -145,10 +140,8 @@ class PiStartupHeader {
145
140
  }
146
141
 
147
142
  private tipLines(width: number, theme: Theme): string[] {
148
- const indent = " ";
149
- const text = `${theme.fg("text", theme.bold("Tip:"))}${theme.fg("dim", ` ${this.tip}`)}`;
150
- return wrapTextWithAnsi(text, width - indent.length)
151
- .map((line) => padRight(`${indent}${line}`, width));
143
+ const text = theme.italic(`${theme.fg("text", theme.bold("Tip:"))}${theme.fg("dim", ` ${this.tip}`)}`);
144
+ return wrapTextWithAnsi(text, width);
152
145
  }
153
146
 
154
147
  render(width: number): string[] {
@@ -156,34 +149,18 @@ class PiStartupHeader {
156
149
  const theme = this.ctx.ui.theme;
157
150
  if (width < 28) return [truncateToWidth(theme.fg("text", theme.bold("KillerOS")), width, "")];
158
151
 
159
- const panelWidth = Math.min(width, COMPACT_HEADER_MAX_WIDTH);
160
- const innerWidth = panelWidth - 4;
161
- const version = KILLEROS_VERSION ? theme.fg("dim", ` (v${KILLEROS_VERSION})`) : "";
162
- const identity = `${theme.fg("dim", "›")} ${theme.fg("text", theme.bold("KillerOS"))}${version}`;
152
+ const textWidth = Math.max(0, width - MASTHEAD_MARK[0].length - MASTHEAD_GAP.length);
153
+ const identity = `${theme.fg("text", "Pi")}${theme.fg("dim", ` ${PI_VERSION} | `)}${theme.fg("text", "KillerOS")}${KILLEROS_VERSION ? theme.fg("dim", ` ${KILLEROS_VERSION}`) : ""}`;
163
154
  const thinkingLevel = this.pi.getThinkingLevel();
164
- const reasoning = this.ctx.model?.reasoning === false
165
- ? theme.fg("thinkingOff", "no reasoning")
166
- : theme.fg(LEVEL_COLORS[thinkingLevel], thinkingLevel);
167
- const agent = `${formatModel(this.ctx.model, theme)}${theme.fg("dim", " · ")}${reasoning}`;
155
+ const reasoning = this.ctx.model?.reasoning === false ? "no reasoning" : thinkingLevel;
156
+ const modelName = this.ctx.model ? modelDisplayName(this.ctx.model) || "unknown model" : "no model";
157
+ const agent = `${theme.fg("dim", "model: ")}${theme.fg("text", `${modelName} ${reasoning}`)}`;
168
158
  const directory = formatCwd(this.ctx.cwd);
169
- const repository = this.branch
170
- ? `${directory} ${theme.fg("dim", ${this.branch}`)}`
171
- : directory;
172
- const modelCommand = theme.fg("mdLink", "/model");
173
- const agentWidth = Math.max(0, innerWidth - visibleWidth(modelCommand) - 1);
174
- const agentCommand = `${truncateToWidth(agent, agentWidth, "…")} ${modelCommand}`;
175
- const border = (left: string, right: string): string => theme.fg("dim", `${left}${"─".repeat(panelWidth - 2)}${right}`);
176
- const lines = [
177
- border("╭", "╮"),
178
- compactBoxLine(identity, panelWidth, theme),
179
- compactBoxLine("", panelWidth, theme),
180
- compactBoxLine(agentCommand, panelWidth, theme),
181
- compactBoxLine(repository, panelWidth, theme),
182
- border("╰", "╯"),
183
- " ".repeat(panelWidth),
184
- ...this.tipLines(panelWidth, theme),
185
- ];
186
- return lines;
159
+ const repository = `${theme.fg("dim", "directory: ")}${theme.fg("text", directory)}${this.branch ? ` ${colorDirectory(this.branch)}` : ""}`;
160
+ const rows = [identity, agent, repository].map((text, index) =>
161
+ `${theme.fg("accent", MASTHEAD_MARK[index] ?? "")}${MASTHEAD_GAP}${truncateToWidth(text, textWidth, "…")}`,
162
+ );
163
+ return [...rows, "", ...this.tipLines(width, theme)];
187
164
  }
188
165
 
189
166
  invalidate(): void {}
@@ -353,9 +330,7 @@ class PiCodeEditor extends CustomEditor {
353
330
 
354
331
  for (let index = 1; index < bottomBorderIndex; index += 1) {
355
332
  const isPromptLine = index === 1 && !isScrolledHeader;
356
- const prefix = isPromptLine
357
- ? this.runtimeTheme.fg(this.focused ? "accent" : "dim", "❯\u00A0")
358
- : " ";
333
+ const prefix = isPromptLine ? this.runtimeTheme.fg("text", "❯\u00A0") : " ";
359
334
  let content = lines[index] ?? "";
360
335
  if (isPromptLine && this.getText() === "") {
361
336
  const first = this.suggestion.slice(0, 1);
@@ -387,10 +362,10 @@ class PiCodeEditor extends CustomEditor {
387
362
  }
388
363
 
389
364
  const ACTIVITY_FRAMES = [
390
- "·", "", "", "", "", "✽",
391
- "", "", "", "", "", "·",
365
+ "", "", "", "", "",
366
+ "", "", "", "", "",
392
367
  ] as const;
393
- const ACTIVITY_FRAME_INTERVAL_MS = 120;
368
+ const ACTIVITY_FRAME_INTERVAL_MS = 80;
394
369
 
395
370
  let killerosEditorFactory: ReturnType<ExtensionContext["ui"]["getEditorComponent"]>;
396
371
 
@@ -1,28 +1,7 @@
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
- import { safeTerminalText } from "./safe-terminal-text.ts";
1
+ import type { ExtensionAPI, ThemeColor } from "@earendil-works/pi-coding-agent";
4
2
 
5
3
  export type ThinkingLevel = ReturnType<ExtensionAPI["getThinkingLevel"]>;
6
4
 
7
- const ALL_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const satisfies readonly ThinkingLevel[];
8
- const LEVEL_LABELS: Readonly<Record<ThinkingLevel, string>> = {
9
- off: "Off",
10
- minimal: "Minimal",
11
- low: "Low",
12
- medium: "Medium",
13
- high: "High",
14
- xhigh: "Extra High",
15
- max: "Maximum",
16
- };
17
- const LEVEL_DESCRIPTIONS: Readonly<Record<ThinkingLevel, string>> = {
18
- off: "No extended reasoning",
19
- minimal: "Brief reasoning",
20
- low: "Light reasoning",
21
- medium: "Balanced reasoning",
22
- high: "Deep reasoning",
23
- xhigh: "Extensive reasoning",
24
- max: "Maximum supported reasoning",
25
- };
26
5
  export const LEVEL_COLORS: Readonly<Record<ThinkingLevel, ThemeColor>> = {
27
6
  off: "thinkingOff",
28
7
  minimal: "thinkingMinimal",
@@ -32,152 +11,3 @@ export const LEVEL_COLORS: Readonly<Record<ThinkingLevel, ThemeColor>> = {
32
11
  xhigh: "thinkingXhigh",
33
12
  max: "thinkingMax",
34
13
  };
35
- const LEVEL_ALIASES: Readonly<Record<string, ThinkingLevel>> = {
36
- quick: "minimal",
37
- fast: "minimal",
38
- light: "low",
39
- balanced: "medium",
40
- deep: "high",
41
- maximum: "max",
42
- none: "off",
43
- };
44
-
45
- function isThinkingLevel(value: string): value is ThinkingLevel {
46
- return ALL_LEVELS.some((level) => level === value);
47
- }
48
-
49
- function resolveThinkingLevel(input: string): ThinkingLevel | undefined {
50
- const normalized = input.trim().toLowerCase();
51
- return isThinkingLevel(normalized) ? normalized : LEVEL_ALIASES[normalized];
52
- }
53
-
54
- function supportedLevels(model: ExtensionContext["model"]): ThinkingLevel[] {
55
- if (!model?.reasoning) return ["off"];
56
- return ALL_LEVELS.filter((level) => {
57
- const mapped = model.thinkingLevelMap?.[level];
58
- if (mapped === null) return false;
59
- return level !== "xhigh" && level !== "max" || mapped !== undefined;
60
- });
61
- }
62
-
63
- function modelLabel(model: ExtensionContext["model"]): string {
64
- return model ? safeTerminalText(`${model.provider}/${model.id}`).replaceAll("\n", "") : "unknown model";
65
- }
66
-
67
- export function registerVariants(pi: ExtensionAPI): void {
68
- const setLevel = (ctx: ExtensionContext, level: ThinkingLevel): void => {
69
- const supported = supportedLevels(ctx.model);
70
- if (!supported.includes(level)) {
71
- ctx.ui.notify(`${LEVEL_LABELS[level]} is not supported by ${modelLabel(ctx.model)}. Supported: ${supported.join(", ")}`, "warning");
72
- return;
73
- }
74
- pi.setThinkingLevel(level);
75
- ctx.ui.notify(`Thinking: ${LEVEL_LABELS[level]}`, "info");
76
- };
77
-
78
- pi.registerCommand("variants", {
79
- description: "Set reasoning level: off, minimal, low, medium, high, xhigh, or max",
80
- handler: async (args, ctx) => {
81
- if (args.trim()) {
82
- const level = resolveThinkingLevel(args);
83
- if (!level) {
84
- ctx.ui.notify(`Unknown reasoning level "${safeTerminalText(args.trim()).replaceAll("\n", "")}". Use: ${ALL_LEVELS.join(", ")}`, "error");
85
- return;
86
- }
87
- setLevel(ctx, level);
88
- return;
89
- }
90
- if (ctx.mode !== "tui") {
91
- ctx.ui.notify("Use /variants <level> outside TUI mode", "error");
92
- return;
93
- }
94
-
95
- const supported = supportedLevels(ctx.model);
96
- if (supported.length === 1) {
97
- ctx.ui.notify(`${modelLabel(ctx.model)} does not support extended reasoning`, "info");
98
- return;
99
- }
100
- const current = pi.getThinkingLevel();
101
- const items = supported.map((level) => ({
102
- value: level,
103
- label: level === current ? `${LEVEL_LABELS[level]} ← current` : LEVEL_LABELS[level],
104
- description: LEVEL_DESCRIPTIONS[level],
105
- }));
106
- const selected = await ctx.ui.custom<ThinkingLevel | null>((tui, theme, keybindings, done) => {
107
- const listTheme = {
108
- selectedPrefix: (text: string) => theme.fg("accent", text),
109
- selectedText: (text: string) => theme.fg("accent", text),
110
- description: (text: string) => theme.fg("muted", text),
111
- scrollInfo: (text: string) => theme.fg("dim", text),
112
- noMatch: (text: string) => theme.fg("warning", text),
113
- };
114
- let selectList: SelectList | undefined;
115
- let visibleOptionRows = 0;
116
-
117
- const chromeFor = (rowBudget: number): "full" | "compact" | "none" => (
118
- rowBudget >= 8 ? "full" : rowBudget >= 4 ? "compact" : "none"
119
- );
120
- const visibleRowsFor = (rowBudget: number): number => {
121
- const chrome = chromeFor(rowBudget);
122
- const chromeRows = chrome === "full" ? 5 : chrome === "compact" ? 2 : 0;
123
- const availableListRows = Math.max(1, rowBudget - chromeRows);
124
- return availableListRows >= items.length
125
- ? items.length
126
- : Math.max(1, availableListRows - 1);
127
- };
128
- const ensureSelectList = (nextVisibleOptionRows: number): SelectList => {
129
- if (selectList && visibleOptionRows === nextVisibleOptionRows) return selectList;
130
- const selectedValue = selectList?.getSelectedItem()?.value ?? current;
131
- const nextSelectList = new SelectList(items, nextVisibleOptionRows, listTheme);
132
- const selectedIndex = items.findIndex((item) => item.value === selectedValue);
133
- nextSelectList.setSelectedIndex(Math.max(0, selectedIndex));
134
- nextSelectList.onSelect = (item) => done(isThinkingLevel(item.value) ? item.value : null);
135
- nextSelectList.onCancel = () => done(null);
136
- selectList = nextSelectList;
137
- visibleOptionRows = nextVisibleOptionRows;
138
- return nextSelectList;
139
- };
140
-
141
- const border = new DynamicBorder((text: string) => theme.fg("accent", text));
142
- const title = ` ${theme.fg("accent", theme.bold("Thinking variants"))}`;
143
- const model = ` ${theme.fg("dim", `Model: ${modelLabel(ctx.model)}`)}`;
144
- const keyHint = (keybinding: Parameters<typeof keybindings.getKeys>[0], description: string): string => {
145
- const keyText = keybindings.getKeys(keybinding)
146
- .join("/")
147
- .split("/")
148
- .map((key) => key.split("+").map((part) => process.platform === "darwin" && part.toLowerCase() === "alt" ? "option" : part).join("+"))
149
- .join("/");
150
- return theme.fg("dim", keyText) + theme.fg("muted", ` ${description}`);
151
- };
152
- 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")}`)}`;
153
-
154
- const render = (width: number): string[] => {
155
- const rowBudget = Math.max(0, tui.terminal.rows);
156
- if (width <= 0 || rowBudget === 0) return [];
157
- const chrome = chromeFor(rowBudget);
158
- const list = ensureSelectList(visibleRowsFor(rowBudget));
159
- const lines: string[] = [];
160
-
161
- if (chrome === "full") lines.push(...border.render(width));
162
- if (chrome !== "none") lines.push(title);
163
- if (chrome === "full") lines.push(model);
164
- lines.push(...list.render(width));
165
- if (chrome !== "none") lines.push(controls);
166
- if (chrome === "full") lines.push(...border.render(width));
167
-
168
- return lines.slice(0, rowBudget).map((line) => truncateToWidth(line, width, ""));
169
- };
170
-
171
- return {
172
- render,
173
- invalidate: () => selectList?.invalidate(),
174
- handleInput: (data) => {
175
- ensureSelectList(visibleRowsFor(Math.max(1, tui.terminal.rows))).handleInput(data);
176
- tui.requestRender();
177
- },
178
- };
179
- });
180
- if (selected) setLevel(ctx, selected);
181
- },
182
- });
183
- }
@@ -53,7 +53,7 @@ export interface WorkedForEntryDataV4 {
53
53
  type WorkedForEntryData = WorkedForEntryDataV1 | WorkedForEntryDataV2 | WorkedForEntryDataV3 | WorkedForEntryDataV4;
54
54
 
55
55
  const OUTCOMES = {
56
- done: { label: "Done", color: "success" },
56
+ done: { label: "Done", color: "success" },
57
57
  stopped: { label: "■ Stopped", color: "warning" },
58
58
  failed: { label: "× Failed", color: "error" },
59
59
  } as const satisfies Record<WorkedForOutcome, { label: string; color: string }>;
@@ -224,47 +224,47 @@ class WorkedForV4Component implements Component {
224
224
  const lines = [
225
225
  `${headline}${theme.fg("dim", ` · ${formatWorkedForDuration(data.milliseconds)} · ↑ ${formatTokens(data.tokens)} tokens${modelSuffix}`)}`,
226
226
  ];
227
- if (data.changes.state === "unavailable") lines.push(theme.fg("dim", " Changes unavailable"));
228
- else if (data.changes.totalFiles === 0) lines.push(theme.fg("dim", " No files changed"));
227
+ if (data.changes.state === "unavailable") lines.push(theme.fg("dim", "Changes unavailable"));
228
+ else if (data.changes.totalFiles === 0) lines.push(theme.fg("dim", "No files changed"));
229
229
  else {
230
230
  const count = `${data.changes.totalFiles} ${data.changes.totalFiles === 1 ? "file" : "files"}`;
231
- lines.push(`${theme.fg("accent", ` ${width < 40 ? count : `Changed ${count}`}`)}${theme.fg("dim", " · ")}${theme.fg("success", `+${data.changes.additions}`)} ${theme.fg("error", `−${data.changes.deletions}`)}`);
231
+ lines.push(`${theme.fg("accent", `${width < 40 ? count : `Changed ${count}`}`)}${theme.fg("dim", " · ")}${theme.fg("success", `+${data.changes.additions}`)} ${theme.fg("error", `−${data.changes.deletions}`)}`);
232
232
  }
233
233
  const passed = data.checks.filter((check) => check.outcome === "passed").length + data.omittedChecks.passed;
234
234
  const failed = data.checks.filter((check) => check.outcome === "failed").length + data.omittedChecks.failed;
235
235
  const totalChecks = passed + failed;
236
236
  if (totalChecks === 0) {
237
- if (data.changes.state === "available" && data.changes.totalFiles > 0) lines.push(theme.fg("warning", " No check recorded"));
237
+ if (data.changes.state === "available" && data.changes.totalFiles > 0) lines.push(theme.fg("warning", "No check recorded"));
238
238
  } else if (totalChecks === 1) {
239
239
  const check = data.checks[0];
240
- if (check?.outcome === "passed") lines.push(theme.fg("success", ` Check passed: ${check.label} ✓`));
241
- else if (check) lines.push(theme.fg("error", ` Check failed: ${check.label} ×`));
240
+ if (check?.outcome === "passed") lines.push(theme.fg("success", `Check passed: ${check.label} ✓`));
241
+ else if (check) lines.push(theme.fg("error", `Check failed: ${check.label} ×`));
242
242
  } else if (failed === 0) {
243
- lines.push(theme.fg("success", ` Checks: ${passed} passed`));
243
+ lines.push(theme.fg("success", `Checks: ${passed} passed`));
244
244
  } else {
245
- lines.push(` ${theme.fg("accent", "Checks:")} ${theme.fg("success", `${passed} passed`)}${theme.fg("dim", " · ")}${theme.fg("error", `${failed} failed`)}`);
245
+ lines.push(`${theme.fg("accent", "Checks:")} ${theme.fg("success", `${passed} passed`)}${theme.fg("dim", " · ")}${theme.fg("error", `${failed} failed`)}`);
246
246
  }
247
247
  if (this.expanded && data.changes.state === "available") {
248
248
  for (const file of data.changes.files) {
249
249
  const marker = file.kind === "added" ? "A" : file.kind === "deleted" ? "D" : file.kind === "renamed" ? "R" : "M";
250
250
  const label = file.kind === "renamed" ? `${safePath(file.previousPath)} → ${safePath(file.path)}` : safePath(file.path);
251
- const prefix = ` ${marker} `;
251
+ const prefix = `${marker} `;
252
252
  const detail = file.detail ? ` ${file.detail}` : ` +${file.additions} −${file.deletions}`;
253
- const labelWidth = width - visibleWidth(prefix) - visibleWidth(detail);
253
+ const labelWidth = width - 1 - visibleWidth(prefix) - visibleWidth(detail);
254
254
  const fittedLabel = labelWidth > 0 ? truncateToWidth(label, labelWidth, "…") : "";
255
255
  const styledDetail = file.detail
256
256
  ? theme.fg("dim", detail)
257
257
  : `${theme.fg("success", ` +${file.additions}`)} ${theme.fg("error", `−${file.deletions}`)}`;
258
258
  lines.push(`${theme.fg("accent", `${prefix}${fittedLabel}`)}${styledDetail}`);
259
259
  }
260
- if (data.changes.omittedFiles > 0) lines.push(theme.fg("dim", ` … ${data.changes.omittedFiles} more files`));
260
+ if (data.changes.omittedFiles > 0) lines.push(theme.fg("dim", `… ${data.changes.omittedFiles} more files`));
261
261
  }
262
262
  if (this.expanded) {
263
- for (const check of data.checks) lines.push(theme.fg(check.outcome === "passed" ? "success" : "error", ` ${check.outcome === "passed" ? "✓" : "×"} ${check.label}`));
263
+ for (const check of data.checks) lines.push(theme.fg(check.outcome === "passed" ? "success" : "error", `${check.outcome === "passed" ? "✓" : "×"} ${check.label}`));
264
264
  const omitted = data.omittedChecks.passed + data.omittedChecks.failed;
265
- if (omitted > 0) lines.push(theme.fg("dim", ` … ${omitted} more checks`));
265
+ if (omitted > 0) lines.push(theme.fg("dim", `… ${omitted} more checks`));
266
266
  }
267
- return lines.map((line) => truncateToWidth(line, width, "…"));
267
+ return lines.map((line) => truncateToWidth(` ${line}`, width, "…"));
268
268
  }
269
269
 
270
270
  invalidate(): void {}
@@ -321,12 +321,12 @@ export function registerWorkedFor(
321
321
  pi.registerEntryRenderer<WorkedForEntryData>(WORKED_FOR_ENTRY_TYPE, (entry, options, theme) => {
322
322
  const data = parseWorkedForEntryData(entry.data);
323
323
  if (!data) return undefined;
324
- if (data.version === 1) return new Text(theme.fg("dim", `✻ Worked for ${formatWorkedForDuration(data.milliseconds)}`), 0, 0);
324
+ if (data.version === 1) return new Text(theme.fg("dim", `✻ Worked for ${formatWorkedForDuration(data.milliseconds)}`), 1, 0);
325
325
  if (data.version === 4) return new WorkedForV4Component(data, options.expanded, theme);
326
326
  const outcome = OUTCOMES[data.outcome];
327
327
  const tokens = data.version === 3 ? ` · ↑ ${formatTokens(data.tokens)} tokens` : "";
328
328
  const headline = theme.fg(outcome.color, outcome.label);
329
- return new Text(`${headline}${theme.fg("dim", ` · ${formatWorkedForDuration(data.milliseconds)}${tokens}`)}`, 0, 0);
329
+ return new Text(`${headline}${theme.fg("dim", ` · ${formatWorkedForDuration(data.milliseconds)}${tokens}`)}`, 1, 0);
330
330
  });
331
331
 
332
332
  pi.on("session_start", async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "killeros",
3
- "version": "2.1.24",
3
+ "version": "2.1.25",
4
4
  "description": "TUI, goals, and workflow automation for the Pi coding agent",
5
5
  "type": "module",
6
6
  "keywords": [