killeros 1.0.1 → 1.2.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,23 @@
1
+ # Changelog
2
+
3
+ All notable changes to KillerOS are documented here.
4
+
5
+ ## [1.2.0] - 2026-07-30
6
+
7
+ ### Added
8
+
9
+ - A 52-column Compact startup card with inline version, polished model/provider identity, reasoning level, `/model`, working directory, and conditional Git branch.
10
+ - A shuffled startup-tip deck that keeps one tip stable per session and exhausts the bank before repeating.
11
+ - A responsive footer that preserves model and context while progressively removing lower-priority telemetry.
12
+ - Packaged KillerOS theme with coral accents and one neutral tool-call surface across pending, success, and error states.
13
+ - Single-glyph Spark activity indicator with a restrained color pulse.
14
+ - Claude-adjacent activity word bank that advances between agent runs.
15
+ - Static `└ Thinking…` label for hidden reasoning blocks.
16
+ - Responsive header and footer tests across narrow terminal widths.
17
+
18
+ ### Changed
19
+
20
+ - Replaced the animated startup illustration and capability inventory with the Compact startup card and one external tip.
21
+ - Standardized product branding on mixed-case `KillerOS` and the neutral `› KillerOS (v1.2.0)` lockup.
22
+ - Made theme neutrals achromatic while preserving the coral accent.
23
+ - Replaced the footer progress bar with direct `percent left (tokens)` context telemetry and a critical `/compact` prompt.
package/Killeros.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
1
3
  import os from "node:os";
2
4
  import {
3
5
  CustomEditor,
@@ -27,29 +29,52 @@ import {
27
29
  } from "@earendil-works/pi-tui";
28
30
  import { Type } from "typebox";
29
31
 
30
- const BRAND_RGB = "215;119;87";
31
- const LEFT_PANEL_WIDTH = 42;
32
- const LOGO_CELL = "███";
33
- const LOGO_ANIMATION_INTERVAL_MS = 120;
34
- const TIP_ROTATION_INTERVAL_MS = 5_000;
32
+ const COMMAND_BLUE_RGB = "120;169;255";
35
33
  const FOOTER_REFRESH_INTERVAL_MS = 1_000;
34
+ const COMPACT_HEADER_MAX_WIDTH = 52;
36
35
 
37
- const brand = (text: string): string => `\x1B[38;2;${BRAND_RGB}m${text}\x1B[39m`;
36
+ const commandBlue = (text: string): string => `\x1B[38;2;${COMMAND_BLUE_RGB}m${text}\x1B[39m`;
38
37
 
39
- const ORCA_ART = [
40
- ".....g......",
41
- "....ggg.....",
42
- ".gggggggg..g",
43
- "gbggwwgggggg",
44
- ".ggwwgggg..g",
45
- "...gggg.....",
46
- ".....gg.....",
38
+ function readPackageVersion(path: string | URL): string | undefined {
39
+ try {
40
+ const value = JSON.parse(readFileSync(path, "utf8")) as { version?: unknown };
41
+ return typeof value.version === "string" ? value.version : undefined;
42
+ } catch {
43
+ return undefined;
44
+ }
45
+ }
46
+
47
+ const KILLEROS_VERSION = readPackageVersion(new URL("./package.json", import.meta.url));
48
+
49
+ const STARTUP_TIPS = [
50
+ "Press Shift+Enter to insert a line break without sending.",
51
+ "Run /variants to tune the model's reasoning depth.",
52
+ "Type / to browse every command available in this session.",
47
53
  ] as const;
48
- const ORCA_WIDTH = ORCA_ART[0].length;
49
- const LOGO_FRAME_COUNT = ORCA_WIDTH + 3;
50
54
 
51
- function extractProvider(model: ExtensionContext["model"]): string {
52
- return model?.provider ?? "";
55
+ function resolveGitBranch(cwd: string): string | undefined {
56
+ try {
57
+ const branch = execFileSync("git", ["-C", cwd, "rev-parse", "--abbrev-ref", "HEAD"], {
58
+ encoding: "utf8",
59
+ maxBuffer: 64 * 1024,
60
+ stdio: ["ignore", "pipe", "ignore"],
61
+ timeout: 500,
62
+ windowsHide: true,
63
+ }).trim();
64
+ if (!branch) return undefined;
65
+ return branch === "HEAD" ? "detached" : branch;
66
+ } catch {
67
+ return undefined;
68
+ }
69
+ }
70
+
71
+ function shuffledTips(): string[] {
72
+ const tips = [...STARTUP_TIPS];
73
+ for (let index = tips.length - 1; index > 0; index -= 1) {
74
+ const swapIndex = Math.floor(Math.random() * (index + 1));
75
+ [tips[index], tips[swapIndex]] = [tips[swapIndex]!, tips[index]!];
76
+ }
77
+ return tips;
53
78
  }
54
79
 
55
80
  function formatCwd(cwd: string): string {
@@ -64,226 +89,74 @@ function formatCwd(cwd: string): string {
64
89
  : cwd;
65
90
  }
66
91
 
67
- function center(text: string, width: number): string {
68
- if (width <= 0) return "";
69
- const textWidth = visibleWidth(text);
70
- if (textWidth >= width) return truncateToWidth(text, width, "");
71
- return `${" ".repeat(Math.floor((width - textWidth) / 2))}${text}`;
72
- }
73
-
74
92
  function padRight(text: string, width: number): string {
75
93
  if (width <= 0) return "";
76
94
  const clipped = truncateToWidth(text, width, "");
77
95
  return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
78
96
  }
79
97
 
80
- type LogoColor = "body" | "white" | "brand" | "panel";
81
-
82
- function colorCell(color: LogoColor): string {
83
- switch (color) {
84
- case "body": return `\x1B[90m${LOGO_CELL}\x1B[39m`;
85
- case "white": return `\x1B[97m${LOGO_CELL}\x1B[39m`;
86
- case "brand": return brand(LOGO_CELL);
87
- default: return " ".repeat(LOGO_CELL.length);
88
- }
89
- }
90
-
91
- function piLogoFrame(frameIndex: number): string[] {
92
- const visibleColumns = Math.min(ORCA_WIDTH, frameIndex + 1);
93
- const eyeColor: LogoColor = frameIndex === ORCA_WIDTH ? "white" : "brand";
94
- return ORCA_ART.map((row) => [...row].map((cell, index) => {
95
- if (index >= visibleColumns) return colorCell("panel");
96
- if (cell === "g") return colorCell("body");
97
- if (cell === "w") return colorCell("white");
98
- if (cell === "b") return colorCell(eyeColor);
99
- return colorCell("panel");
100
- }).join(""));
101
- }
102
-
103
- function borderLine(left: string, label: string, right: string, width: number): string {
104
- if (width <= 0) return "";
105
- if (width === 1) return brand(truncateToWidth(left, 1, ""));
106
- const edgeWidth = visibleWidth(left) + visibleWidth(right);
107
- if (width <= edgeWidth) return brand(truncateToWidth(left + right, width, ""));
108
- const available = width - edgeWidth;
109
- const clippedLabel = truncateToWidth(label, Math.max(0, available - 2), "");
110
- const labelWidth = visibleWidth(clippedLabel);
111
- if (labelWidth === 0 || available < labelWidth + 2) {
112
- return `${brand(left)}${brand("─".repeat(available))}${brand(right)}`;
113
- }
114
- const fill = available - labelWidth - 2;
115
- const before = Math.min(3, fill);
116
- const after = fill - before;
117
- return `${brand(left)}${brand("─".repeat(before))} ${clippedLabel} ${brand("─".repeat(after))}${brand(right)}`;
118
- }
119
-
120
- function boxedLine(content: string, width: number): string {
121
- if (width <= 0) return "";
122
- if (width <= 2) return truncateToWidth(content, width, "");
123
- return `${brand("│")}${padRight(content, width - 2)}${brand("│")}`;
124
- }
125
-
126
- function twoColumn(left: string, right: string, leftWidth: number, rightWidth: number): string {
127
- return `${padRight(left, leftWidth)} ${brand("│")} ${padRight(right, rightWidth)}`;
128
- }
129
-
130
- const TIP_SETS = [
131
- [
132
- "",
133
- "Shortcuts & Commands",
134
- "/variants — model reasoning",
135
- "/compact — compress context",
136
- "/model — choose a model",
137
- "────────────────────────",
138
- "Keybindings",
139
- "Shift+Enter — new line",
140
- "Esc — cancel generation",
141
- "Ctrl+C — interrupt agent",
142
- ],
143
- [
144
- "",
145
- "Session",
146
- "/new — start a session",
147
- "/name — name this session",
148
- "/session — usage and stats",
149
- "────────────────────────",
150
- "Workflow",
151
- "Give Pi a goal and constraints",
152
- "Ask it to run the relevant tests",
153
- "Review changes before committing",
154
- ],
155
- [
156
- "",
157
- "Useful Commands",
158
- "/copy — copy last response",
159
- "/tree — navigate branches",
160
- "/reload — reload resources",
161
- "────────────────────────",
162
- "Extension locations",
163
- "Global: ~/.pi/agent/extensions",
164
- "Project: .pi/extensions",
165
- "Reload after making changes",
166
- ],
167
- [
168
- "",
169
- "Navigation",
170
- "Up/Down — command history",
171
- "Tab — autocomplete",
172
- "Ctrl+L — clear the screen",
173
- "────────────────────────",
174
- "Good defaults",
175
- "Keep edits scoped",
176
- "Test after refactoring",
177
- "Verify output before committing",
178
- ],
179
- ] as const;
180
-
181
- function getTipLines(index: number, theme: Theme): string[] {
182
- const selected = TIP_SETS[index % TIP_SETS.length] ?? TIP_SETS[0];
183
- return selected.map((line, lineIndex) => {
184
- if (lineIndex === 1 || lineIndex === 6) return brand(theme.bold(line));
185
- if (line.startsWith("─")) return brand(line);
186
- if (line.startsWith("/")) {
187
- const [command, ...rest] = line.split(" ");
188
- return `${theme.fg("accent", command ?? "")}${theme.fg("dim", ` ${rest.join(" ")}`)}`;
189
- }
190
- return theme.fg(lineIndex > 6 ? "muted" : "dim", line);
191
- });
98
+ function compactBoxLine(content: string, width: number, theme: Theme): string {
99
+ if (width < 4) return truncateToWidth(content, width, "");
100
+ return `${theme.fg("dim", "│")} ${padRight(content, width - 4)} ${theme.fg("dim", "│")}`;
192
101
  }
193
102
 
194
103
  class PiStartupHeader {
195
104
  private readonly pi: ExtensionAPI;
196
105
  private readonly ctx: ExtensionContext;
197
- private readonly tui: TUI;
198
- private frame = 0;
199
- private tipIndex = 0;
200
- private animationTimer?: ReturnType<typeof setInterval>;
201
- private tipTimer?: ReturnType<typeof setInterval>;
202
- private disposed = false;
203
-
204
- constructor(
205
- pi: ExtensionAPI,
206
- ctx: ExtensionContext,
207
- tui: TUI,
208
- ) {
106
+ private readonly branch: string | undefined;
107
+ private readonly tip: string;
108
+
109
+ constructor(pi: ExtensionAPI, ctx: ExtensionContext, tip: string) {
209
110
  this.pi = pi;
210
111
  this.ctx = ctx;
211
- this.tui = tui;
212
- this.animationTimer = setInterval(() => {
213
- if (this.disposed) return;
214
- if (this.frame >= LOGO_FRAME_COUNT - 1) {
215
- this.stopAnimation();
216
- return;
217
- }
218
- this.frame += 1;
219
- this.tui.requestRender();
220
- if (this.frame >= LOGO_FRAME_COUNT - 1) this.stopAnimation();
221
- }, LOGO_ANIMATION_INTERVAL_MS);
222
- this.animationTimer.unref?.();
223
-
224
- this.tipTimer = setInterval(() => {
225
- if (this.disposed) return;
226
- this.tipIndex = (this.tipIndex + 1) % TIP_SETS.length;
227
- this.tui.requestRender();
228
- }, TIP_ROTATION_INTERVAL_MS);
229
- this.tipTimer.unref?.();
112
+ this.branch = resolveGitBranch(ctx.cwd);
113
+ this.tip = tip;
230
114
  }
231
115
 
232
- private stopAnimation(): void {
233
- if (!this.animationTimer) return;
234
- clearInterval(this.animationTimer);
235
- this.animationTimer = undefined;
116
+ private tipLines(width: number, theme: Theme): string[] {
117
+ const indent = " ";
118
+ const text = `${theme.fg("text", theme.bold("Tip:"))}${theme.fg("dim", ` ${this.tip}`)}`;
119
+ return wrapTextWithAnsi(text, width - indent.length)
120
+ .map((line) => padRight(`${indent}${line}`, width));
236
121
  }
237
122
 
238
123
  render(width: number): string[] {
239
124
  if (width <= 0) return [];
240
125
  const theme = this.ctx.ui.theme;
241
- if (width < 16) return [truncateToWidth(theme.fg("accent", `Pi v${VERSION}`), width, "")];
242
-
243
- const innerWidth = width - 2;
244
- const isTwoColumn = innerWidth >= 64;
245
- const provider = extractProvider(this.ctx.model);
246
- const rawModel = this.ctx.model?.id ?? this.ctx.model?.name;
247
- const model = rawModel
248
- ? provider && !rawModel.startsWith(`${provider}/`) ? `${provider}/${rawModel}` : rawModel
249
- : "Default model";
250
- const effort = this.pi.getThinkingLevel();
251
- const cwd = formatCwd(this.ctx.cwd);
252
- const leftWidth = isTwoColumn ? Math.min(LEFT_PANEL_WIDTH, Math.floor(innerWidth * 0.55)) : innerWidth;
253
- const rightWidth = isTwoColumn ? innerWidth - leftWidth - 3 : 0;
254
- const logoLines = leftWidth >= ORCA_WIDTH * visibleWidth(LOGO_CELL)
255
- ? piLogoFrame(this.frame).map((line) => center(line, leftWidth))
256
- : ["", "", center(brand(theme.bold("Pi Coding Agent")), leftWidth), "", "", "", ""];
257
- const modelText = leftWidth < 34 ? `${model} (${effort})` : `${model} with ${effort} effort`;
258
- const leftLines = [
259
- ...logoLines,
260
- center(theme.bold("Let's build something great"), leftWidth),
261
- center(theme.fg("muted", truncateToWidth(modelText, leftWidth, "")), leftWidth),
262
- center(theme.fg("dim", truncateToWidth(cwd, leftWidth, "…")), leftWidth),
126
+ if (width < 28) return [truncateToWidth(theme.fg("text", theme.bold("KillerOS")), width, "")];
127
+
128
+ const panelWidth = Math.min(width, COMPACT_HEADER_MAX_WIDTH);
129
+ const innerWidth = panelWidth - 4;
130
+ const version = KILLEROS_VERSION ? theme.fg("dim", ` (v${KILLEROS_VERSION})`) : "";
131
+ const identity = `${theme.fg("dim", "›")} ${theme.fg("text", theme.bold("KillerOS"))}${version}`;
132
+ const thinkingLevel = this.pi.getThinkingLevel() as ThinkingLevel;
133
+ const reasoning = this.ctx.model?.reasoning === false
134
+ ? theme.fg("thinkingOff", "no reasoning")
135
+ : theme.fg(LEVEL_COLORS[thinkingLevel], thinkingLevel);
136
+ const agent = `${formatModel(this.ctx.model, theme)}${theme.fg("dim", " · ")}${reasoning}`;
137
+ const directory = formatCwd(this.ctx.cwd);
138
+ const repository = this.branch
139
+ ? `${directory} ${theme.fg("dim", ${this.branch}`)}`
140
+ : directory;
141
+ const modelCommand = commandBlue("/model");
142
+ const agentWidth = Math.max(0, innerWidth - visibleWidth(modelCommand) - 1);
143
+ const agentCommand = `${truncateToWidth(agent, agentWidth, "…")} ${modelCommand}`;
144
+ const border = (left: string, right: string): string => theme.fg("dim", `${left}${"─".repeat(panelWidth - 2)}${right}`);
145
+ const lines = [
146
+ border("", ""),
147
+ compactBoxLine(identity, panelWidth, theme),
148
+ compactBoxLine("", panelWidth, theme),
149
+ compactBoxLine(agentCommand, panelWidth, theme),
150
+ compactBoxLine(repository, panelWidth, theme),
151
+ border("╰", "╯"),
152
+ " ".repeat(panelWidth),
153
+ ...this.tipLines(panelWidth, theme),
263
154
  ];
264
- const tipLines = isTwoColumn ? getTipLines(this.tipIndex, theme) : [];
265
- const lines = [borderLine("╭", `${brand("Pi")} v${VERSION}`, "╮", width)];
266
- for (let index = 0; index < leftLines.length; index += 1) {
267
- const content = isTwoColumn
268
- ? twoColumn(leftLines[index] ?? "", tipLines[index] ?? "", leftWidth, rightWidth)
269
- : padRight(leftLines[index] ?? "", leftWidth);
270
- lines.push(boxedLine(content, width));
271
- }
272
- lines.push(borderLine("╰", "", "╯", width));
273
- return lines.map((line) => truncateToWidth(line, width, ""));
155
+ return lines;
274
156
  }
275
157
 
276
158
  invalidate(): void {}
277
-
278
- dispose(): void {
279
- if (this.disposed) return;
280
- this.disposed = true;
281
- this.stopAnimation();
282
- if (this.tipTimer) {
283
- clearInterval(this.tipTimer);
284
- this.tipTimer = undefined;
285
- }
286
- }
159
+ dispose(): void {}
287
160
  }
288
161
 
289
162
  const ANSI_REGEX = /\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
@@ -372,30 +245,57 @@ function reportError(ctx: ExtensionContext, area: string, error: unknown): void
372
245
  ctx.ui.notify(`${area}: ${message}`, "error");
373
246
  }
374
247
 
248
+ const ACTIVITY_WORDS = ["Brewing", "Pondering", "Tinkering", "Wrangling", "Noodling", "Cooking"] as const;
249
+
375
250
  function registerShellUi(pi: ExtensionAPI): void {
376
251
  let activeHeader: PiStartupHeader | undefined;
252
+ let activityWordIndex = 0;
253
+ let tipDeck: string[] = [];
254
+ const nextStartupTip = (): string => {
255
+ if (tipDeck.length === 0) tipDeck = shuffledTips();
256
+ return tipDeck.pop() ?? STARTUP_TIPS[0];
257
+ };
377
258
 
378
259
  pi.on("session_start", (_event, ctx) => {
379
260
  if (ctx.mode !== "tui") return;
380
261
  try {
381
- ctx.ui.setHeader((tui) => {
262
+ ctx.ui.setTheme("killeros");
263
+ const startupTip = nextStartupTip();
264
+ ctx.ui.setHeader(() => {
382
265
  activeHeader?.dispose();
383
- activeHeader = new PiStartupHeader(pi, ctx, tui);
266
+ activeHeader = new PiStartupHeader(pi, ctx, startupTip);
384
267
  return activeHeader;
385
268
  });
386
269
  ctx.ui.setWorkingIndicator({
387
- frames: ["◐", "◓", "◑", "◒"].map((frame) => ctx.ui.theme.fg("accent", frame)),
388
- intervalMs: 120,
270
+ frames: [
271
+ ctx.ui.theme.fg("dim", "✻"),
272
+ ctx.ui.theme.fg("muted", "✻"),
273
+ ctx.ui.theme.fg("accent", "✻"),
274
+ ctx.ui.theme.fg("muted", "✻"),
275
+ ],
276
+ intervalMs: 180,
389
277
  });
278
+ ctx.ui.setHiddenThinkingLabel("└ Thinking…");
390
279
  ctx.ui.setEditorComponent((tui, theme, keybindings) => new PiCodeEditor(tui, theme, keybindings));
391
280
  } catch (error) {
392
281
  reportError(ctx, "Killeros UI failed to initialize", error);
393
282
  }
394
283
  });
395
284
 
285
+ pi.on("agent_start", (_event, ctx) => {
286
+ if (ctx.mode !== "tui") return;
287
+ ctx.ui.setWorkingMessage(`${ACTIVITY_WORDS[activityWordIndex]}…`);
288
+ activityWordIndex = (activityWordIndex + 1) % ACTIVITY_WORDS.length;
289
+ });
290
+
291
+ pi.on("agent_end", (_event, ctx) => {
292
+ if (ctx.mode === "tui") ctx.ui.setWorkingMessage();
293
+ });
294
+
396
295
  pi.on("session_shutdown", () => {
397
296
  activeHeader?.dispose();
398
297
  activeHeader = undefined;
298
+ activityWordIndex = 0;
399
299
  });
400
300
  }
401
301
 
@@ -1089,10 +989,6 @@ export function formatCost(usd: number): string {
1089
989
  return `$${usd.toFixed(2)}`;
1090
990
  }
1091
991
 
1092
- export function resolveShortcutHint(): string {
1093
- return process.env.PI_SHORTCUT_HINT?.trim() || "/variants";
1094
- }
1095
-
1096
992
  function formatTime(milliseconds: number): string {
1097
993
  const totalSeconds = Math.max(0, Math.floor(milliseconds / 1_000));
1098
994
  if (totalSeconds < 60) return `${totalSeconds}s`;
@@ -1104,20 +1000,22 @@ function formatTime(milliseconds: number): string {
1104
1000
  function formatTokens(value: number): string {
1105
1001
  const amount = Math.max(0, value);
1106
1002
  if (amount < 1_000) return `${Math.round(amount)}`;
1107
- if (amount >= 1_000_000) return `${(amount / 1_000_000).toFixed(amount >= 10_000_000 ? 0 : 1)}M`;
1108
- return `${(amount / 1_000).toFixed(amount >= 100_000 ? 0 : 1)}k`;
1003
+ if (amount >= 1_000_000) {
1004
+ const precision = amount >= 10_000_000 ? 0 : 1;
1005
+ return `${Number((amount / 1_000_000).toFixed(precision))}M`;
1006
+ }
1007
+ const precision = amount >= 100_000 ? 0 : 1;
1008
+ return `${Number((amount / 1_000).toFixed(precision))}k`;
1109
1009
  }
1110
1010
 
1111
1011
  export function formatContextProgress(tokensUsed: number | null, contextWindow: number, theme: Theme): string {
1112
- if (tokensUsed === null) return theme.fg("dim", "[░░░░░░░░░░] —");
1012
+ if (tokensUsed === null) return theme.fg("dim", "—% left ()");
1113
1013
  const windowSize = contextWindow > 0 ? contextWindow : 128_000;
1114
1014
  const remaining = Math.max(0, Math.min(windowSize, windowSize - Math.max(0, tokensUsed)));
1115
1015
  const percentLeft = Math.max(0, Math.min(100, Math.round((remaining / windowSize) * 100)));
1116
- const filled = Math.round((percentLeft / 100) * 10);
1117
- const bar = "█".repeat(filled) + "░".repeat(10 - filled);
1118
1016
  const color: ThemeColor = percentLeft < 20 ? "error" : percentLeft <= 50 ? "warning" : "success";
1119
- const warning = percentLeft < 15 ? " /compact" : "";
1120
- return theme.fg(color, `[${bar}] ${percentLeft}% left (${formatTokens(remaining)})${warning}`);
1017
+ const action = percentLeft < 15 ? " · /compact" : "";
1018
+ return theme.fg(color, `${percentLeft}% left (${formatTokens(remaining)})${action}`);
1121
1019
  }
1122
1020
 
1123
1021
  function sumSessionCost(ctx: ExtensionContext): number {
@@ -1133,9 +1031,80 @@ function sumSessionCost(ctx: ExtensionContext): number {
1133
1031
  return total;
1134
1032
  }
1135
1033
 
1136
- function formatModel(model: ExtensionContext["model"], theme: Theme): string {
1137
- if (!model) return theme.fg("dim", "no model");
1138
- return `${theme.fg("dim", `${model.provider}/`)}${theme.fg("accent", model.id)}`;
1034
+ const PROVIDER_LABELS: Readonly<Record<string, string>> = {
1035
+ "amazon-bedrock": "Amazon Bedrock",
1036
+ "azure-openai-responses": "Azure OpenAI",
1037
+ "github-copilot": "GitHub Copilot",
1038
+ "google-vertex": "Google Vertex",
1039
+ "openai-codex": "OpenAI",
1040
+ anthropic: "Anthropic",
1041
+ deepseek: "DeepSeek",
1042
+ google: "Google",
1043
+ ollama: "Ollama",
1044
+ openai: "OpenAI",
1045
+ openrouter: "OpenRouter",
1046
+ };
1047
+
1048
+ const PROVIDER_WORDS: Readonly<Record<string, string>> = {
1049
+ ai: "AI",
1050
+ api: "API",
1051
+ deepseek: "DeepSeek",
1052
+ github: "GitHub",
1053
+ llm: "LLM",
1054
+ openai: "OpenAI",
1055
+ openrouter: "OpenRouter",
1056
+ };
1057
+
1058
+ function formatProviderName(provider: string): string {
1059
+ const normalized = provider.trim();
1060
+ const known = PROVIDER_LABELS[normalized.toLocaleLowerCase()];
1061
+ if (known) return known;
1062
+ return normalized
1063
+ .split(/[-_]+/u)
1064
+ .filter(Boolean)
1065
+ .map((word) => PROVIDER_WORDS[word.toLocaleLowerCase()] ?? `${word.charAt(0).toLocaleUpperCase()}${word.slice(1)}`)
1066
+ .join(" ") || "Unknown provider";
1067
+ }
1068
+
1069
+ function modelDisplayName(model: NonNullable<ExtensionContext["model"]>): string {
1070
+ return model.name?.trim() || model.id;
1071
+ }
1072
+
1073
+ function formatModel(model: ExtensionContext["model"], theme: Theme, includeProvider = true): string {
1074
+ if (!model) return theme.fg("dim", "No model");
1075
+ const name = theme.fg("text", theme.bold(modelDisplayName(model)));
1076
+ return includeProvider ? `${name} ${theme.fg("dim", formatProviderName(model.provider))}` : name;
1077
+ }
1078
+
1079
+ function compactDirectory(cwd: string): string {
1080
+ if (cwd === "~" || cwd === "/" || /^[A-Za-z]:[\\/]?$/u.test(cwd)) return cwd;
1081
+ const normalized = cwd.replace(/\\/gu, "/").replace(/\/$/u, "");
1082
+ const finalSegment = normalized.split("/").at(-1);
1083
+ return finalSegment ? `…/${finalSegment}` : cwd;
1084
+ }
1085
+
1086
+ function joinFooterParts(parts: string[], theme: Theme): string {
1087
+ return parts.filter(Boolean).join(theme.fg("dim", " · "));
1088
+ }
1089
+
1090
+ function footerRowFits(left: string, right: string, width: number): boolean {
1091
+ const contentWidth = visibleWidth(left) + (right ? visibleWidth(right) + 1 : 0);
1092
+ return contentWidth + 2 <= width;
1093
+ }
1094
+
1095
+ function renderFooterRow(left: string, right: string, width: number): string {
1096
+ if (width <= 0) return "";
1097
+ if (width < 3) return " ".repeat(width);
1098
+
1099
+ const innerWidth = width - 2;
1100
+ if (!right) return ` ${padRight(left, innerWidth)} `;
1101
+
1102
+ const clippedRight = truncateToWidth(right, innerWidth, "");
1103
+ const rightWidth = visibleWidth(clippedRight);
1104
+ const leftBudget = Math.max(0, innerWidth - rightWidth - 1);
1105
+ const clippedLeft = truncateToWidth(left, leftBudget, "…");
1106
+ const gap = " ".repeat(Math.max(0, innerWidth - visibleWidth(clippedLeft) - rightWidth));
1107
+ return ` ${clippedLeft}${gap}${clippedRight} `;
1139
1108
  }
1140
1109
 
1141
1110
  function registerFooter(pi: ExtensionAPI): void {
@@ -1167,27 +1136,39 @@ function registerFooter(pi: ExtensionAPI): void {
1167
1136
  const model = currentModel ?? ctx.model;
1168
1137
  const level = model?.reasoning === false
1169
1138
  ? theme.fg("thinkingOff", "no reasoning")
1170
- : theme.fg(LEVEL_COLORS[thinkingLevel], LEVEL_LABELS[thinkingLevel]);
1139
+ : theme.fg(LEVEL_COLORS[thinkingLevel], thinkingLevel);
1171
1140
  const usage = ctx.getContextUsage();
1172
- const context = formatContextProgress(usage?.tokens ?? null, usage?.contextWindow ?? 128_000, theme);
1141
+ const contextWindow = usage?.contextWindow ?? model?.contextWindow ?? 128_000;
1142
+ const context = formatContextProgress(usage?.tokens ?? null, contextWindow, theme);
1173
1143
  const branch = footerData.getGitBranch();
1174
- const parts = [
1175
- formatModel(model, theme),
1144
+ const signature = formatModel(model, theme);
1145
+ const fullDirectory = theme.fg("dim", cwd);
1146
+ const focusedDirectory = theme.fg("dim", compactDirectory(cwd));
1147
+ const rich = joinFooterParts([
1148
+ signature,
1176
1149
  level,
1177
1150
  context,
1178
1151
  branch ? theme.fg("dim", branch) : "",
1179
1152
  theme.fg("dim", formatTime(Date.now() - sessionStart)),
1180
- ];
1181
- const cost = sumSessionCost(ctx);
1182
- if (cost > 0) parts.push(theme.fg("dim", formatCost(cost)));
1183
- const hint = resolveShortcutHint();
1184
- if (hint) parts.push(theme.fg("dim", hint));
1185
- const separator = theme.fg("dim", "·");
1186
- const left = ` ${parts.filter(Boolean).join(` ${separator} `)} `;
1187
- const rightBudget = Math.max(0, width - visibleWidth(left) - 1);
1188
- const right = theme.fg("dim", truncateToWidth(cwd, rightBudget, "…"));
1189
- const gap = " ".repeat(Math.max(1, width - visibleWidth(left) - visibleWidth(right)));
1190
- return [truncateToWidth(left + gap + right, width, "")];
1153
+ theme.fg("dim", formatCost(sumSessionCost(ctx))),
1154
+ ], theme);
1155
+ const focused = joinFooterParts([signature, context], theme);
1156
+
1157
+ if (footerRowFits(rich, fullDirectory, width)) {
1158
+ return [renderFooterRow(rich, fullDirectory, width)];
1159
+ }
1160
+ if (footerRowFits(rich, focusedDirectory, width)) {
1161
+ return [renderFooterRow(rich, focusedDirectory, width)];
1162
+ }
1163
+ if (footerRowFits(focused, focusedDirectory, width)) {
1164
+ return [renderFooterRow(focused, focusedDirectory, width)];
1165
+ }
1166
+ if (footerRowFits(focused, "", width)) {
1167
+ return [renderFooterRow(focused, "", width)];
1168
+ }
1169
+
1170
+ const essentialModel = formatModel(model, theme, false);
1171
+ return [renderFooterRow(essentialModel, context, width)];
1191
1172
  },
1192
1173
  };
1193
1174
  });
package/README.md CHANGED
@@ -29,16 +29,18 @@ pi install git:github.com/KyrosHendrix/pi-KillerOS
29
29
  Pin an install to a release:
30
30
 
31
31
  ```bash
32
- pi install git:github.com/KyrosHendrix/pi-KillerOS@v1.0.0
32
+ pi install git:github.com/KyrosHendrix/pi-KillerOS@v1.2.0
33
33
  ```
34
34
 
35
35
  Add `-l` to either command for a project-only install. Restart Pi after installing.
36
36
 
37
37
  ## Features
38
38
 
39
- - Animated startup header with current model, reasoning level, and working-directory context
39
+ - 52-column Compact startup card with inline version, polished model/provider identity, adjacent `/model`, directory, conditional Git branch, and a shuffled session-stable tip
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
40
42
  - Framed multiline editor with Shift+Enter support
41
- - Footer with model, reasoning, context remaining, Git branch, elapsed time, and cost
43
+ - Responsive footer with polished model/provider identity and plain-language context remaining; reasoning, Git branch, elapsed time, cost, and path cut down by available width
42
44
  - `/variants` selector and direct reasoning-level arguments
43
45
  - `question` tool with filtering, keyboard selection, custom answers, history, cancellation, and resize-safe rendering
44
46
  - Mid-prompt slash completion with current Pi `0.82.1` commands, extensions, prompts, and skills
@@ -58,13 +60,9 @@ Supported reasoning levels are `off`, `minimal`, `low`, `medium`, `high`, `xhigh
58
60
 
59
61
  ## Configuration
60
62
 
61
- KillerOS displays provider costs in USD.
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.
62
64
 
63
- Set a custom footer shortcut hint with:
64
-
65
- ```text
66
- PI_SHORTCUT_HINT=/variants
67
- ```
65
+ KillerOS displays session costs in USD. The footer uses Pi's human-readable model name when available, keeps the provider visually secondary, and renders context as `percent left (tokens)` without a progress bar.
68
66
 
69
67
  ## Behavior by mode
70
68
 
@@ -83,7 +81,7 @@ npm ci
83
81
  npm run check
84
82
  npm test
85
83
  npm pack --dry-run
86
- pi -e . --mode rpc
84
+ pi -ne -e . --mode rpc
87
85
  ```
88
86
 
89
87
  The package manifest lists Pi’s built-in modules as peer dependencies, so npm does not bundle a second copy.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "killeros",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "A production-hardened TUI and workflow extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -21,7 +21,9 @@
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"
@@ -33,6 +35,9 @@
33
35
  "pi": {
34
36
  "extensions": [
35
37
  "./Killeros.ts"
38
+ ],
39
+ "themes": [
40
+ "./themes/killeros.json"
36
41
  ]
37
42
  },
38
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": "#0a0a0a",
8
+ "surface": "#121212",
9
+ "surfaceRaised": "#1a1a1a",
10
+ "line": "#404040",
11
+ "lineMuted": "#2e2e2e",
12
+ "text": "#f2f2f2",
13
+ "muted": "#adadad",
14
+ "dim": "#808080",
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": "#0a0a0a",
82
+ "cardBg": "#121212",
83
+ "infoBg": "#1a1a1a"
84
+ }
85
+ }