killeros 1.0.0 → 1.1.0

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 ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ All notable changes to KillerOS are documented here.
4
+
5
+ ## [1.1.0] - 2026-07-31
6
+
7
+ ### Added
8
+
9
+ - Compact startup card with model, reasoning level, working directory, context remaining, and detected package capabilities.
10
+ - Packaged KillerOS theme with coral accents and one neutral tool-call surface across pending, success, and error states.
11
+ - Single-glyph Spark activity indicator with a restrained color pulse.
12
+ - Claude-adjacent activity word bank that advances between agent runs.
13
+ - Static `└ Thinking…` label for hidden reasoning blocks.
14
+ - Responsive header tests across terminal widths from 1 to 100 columns.
15
+
16
+ ### Changed
17
+
18
+ - Replaced the animated startup illustration and rotating tips with the compact operational card.
19
+ - Standardized product branding on mixed-case `KillerOS` and the `› KillerOS` lockup.
20
+ - Reduced saturated semantic color usage while preserving status text and diff distinctions.
package/Killeros.ts CHANGED
@@ -1,4 +1,6 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import os from "node:os";
3
+ import { dirname, join } from "node:path";
2
4
  import {
3
5
  CustomEditor,
4
6
  DynamicBorder,
@@ -12,6 +14,7 @@ import {
12
14
  } from "@earendil-works/pi-coding-agent";
13
15
  import {
14
16
  Container,
17
+ decodeKittyPrintable,
15
18
  Editor,
16
19
  Key,
17
20
  matchesKey,
@@ -27,42 +30,24 @@ import {
27
30
  import { Type } from "typebox";
28
31
 
29
32
  const BRAND_RGB = "215;119;87";
30
- const LEFT_PANEL_WIDTH = 42;
31
- const LOGO_CELL = "███";
32
- const LOGO_ANIMATION_INTERVAL_MS = 120;
33
- const TIP_ROTATION_INTERVAL_MS = 5_000;
34
33
  const FOOTER_REFRESH_INTERVAL_MS = 1_000;
34
+ const COMPACT_HEADER_MAX_WIDTH = 76;
35
35
 
36
36
  const brand = (text: string): string => `\x1B[38;2;${BRAND_RGB}m${text}\x1B[39m`;
37
37
 
38
- interface LogoFrame {
39
- phase: number;
40
- active: "left" | "top" | "right" | "none";
41
- ax: number;
42
- ay: number;
43
- flash: boolean;
44
- white: boolean;
38
+ function readPackageMetadata(path: string | URL): { name?: string; version?: string } {
39
+ try {
40
+ const value = JSON.parse(readFileSync(path, "utf8")) as { name?: unknown; version?: unknown };
41
+ return {
42
+ name: typeof value.name === "string" ? value.name : undefined,
43
+ version: typeof value.version === "string" ? value.version : undefined,
44
+ };
45
+ } catch {
46
+ return {};
47
+ }
45
48
  }
46
49
 
47
- const LOGO_FRAMES: LogoFrame[] = [
48
- ...Array.from({ length: 4 }, (_, ay): LogoFrame => ({ phase: 0, active: "left", ax: 2, ay, flash: false, white: false })),
49
- ...Array.from({ length: 3 }, (_, ay): LogoFrame => ({ phase: 1, active: "top", ax: 2, ay, flash: false, white: false })),
50
- ...Array.from({ length: 5 }, (_, ay): LogoFrame => ({ phase: 2, active: "right", ax: 5, ay, flash: false, white: false })),
51
- { phase: 3, active: "none", ax: 0, ay: 0, flash: false, white: false },
52
- { phase: 3, active: "none", ax: 0, ay: 0, flash: true, white: false },
53
- { phase: 3, active: "none", ax: 0, ay: 0, flash: false, white: false },
54
- { phase: 3, active: "none", ax: 0, ay: 0, flash: true, white: false },
55
- { phase: 4, active: "none", ax: 0, ay: 0, flash: false, white: false },
56
- { phase: 5, active: "none", ax: 0, ay: 0, flash: false, white: false },
57
- { phase: 5, active: "none", ax: 0, ay: 0, flash: false, white: true },
58
- { phase: 5, active: "none", ax: 0, ay: 0, flash: false, white: false },
59
- { phase: 5, active: "none", ax: 0, ay: 0, flash: false, white: true },
60
- { phase: 6, active: "none", ax: 0, ay: 0, flash: false, white: false },
61
- ];
62
-
63
- function extractProvider(model: ExtensionContext["model"]): string {
64
- return model?.provider ?? "";
65
- }
50
+ const KILLEROS_VERSION = readPackageMetadata(new URL("./package.json", import.meta.url)).version;
66
51
 
67
52
  function formatCwd(cwd: string): string {
68
53
  const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
@@ -76,264 +61,125 @@ function formatCwd(cwd: string): string {
76
61
  : cwd;
77
62
  }
78
63
 
79
- function center(text: string, width: number): string {
80
- if (width <= 0) return "";
81
- const textWidth = visibleWidth(text);
82
- if (textWidth >= width) return truncateToWidth(text, width, "");
83
- return `${" ".repeat(Math.floor((width - textWidth) / 2))}${text}`;
84
- }
85
-
86
64
  function padRight(text: string, width: number): string {
87
65
  if (width <= 0) return "";
88
66
  const clipped = truncateToWidth(text, width, "");
89
67
  return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
90
68
  }
91
69
 
92
- function hasCell(y: number, x: number, cells: string): boolean {
93
- return cells.split(" ").includes(`${y},${x}`);
70
+ interface CapabilitySourceInfo {
71
+ path?: string;
72
+ source?: string;
73
+ baseDir?: string;
94
74
  }
95
75
 
96
- function hasPiece(y: number, x: number, py: number, px: number, cells: string): boolean {
97
- return cells.split(" ").some((item) => {
98
- const [dy, dx] = item.split(",").map(Number);
99
- return y === py + dy && x === px + dx;
100
- });
101
- }
102
-
103
- type LogoColor = "cyan" | "red" | "green" | "orange" | "flash" | "white" | "brand" | "panel";
104
-
105
- function colorCell(color: LogoColor): string {
106
- switch (color) {
107
- case "cyan": return `\x1B[36m${LOGO_CELL}\x1B[39m`;
108
- case "red": return `\x1B[31m${LOGO_CELL}\x1B[39m`;
109
- case "green": return `\x1B[32m${LOGO_CELL}\x1B[39m`;
110
- case "orange":
111
- case "flash": return `\x1B[33m${LOGO_CELL}\x1B[39m`;
112
- case "white": return `\x1B[97m${LOGO_CELL}\x1B[39m`;
113
- case "brand": return brand(LOGO_CELL);
114
- default: return " ".repeat(LOGO_CELL.length);
115
- }
116
- }
117
-
118
- function logoCellColor(frame: LogoFrame, y: number, x: number): LogoColor {
119
- if (frame.white) {
120
- return hasCell(y, x, "3,2 3,3 3,4 4,2 4,4 5,2 5,3 5,5 6,2 6,5") ? "white" : "panel";
121
- }
122
- if (frame.flash && y === 6 && x >= 1 && x <= 6) return "flash";
123
- if (frame.active === "left" && hasPiece(y, x, frame.ay, frame.ax, "0,0 1,0 1,1 2,0")) return "red";
124
- if (frame.active === "top" && hasPiece(y, x, frame.ay, frame.ax, "0,0 0,1 0,2 1,2")) return "cyan";
125
- if (frame.active === "right" && hasPiece(y, x, frame.ay, frame.ax, "0,0 1,0 2,0 2,1")) return "green";
126
- if (frame.phase === 6) {
127
- return hasCell(y, x, "3,2 3,3 3,4 4,4 4,2 5,2 5,3 5,5 6,2 6,5") ? "brand" : "panel";
128
- }
129
- if (frame.phase === 4) {
130
- if (hasCell(y, x, "2,2 2,3 2,4 3,4")) return "cyan";
131
- if (hasCell(y, x, "3,2 4,2 4,3 5,2")) return "red";
132
- if (hasCell(y, x, "4,5 5,5")) return "green";
133
- return "panel";
134
- }
135
- if (frame.phase >= 5) {
136
- if (hasCell(y, x, "3,2 3,3 3,4 4,4")) return "cyan";
137
- if (hasCell(y, x, "4,2 5,2 5,3 6,2")) return "red";
138
- if (hasCell(y, x, "5,5 6,5")) return "green";
139
- return "panel";
140
- }
141
- if (frame.phase <= 3 && hasCell(y, x, "6,1 6,2 6,3 6,4")) return "orange";
142
- if (frame.phase >= 2 && hasCell(y, x, "2,2 2,3 2,4 3,4")) return "cyan";
143
- if (frame.phase >= 1 && hasCell(y, x, "3,2 4,2 4,3 5,2")) return "red";
144
- if (frame.phase >= 3 && hasCell(y, x, "4,5 5,5 6,5 6,6")) return "green";
145
- return "panel";
76
+ interface CompactCapability {
77
+ label: string;
78
+ version?: string;
146
79
  }
147
80
 
148
- function piLogoFrame(frameIndex: number): string[] {
149
- const frame = LOGO_FRAMES[frameIndex % LOGO_FRAMES.length]!;
150
- const lines: string[] = [];
151
- for (let y = 1; y <= 7; y += 1) {
152
- let line = "";
153
- for (let x = 1; x <= 8; x += 1) line += colorCell(logoCellColor(frame, y, x));
154
- lines.push(line);
155
- }
156
- return lines;
81
+ function capabilityLabel(packageName: string): string {
82
+ const name = packageName.replace(/^npm:/, "").split("/").at(-1) ?? packageName;
83
+ if (name === "pi-mcp-adapter") return "MCP adapter";
84
+ if (name === "pi-web-access") return "Web access";
85
+ return name.replace(/^pi-/, "").replace(/[-_]+/g, " ").replace(/^\w/, (letter) => letter.toUpperCase());
157
86
  }
158
87
 
159
- function borderLine(left: string, label: string, right: string, width: number): string {
160
- if (width <= 0) return "";
161
- if (width === 1) return brand(truncateToWidth(left, 1, ""));
162
- const edgeWidth = visibleWidth(left) + visibleWidth(right);
163
- if (width <= edgeWidth) return brand(truncateToWidth(left + right, width, ""));
164
- const available = width - edgeWidth;
165
- const clippedLabel = truncateToWidth(label, Math.max(0, available - 2), "");
166
- const labelWidth = visibleWidth(clippedLabel);
167
- if (labelWidth === 0 || available < labelWidth + 2) {
168
- return `${brand(left)}${brand("".repeat(available))}${brand(right)}`;
88
+ function collectCompactCapabilities(pi: Pick<ExtensionAPI, "getCommands" | "getAllTools">): CompactCapability[] {
89
+ const sources: CapabilitySourceInfo[] = [];
90
+ try {
91
+ for (const command of pi.getCommands()) {
92
+ if (command.source === "extension" && command.sourceInfo.source !== "inline") sources.push(command.sourceInfo);
93
+ }
94
+ } catch {}
95
+ try {
96
+ for (const tool of pi.getAllTools()) {
97
+ if (tool.sourceInfo.source !== "builtin" && tool.sourceInfo.source !== "sdk") sources.push(tool.sourceInfo);
98
+ }
99
+ } catch {}
100
+
101
+ const capabilities = new Map<string, CompactCapability>();
102
+ for (const source of sources) {
103
+ const directories = [source.baseDir, source.path ? dirname(source.path) : undefined]
104
+ .filter((directory): directory is string => Boolean(directory));
105
+ let metadata: { name?: string; version?: string } = {};
106
+ for (const directory of new Set(directories)) {
107
+ metadata = readPackageMetadata(join(directory, "package.json"));
108
+ if (metadata.name) break;
109
+ }
110
+ const packageName = metadata.name ?? (source.source?.startsWith("npm:") ? source.source.slice(4) : undefined);
111
+ if (!packageName || packageName === "killeros") continue;
112
+ capabilities.set(packageName, { label: capabilityLabel(packageName), version: metadata.version });
169
113
  }
170
- const fill = available - labelWidth - 2;
171
- const before = Math.min(3, fill);
172
- const after = fill - before;
173
- return `${brand(left)}${brand("─".repeat(before))} ${clippedLabel} ${brand("─".repeat(after))}${brand(right)}`;
114
+ return [...capabilities.values()].sort((left, right) => left.label.localeCompare(right.label));
174
115
  }
175
116
 
176
- function boxedLine(content: string, width: number): string {
117
+ function alignEdges(left: string, right: string, width: number): string {
177
118
  if (width <= 0) return "";
178
- if (width <= 2) return truncateToWidth(content, width, "");
179
- return `${brand("│")}${padRight(content, width - 2)}${brand("│")}`;
180
- }
181
-
182
- function twoColumn(left: string, right: string, leftWidth: number, rightWidth: number): string {
183
- return `${padRight(left, leftWidth)} ${brand("│")} ${padRight(right, rightWidth)}`;
119
+ const clippedRight = truncateToWidth(right, width, "");
120
+ const leftWidth = Math.max(0, width - visibleWidth(clippedRight) - 1);
121
+ const clippedLeft = truncateToWidth(left, leftWidth, "…");
122
+ const gap = " ".repeat(Math.max(1, width - visibleWidth(clippedLeft) - visibleWidth(clippedRight)));
123
+ return truncateToWidth(`${clippedLeft}${gap}${clippedRight}`, width, "");
184
124
  }
185
125
 
186
- const TIP_SETS = [
187
- [
188
- "",
189
- "Shortcuts & Commands",
190
- "/variants — model reasoning",
191
- "/compact — compress context",
192
- "/model — choose a model",
193
- "────────────────────────",
194
- "Keybindings",
195
- "Shift+Enter — new line",
196
- "Esc — cancel generation",
197
- "Ctrl+C — interrupt agent",
198
- ],
199
- [
200
- "",
201
- "Session",
202
- "/new — start a session",
203
- "/name — name this session",
204
- "/session — usage and stats",
205
- "────────────────────────",
206
- "Workflow",
207
- "Give Pi a goal and constraints",
208
- "Ask it to run the relevant tests",
209
- "Review changes before committing",
210
- ],
211
- [
212
- "",
213
- "Useful Commands",
214
- "/copy — copy last response",
215
- "/tree — navigate branches",
216
- "/reload — reload resources",
217
- "────────────────────────",
218
- "Extension locations",
219
- "Global: ~/.pi/agent/extensions",
220
- "Project: .pi/extensions",
221
- "Reload after making changes",
222
- ],
223
- [
224
- "",
225
- "Navigation",
226
- "Up/Down — command history",
227
- "Tab — autocomplete",
228
- "Ctrl+L — clear the screen",
229
- "────────────────────────",
230
- "Good defaults",
231
- "Keep edits scoped",
232
- "Test after refactoring",
233
- "Verify output before committing",
234
- ],
235
- ] as const;
236
-
237
- function getTipLines(index: number, theme: Theme): string[] {
238
- const selected = TIP_SETS[index % TIP_SETS.length] ?? TIP_SETS[0];
239
- return selected.map((line, lineIndex) => {
240
- if (lineIndex === 1 || lineIndex === 6) return brand(theme.bold(line));
241
- if (line.startsWith("─")) return brand(line);
242
- if (line.startsWith("/")) {
243
- const [command, ...rest] = line.split(" ");
244
- return `${theme.fg("accent", command ?? "")}${theme.fg("dim", ` ${rest.join(" ")}`)}`;
245
- }
246
- return theme.fg(lineIndex > 6 ? "muted" : "dim", line);
247
- });
126
+ function compactBoxLine(content: string, width: number, theme: Theme): string {
127
+ if (width < 4) return truncateToWidth(content, width, "");
128
+ return `${theme.fg("dim", "│")} ${padRight(content, width - 4)} ${theme.fg("dim", "│")}`;
248
129
  }
249
130
 
250
131
  class PiStartupHeader {
251
- private frame = 0;
252
- private tipIndex = 0;
253
- private animationTimer?: ReturnType<typeof setInterval>;
254
- private tipTimer?: ReturnType<typeof setInterval>;
255
- private disposed = false;
256
-
257
- constructor(
258
- private readonly pi: ExtensionAPI,
259
- private readonly ctx: ExtensionContext,
260
- private readonly tui: TUI,
261
- ) {
262
- this.animationTimer = setInterval(() => {
263
- if (this.disposed) return;
264
- if (this.frame >= LOGO_FRAMES.length - 1) {
265
- this.stopAnimation();
266
- return;
267
- }
268
- this.frame += 1;
269
- this.tui.requestRender();
270
- if (this.frame >= LOGO_FRAMES.length - 1) this.stopAnimation();
271
- }, LOGO_ANIMATION_INTERVAL_MS);
272
- this.animationTimer.unref?.();
273
-
274
- this.tipTimer = setInterval(() => {
275
- if (this.disposed) return;
276
- this.tipIndex = (this.tipIndex + 1) % TIP_SETS.length;
277
- this.tui.requestRender();
278
- }, TIP_ROTATION_INTERVAL_MS);
279
- this.tipTimer.unref?.();
132
+ private readonly pi: ExtensionAPI;
133
+ private readonly ctx: ExtensionContext;
134
+ private readonly capabilities: CompactCapability[];
135
+
136
+ constructor(pi: ExtensionAPI, ctx: ExtensionContext) {
137
+ this.pi = pi;
138
+ this.ctx = ctx;
139
+ this.capabilities = collectCompactCapabilities(pi);
280
140
  }
281
141
 
282
- private stopAnimation(): void {
283
- if (!this.animationTimer) return;
284
- clearInterval(this.animationTimer);
285
- this.animationTimer = undefined;
142
+ private contextText(theme: Theme): string {
143
+ const usage = this.ctx.getContextUsage();
144
+ if (!usage || usage.tokens === null) return theme.fg("dim", "context —");
145
+ const windowSize = usage.contextWindow > 0 ? usage.contextWindow : 128_000;
146
+ const percent = Math.max(0, Math.min(100, Math.round((1 - usage.tokens / windowSize) * 100)));
147
+ return theme.fg(percent < 20 ? "error" : percent <= 50 ? "warning" : "success", `${percent}% context`);
286
148
  }
287
149
 
288
150
  render(width: number): string[] {
289
151
  if (width <= 0) return [];
290
152
  const theme = this.ctx.ui.theme;
291
- if (width < 16) return [truncateToWidth(theme.fg("accent", `Pi v${VERSION}`), width, "")];
292
-
293
- const innerWidth = width - 2;
294
- const isTwoColumn = innerWidth >= 64;
295
- const provider = extractProvider(this.ctx.model);
296
- const rawModel = this.ctx.model?.id ?? this.ctx.model?.name;
297
- const model = rawModel
298
- ? provider && !rawModel.startsWith(`${provider}/`) ? `${provider}/${rawModel}` : rawModel
299
- : "Default model";
300
- const effort = this.pi.getThinkingLevel();
301
- const cwd = formatCwd(this.ctx.cwd);
302
- const leftWidth = isTwoColumn ? Math.min(LEFT_PANEL_WIDTH, Math.floor(innerWidth * 0.55)) : innerWidth;
303
- const rightWidth = isTwoColumn ? innerWidth - leftWidth - 3 : 0;
304
- const logoLines = leftWidth >= 24
305
- ? piLogoFrame(this.frame).map((line) => center(line, leftWidth))
306
- : ["", "", center(brand(theme.bold("Pi Coding Agent")), leftWidth), "", "", "", ""];
307
- const modelText = leftWidth < 34 ? `${model} (${effort})` : `${model} with ${effort} effort`;
308
- const leftLines = [
309
- ...logoLines,
310
- center(theme.bold("Let's build something great"), leftWidth),
311
- center(theme.fg("muted", truncateToWidth(modelText, leftWidth, "…")), leftWidth),
312
- center(theme.fg("dim", truncateToWidth(cwd, leftWidth, "…")), leftWidth),
153
+ if (width < 28) return [truncateToWidth(brand(theme.bold("KillerOS")), width, "")];
154
+
155
+ const panelWidth = Math.min(width, COMPACT_HEADER_MAX_WIDTH);
156
+ const innerWidth = panelWidth - 4;
157
+ const version = KILLEROS_VERSION ? theme.fg("dim", ` ${KILLEROS_VERSION}`) : "";
158
+ const identity = `${brand(theme.bold("› KillerOS"))}${version}`;
159
+ const model = this.ctx.model?.id ?? "default model";
160
+ const agent = `${model} · ${this.pi.getThinkingLevel()}`;
161
+ const directory = formatCwd(this.ctx.cwd);
162
+ const border = (left: string, right: string): string => theme.fg("dim", `${left}${"─".repeat(panelWidth - 2)}${right}`);
163
+ const lines = [
164
+ border("╭", "╮"),
165
+ compactBoxLine(alignEdges(identity, theme.fg("success", "READY"), innerWidth), panelWidth, theme),
166
+ compactBoxLine("", panelWidth, theme),
167
+ compactBoxLine(alignEdges(agent, theme.fg("dim", "/model"), innerWidth), panelWidth, theme),
168
+ compactBoxLine(alignEdges(directory, this.contextText(theme), innerWidth), panelWidth, theme),
313
169
  ];
314
- const tipLines = isTwoColumn ? getTipLines(this.tipIndex, theme) : [];
315
- const lines = [borderLine("", `${brand("Pi")} v${VERSION}`, "╮", width)];
316
- for (let index = 0; index < leftLines.length; index += 1) {
317
- const content = isTwoColumn
318
- ? twoColumn(leftLines[index] ?? "", tipLines[index] ?? "", leftWidth, rightWidth)
319
- : padRight(leftLines[index] ?? "", leftWidth);
320
- lines.push(boxedLine(content, width));
170
+ if (this.capabilities.length > 0) {
171
+ lines.push(compactBoxLine(theme.fg("dim", "".repeat(innerWidth)), panelWidth, theme));
172
+ const capabilityText = this.capabilities
173
+ .map((capability) => `${capability.label}${capability.version ? ` ${capability.version}` : ""}`)
174
+ .join(" · ");
175
+ lines.push(compactBoxLine(theme.fg("dim", capabilityText), panelWidth, theme));
321
176
  }
322
- lines.push(borderLine("╰", "", "╯", width));
323
- return lines.map((line) => truncateToWidth(line, width, ""));
177
+ lines.push(border("╰", "╯"));
178
+ return lines;
324
179
  }
325
180
 
326
181
  invalidate(): void {}
327
-
328
- dispose(): void {
329
- if (this.disposed) return;
330
- this.disposed = true;
331
- this.stopAnimation();
332
- if (this.tipTimer) {
333
- clearInterval(this.tipTimer);
334
- this.tipTimer = undefined;
335
- }
336
- }
182
+ dispose(): void {}
337
183
  }
338
184
 
339
185
  const ANSI_REGEX = /\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
@@ -348,8 +194,11 @@ function isBorderLine(line: string): boolean {
348
194
  }
349
195
 
350
196
  class PiCodeEditor extends CustomEditor {
351
- constructor(tui: TUI, theme: EditorTheme, private readonly appKeybindings: KeybindingsManager) {
197
+ private readonly appKeybindings: KeybindingsManager;
198
+
199
+ constructor(tui: TUI, theme: EditorTheme, appKeybindings: KeybindingsManager) {
352
200
  super(tui, theme, appKeybindings);
201
+ this.appKeybindings = appKeybindings;
353
202
  }
354
203
 
355
204
  override handleInput(data: string): void {
@@ -419,30 +268,51 @@ function reportError(ctx: ExtensionContext, area: string, error: unknown): void
419
268
  ctx.ui.notify(`${area}: ${message}`, "error");
420
269
  }
421
270
 
271
+ const ACTIVITY_WORDS = ["Brewing", "Pondering", "Tinkering", "Wrangling", "Noodling", "Cooking"] as const;
272
+
422
273
  function registerShellUi(pi: ExtensionAPI): void {
423
274
  let activeHeader: PiStartupHeader | undefined;
275
+ let activityWordIndex = 0;
424
276
 
425
277
  pi.on("session_start", (_event, ctx) => {
426
278
  if (ctx.mode !== "tui") return;
427
279
  try {
428
- ctx.ui.setHeader((tui) => {
280
+ ctx.ui.setTheme("killeros");
281
+ ctx.ui.setHeader(() => {
429
282
  activeHeader?.dispose();
430
- activeHeader = new PiStartupHeader(pi, ctx, tui);
283
+ activeHeader = new PiStartupHeader(pi, ctx);
431
284
  return activeHeader;
432
285
  });
433
286
  ctx.ui.setWorkingIndicator({
434
- frames: ["◐", "◓", "◑", "◒"].map((frame) => ctx.ui.theme.fg("accent", frame)),
435
- intervalMs: 120,
287
+ frames: [
288
+ ctx.ui.theme.fg("dim", "✻"),
289
+ ctx.ui.theme.fg("muted", "✻"),
290
+ ctx.ui.theme.fg("accent", "✻"),
291
+ ctx.ui.theme.fg("muted", "✻"),
292
+ ],
293
+ intervalMs: 180,
436
294
  });
295
+ ctx.ui.setHiddenThinkingLabel("└ Thinking…");
437
296
  ctx.ui.setEditorComponent((tui, theme, keybindings) => new PiCodeEditor(tui, theme, keybindings));
438
297
  } catch (error) {
439
298
  reportError(ctx, "Killeros UI failed to initialize", error);
440
299
  }
441
300
  });
442
301
 
302
+ pi.on("agent_start", (_event, ctx) => {
303
+ if (ctx.mode !== "tui") return;
304
+ ctx.ui.setWorkingMessage(`${ACTIVITY_WORDS[activityWordIndex]}…`);
305
+ activityWordIndex = (activityWordIndex + 1) % ACTIVITY_WORDS.length;
306
+ });
307
+
308
+ pi.on("agent_end", (_event, ctx) => {
309
+ if (ctx.mode === "tui") ctx.ui.setWorkingMessage();
310
+ });
311
+
443
312
  pi.on("session_shutdown", () => {
444
313
  activeHeader?.dispose();
445
314
  activeHeader = undefined;
315
+ activityWordIndex = 0;
446
316
  });
447
317
  }
448
318
 
@@ -514,7 +384,34 @@ function rememberCustomInput(value: string): void {
514
384
  }
515
385
 
516
386
  function isPrintableInput(data: string): boolean {
517
- return data.length > 0 && !/[\u0000-\u001F\u007F]/u.test(data);
387
+ return data.length > 0 && !/[\u0000-\u001F\u007F-\u009F]/u.test(data);
388
+ }
389
+
390
+ const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
391
+
392
+ function decodeQuestionFilterInput(data: string): string | undefined {
393
+ const kittyPrintable = decodeKittyPrintable(data);
394
+ if (kittyPrintable !== undefined) return isPrintableInput(kittyPrintable) ? kittyPrintable : undefined;
395
+
396
+ const pasteStart = "\x1B[200~";
397
+ const pasteEnd = "\x1B[201~";
398
+ const startIndex = data.indexOf(pasteStart);
399
+ const endIndex = data.indexOf(pasteEnd, startIndex + pasteStart.length);
400
+ if (startIndex >= 0 && endIndex >= 0) {
401
+ return data
402
+ .slice(startIndex + pasteStart.length, endIndex)
403
+ .replace(/\r\n|\r|\n/gu, "")
404
+ .replace(/\t/gu, " ")
405
+ .replace(/[\u0000-\u001F\u007F-\u009F]/gu, "");
406
+ }
407
+
408
+ return isPrintableInput(data) ? data : undefined;
409
+ }
410
+
411
+ function removeLastGrapheme(value: string): string {
412
+ const segments = [...graphemeSegmenter.segment(value)];
413
+ const last = segments.at(-1);
414
+ return last ? value.slice(0, last.index) : "";
518
415
  }
519
416
 
520
417
  function registerQuestionTool(pi: ExtensionAPI): void {
@@ -552,8 +449,6 @@ function registerQuestionTool(pi: ExtensionAPI): void {
552
449
  let optionIndex = 0;
553
450
  let editMode = false;
554
451
  let filterQuery = "";
555
- let historyIndex = -1;
556
- let savedDraft = "";
557
452
  let cachedWidth: number | undefined;
558
453
  let cachedLines: string[] | undefined;
559
454
  let completed = false;
@@ -576,6 +471,7 @@ function registerQuestionTool(pi: ExtensionAPI): void {
576
471
  },
577
472
  };
578
473
  const editor = new Editor(tui, editorTheme);
474
+ customInputHistory.forEach((value) => editor.addToHistory(value));
579
475
 
580
476
  const filteredOptions = (): DisplayOption[] => {
581
477
  const query = filterQuery.trim().toLocaleLowerCase();
@@ -605,15 +501,11 @@ function registerQuestionTool(pi: ExtensionAPI): void {
605
501
  }
606
502
  editMode = false;
607
503
  editor.setText("");
608
- historyIndex = -1;
609
- savedDraft = "";
610
504
  refresh();
611
505
  };
612
506
 
613
507
  const enterCustomMode = (): void => {
614
508
  editMode = true;
615
- historyIndex = -1;
616
- savedDraft = "";
617
509
  refresh();
618
510
  };
619
511
 
@@ -622,30 +514,6 @@ function registerQuestionTool(pi: ExtensionAPI): void {
622
514
  if (matchesKey(data, Key.escape)) {
623
515
  editMode = false;
624
516
  editor.setText("");
625
- historyIndex = -1;
626
- savedDraft = "";
627
- refresh();
628
- return;
629
- }
630
- if (matchesKey(data, Key.up) && customInputHistory.length > 0) {
631
- if (historyIndex < 0) {
632
- savedDraft = editor.getText();
633
- historyIndex = customInputHistory.length - 1;
634
- } else if (historyIndex > 0) {
635
- historyIndex -= 1;
636
- }
637
- editor.setText(customInputHistory[historyIndex] ?? "");
638
- refresh();
639
- return;
640
- }
641
- if (matchesKey(data, Key.down) && historyIndex >= 0) {
642
- if (historyIndex < customInputHistory.length - 1) {
643
- historyIndex += 1;
644
- editor.setText(customInputHistory[historyIndex] ?? "");
645
- } else {
646
- historyIndex = -1;
647
- editor.setText(savedDraft);
648
- }
649
517
  refresh();
650
518
  return;
651
519
  }
@@ -685,21 +553,23 @@ function registerQuestionTool(pi: ExtensionAPI): void {
685
553
  }
686
554
  if (matchesKey(data, Key.backspace)) {
687
555
  if (filterQuery) {
688
- filterQuery = Array.from(filterQuery).slice(0, -1).join("");
556
+ filterQuery = removeLastGrapheme(filterQuery);
689
557
  optionIndex = 0;
690
558
  refresh();
691
559
  }
692
560
  return;
693
561
  }
694
- if (/^[1-9]$/.test(data)) {
695
- const selected = visibleOptions[Number(data) - 1];
562
+ const printableInput = decodeQuestionFilterInput(data);
563
+ const isPasteInput = data.includes("\x1B[200~");
564
+ if (!isPasteInput && printableInput && /^[1-9]$/.test(printableInput)) {
565
+ const selected = visibleOptions[Number(printableInput) - 1];
696
566
  if (!selected) return;
697
567
  if (selected.isOther) enterCustomMode();
698
568
  else finish({ kind: "selected", answer: selected.label, originalIndex: selected.originalIndex });
699
569
  return;
700
570
  }
701
- if (isPrintableInput(data)) {
702
- filterQuery += data;
571
+ if (printableInput) {
572
+ filterQuery += printableInput;
703
573
  optionIndex = 0;
704
574
  refresh();
705
575
  }
@@ -848,7 +718,7 @@ function registerAliases(pi: ExtensionAPI): void {
848
718
  await ctx.newSession();
849
719
  };
850
720
  pi.registerCommand("clear", { description: "Start a new session after confirmation", handler: startNewSession });
851
- pi.registerCommand("quit", {
721
+ pi.registerCommand("exit", {
852
722
  description: "Quit Pi gracefully",
853
723
  handler: async (_args, ctx) => ctx.shutdown(),
854
724
  });
@@ -891,13 +761,9 @@ const COMMAND_SYNTAX_HINTS: Readonly<Record<string, string>> = {
891
761
  model: "/model [provider/model]",
892
762
  "scoped-models": "/scoped-models",
893
763
  login: "/login [provider]",
894
- logout: "/logout [provider]",
895
764
  export: "/export [filename]",
896
765
  import: "/import [path]",
897
766
  name: "/name [session-name]",
898
- fork: "/fork [name]",
899
- clone: "/clone [name]",
900
- resume: "/resume [session-id]",
901
767
  };
902
768
 
903
769
  interface TaggedAutocompleteItem extends AutocompleteItem {
@@ -1173,9 +1039,12 @@ export function formatContextProgress(tokensUsed: number | null, contextWindow:
1173
1039
 
1174
1040
  function sumSessionCost(ctx: ExtensionContext): number {
1175
1041
  let total = 0;
1176
- for (const entry of ctx.sessionManager.getBranch()) {
1177
- if (entry.type === "message" && entry.message.role === "assistant") {
1042
+ for (const entry of ctx.sessionManager.getEntries()) {
1043
+ if (entry.type === "message" && entry.message.role === "assistant") total += entry.message.usage.cost.total;
1044
+ else if (entry.type === "message" && entry.message.role === "toolResult" && entry.message.usage) {
1178
1045
  total += entry.message.usage.cost.total;
1046
+ } else if ((entry.type === "compaction" || entry.type === "branch_summary") && entry.usage) {
1047
+ total += entry.usage.cost.total;
1179
1048
  }
1180
1049
  }
1181
1050
  return total;
package/README.md CHANGED
@@ -14,8 +14,6 @@ The extension is strict TypeScript and uses only packages provided by Pi.
14
14
 
15
15
  ### npm
16
16
 
17
- After the first npm release:
18
-
19
17
  ```bash
20
18
  pi install npm:killeros
21
19
  ```
@@ -31,20 +29,22 @@ pi install git:github.com/KyrosHendrix/pi-KillerOS
31
29
  Pin an install to a release:
32
30
 
33
31
  ```bash
34
- pi install git:github.com/KyrosHendrix/pi-KillerOS@v1.0.0
32
+ pi install git:github.com/KyrosHendrix/pi-KillerOS@v1.1.0
35
33
  ```
36
34
 
37
35
  Add `-l` to either command for a project-only install. Restart Pi after installing.
38
36
 
39
37
  ## Features
40
38
 
41
- - Animated startup header with current model, reasoning level, and working-directory context
39
+ - Compact KillerOS startup card with model, directory, context, and loaded capability state
40
+ - Cohesive dark theme with coral accents and neutral tool-call containers across pending, success, and error states
41
+ - Coral Spark activity indicator with Claude-adjacent verbs that advance between agent runs and a quiet hidden-thinking label
42
42
  - Framed multiline editor with Shift+Enter support
43
43
  - Footer with model, reasoning, context remaining, Git branch, elapsed time, and cost
44
44
  - `/variants` selector and direct reasoning-level arguments
45
45
  - `question` tool with filtering, keyboard selection, custom answers, history, cancellation, and resize-safe rendering
46
46
  - Mid-prompt slash completion with current Pi `0.82.1` commands, extensions, prompts, and skills
47
- - `/clear` for a confirmed new session, plus `/quit` for graceful shutdown
47
+ - `/clear` for a confirmed new session, plus `/exit` for graceful shutdown
48
48
  - Concise system-prompt guidance without modifying completed assistant messages
49
49
 
50
50
  ## Commands
@@ -53,13 +53,15 @@ Add `-l` to either command for a project-only install. Restart Pi after installi
53
53
  /variants Open the reasoning-level selector
54
54
  /variants high Set a reasoning level directly
55
55
  /clear Start a new session after confirmation
56
- /quit Quit Pi gracefully
56
+ /exit Quit Pi gracefully
57
57
  ```
58
58
 
59
59
  Supported reasoning levels are `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`. KillerOS limits choices to levels supported by the current model.
60
60
 
61
61
  ## Configuration
62
62
 
63
+ KillerOS activates its packaged `killeros` theme when a TUI session starts. Tool-call backgrounds stay neutral across pending, successful, and failed states; restrained text and icons preserve status visibility.
64
+
63
65
  KillerOS displays provider costs in USD.
64
66
 
65
67
  Set a custom footer shortcut hint with:
@@ -83,8 +85,9 @@ Before release, run:
83
85
  ```bash
84
86
  npm ci
85
87
  npm run check
88
+ npm test
86
89
  npm pack --dry-run
87
- pi -e . --mode rpc
90
+ pi -ne -e . --mode rpc
88
91
  ```
89
92
 
90
93
  The package manifest lists Pi’s built-in modules as peer dependencies, so npm does not bundle a second copy.
@@ -93,16 +96,13 @@ The package manifest lists Pi’s built-in modules as peer dependencies, so npm
93
96
 
94
97
  The [`pi-package`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md) keyword makes a published npm release visible in Pi’s package catalog.
95
98
 
99
+ Choose `patch`, `minor`, or `major` for the release, then publish and push the version commit and tag:
100
+
96
101
  ```bash
97
102
  npm login
103
+ npm version patch
98
104
  npm publish
99
- ```
100
-
101
- Create and push a matching Git tag after publication:
102
-
103
- ```bash
104
- git tag v1.0.0
105
- git push origin main v1.0.0
105
+ git push origin main --follow-tags
106
106
  ```
107
107
 
108
108
  ## Security
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "killeros",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "A production-hardened TUI and workflow extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -21,17 +21,23 @@
21
21
  },
22
22
  "files": [
23
23
  "Killeros.ts",
24
- "README.md"
24
+ "themes/killeros.json",
25
+ "README.md",
26
+ "CHANGELOG.md"
25
27
  ],
26
28
  "engines": {
27
29
  "node": ">=22.19.0"
28
30
  },
29
31
  "scripts": {
30
- "check": "tsc --noEmit"
32
+ "check": "tsc --noEmit",
33
+ "test": "node --test --experimental-strip-types test/*.test.js"
31
34
  },
32
35
  "pi": {
33
36
  "extensions": [
34
37
  "./Killeros.ts"
38
+ ],
39
+ "themes": [
40
+ "./themes/killeros.json"
35
41
  ]
36
42
  },
37
43
  "peerDependencies": {
@@ -0,0 +1,85 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
3
+ "name": "killeros",
4
+ "vars": {
5
+ "coral": "#d77757",
6
+ "coralBright": "#e58b6d",
7
+ "canvas": "#090b0e",
8
+ "surface": "#101419",
9
+ "surfaceRaised": "#151a20",
10
+ "line": "#39424b",
11
+ "lineMuted": "#2b333b",
12
+ "text": "#dce1e5",
13
+ "muted": "#8f99a3",
14
+ "dim": "#606b75",
15
+ "success": "#8fa88b",
16
+ "error": "#c8786c",
17
+ "warning": "#bda36c",
18
+ "pink": "#b98aa5"
19
+ },
20
+ "colors": {
21
+ "accent": "coral",
22
+ "border": "line",
23
+ "borderAccent": "coral",
24
+ "borderMuted": "lineMuted",
25
+ "success": "success",
26
+ "error": "error",
27
+ "warning": "warning",
28
+ "muted": "muted",
29
+ "dim": "dim",
30
+ "text": "text",
31
+ "thinkingText": "muted",
32
+
33
+ "selectedBg": "surfaceRaised",
34
+ "userMessageBg": "surface",
35
+ "userMessageText": "text",
36
+ "customMessageBg": "surface",
37
+ "customMessageText": "text",
38
+ "customMessageLabel": "coralBright",
39
+ "toolPendingBg": "surface",
40
+ "toolSuccessBg": "surface",
41
+ "toolErrorBg": "surface",
42
+ "toolTitle": "coralBright",
43
+ "toolOutput": "muted",
44
+
45
+ "mdHeading": "coralBright",
46
+ "mdLink": "coralBright",
47
+ "mdLinkUrl": "dim",
48
+ "mdCode": "coralBright",
49
+ "mdCodeBlock": "text",
50
+ "mdCodeBlockBorder": "lineMuted",
51
+ "mdQuote": "muted",
52
+ "mdQuoteBorder": "line",
53
+ "mdHr": "line",
54
+ "mdListBullet": "coral",
55
+
56
+ "toolDiffAdded": "success",
57
+ "toolDiffRemoved": "error",
58
+ "toolDiffContext": "muted",
59
+
60
+ "syntaxComment": "dim",
61
+ "syntaxKeyword": "coralBright",
62
+ "syntaxFunction": "#c9a48d",
63
+ "syntaxVariable": "#b7c0c8",
64
+ "syntaxString": "#a9b78d",
65
+ "syntaxNumber": "#c5a77a",
66
+ "syntaxType": "#a3afb8",
67
+ "syntaxOperator": "muted",
68
+ "syntaxPunctuation": "muted",
69
+
70
+ "thinkingOff": "dim",
71
+ "thinkingMinimal": "#78685f",
72
+ "thinkingLow": "#98705f",
73
+ "thinkingMedium": "#b27762",
74
+ "thinkingHigh": "coral",
75
+ "thinkingXhigh": "#d58272",
76
+ "thinkingMax": "pink",
77
+
78
+ "bashMode": "warning"
79
+ },
80
+ "export": {
81
+ "pageBg": "#090b0e",
82
+ "cardBg": "#101419",
83
+ "infoBg": "#151a20"
84
+ }
85
+ }