killeros 1.1.0 → 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 CHANGED
@@ -2,19 +2,22 @@
2
2
 
3
3
  All notable changes to KillerOS are documented here.
4
4
 
5
- ## [1.1.0] - 2026-07-31
5
+ ## [1.2.0] - 2026-07-30
6
6
 
7
7
  ### Added
8
8
 
9
- - Compact startup card with model, reasoning level, working directory, context remaining, and detected package capabilities.
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.
10
12
  - Packaged KillerOS theme with coral accents and one neutral tool-call surface across pending, success, and error states.
11
13
  - Single-glyph Spark activity indicator with a restrained color pulse.
12
14
  - Claude-adjacent activity word bank that advances between agent runs.
13
15
  - Static `└ Thinking…` label for hidden reasoning blocks.
14
- - Responsive header tests across terminal widths from 1 to 100 columns.
16
+ - Responsive header and footer tests across narrow terminal widths.
15
17
 
16
18
  ### Changed
17
19
 
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.
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,6 +1,6 @@
1
+ import { execFileSync } from "node:child_process";
1
2
  import { readFileSync } from "node:fs";
2
3
  import os from "node:os";
3
- import { dirname, join } from "node:path";
4
4
  import {
5
5
  CustomEditor,
6
6
  DynamicBorder,
@@ -29,25 +29,53 @@ import {
29
29
  } from "@earendil-works/pi-tui";
30
30
  import { Type } from "typebox";
31
31
 
32
- const BRAND_RGB = "215;119;87";
32
+ const COMMAND_BLUE_RGB = "120;169;255";
33
33
  const FOOTER_REFRESH_INTERVAL_MS = 1_000;
34
- const COMPACT_HEADER_MAX_WIDTH = 76;
34
+ const COMPACT_HEADER_MAX_WIDTH = 52;
35
35
 
36
- 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`;
37
37
 
38
- function readPackageMetadata(path: string | URL): { name?: string; version?: string } {
38
+ function readPackageVersion(path: string | URL): string | undefined {
39
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
- };
40
+ const value = JSON.parse(readFileSync(path, "utf8")) as { version?: unknown };
41
+ return typeof value.version === "string" ? value.version : undefined;
45
42
  } catch {
46
- return {};
43
+ return undefined;
47
44
  }
48
45
  }
49
46
 
50
- const KILLEROS_VERSION = readPackageMetadata(new URL("./package.json", import.meta.url)).version;
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.",
53
+ ] as const;
54
+
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;
78
+ }
51
79
 
52
80
  function formatCwd(cwd: string): string {
53
81
  const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
@@ -67,62 +95,6 @@ function padRight(text: string, width: number): string {
67
95
  return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
68
96
  }
69
97
 
70
- interface CapabilitySourceInfo {
71
- path?: string;
72
- source?: string;
73
- baseDir?: string;
74
- }
75
-
76
- interface CompactCapability {
77
- label: string;
78
- version?: string;
79
- }
80
-
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());
86
- }
87
-
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 });
113
- }
114
- return [...capabilities.values()].sort((left, right) => left.label.localeCompare(right.label));
115
- }
116
-
117
- function alignEdges(left: string, right: string, width: number): string {
118
- if (width <= 0) return "";
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, "");
124
- }
125
-
126
98
  function compactBoxLine(content: string, width: number, theme: Theme): string {
127
99
  if (width < 4) return truncateToWidth(content, width, "");
128
100
  return `${theme.fg("dim", "│")} ${padRight(content, width - 4)} ${theme.fg("dim", "│")}`;
@@ -131,50 +103,55 @@ function compactBoxLine(content: string, width: number, theme: Theme): string {
131
103
  class PiStartupHeader {
132
104
  private readonly pi: ExtensionAPI;
133
105
  private readonly ctx: ExtensionContext;
134
- private readonly capabilities: CompactCapability[];
106
+ private readonly branch: string | undefined;
107
+ private readonly tip: string;
135
108
 
136
- constructor(pi: ExtensionAPI, ctx: ExtensionContext) {
109
+ constructor(pi: ExtensionAPI, ctx: ExtensionContext, tip: string) {
137
110
  this.pi = pi;
138
111
  this.ctx = ctx;
139
- this.capabilities = collectCompactCapabilities(pi);
112
+ this.branch = resolveGitBranch(ctx.cwd);
113
+ this.tip = tip;
140
114
  }
141
115
 
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`);
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));
148
121
  }
149
122
 
150
123
  render(width: number): string[] {
151
124
  if (width <= 0) return [];
152
125
  const theme = this.ctx.ui.theme;
153
- if (width < 28) return [truncateToWidth(brand(theme.bold("KillerOS")), width, "")];
126
+ if (width < 28) return [truncateToWidth(theme.fg("text", theme.bold("KillerOS")), width, "")];
154
127
 
155
128
  const panelWidth = Math.min(width, COMPACT_HEADER_MAX_WIDTH);
156
129
  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()}`;
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}`;
161
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}`;
162
144
  const border = (left: string, right: string): string => theme.fg("dim", `${left}${"─".repeat(panelWidth - 2)}${right}`);
163
145
  const lines = [
164
146
  border("╭", "╮"),
165
- compactBoxLine(alignEdges(identity, theme.fg("success", "READY"), innerWidth), panelWidth, theme),
147
+ compactBoxLine(identity, panelWidth, theme),
166
148
  compactBoxLine("", panelWidth, theme),
167
- compactBoxLine(alignEdges(agent, theme.fg("dim", "/model"), innerWidth), panelWidth, theme),
168
- compactBoxLine(alignEdges(directory, this.contextText(theme), innerWidth), panelWidth, theme),
149
+ compactBoxLine(agentCommand, panelWidth, theme),
150
+ compactBoxLine(repository, panelWidth, theme),
151
+ border("╰", "╯"),
152
+ " ".repeat(panelWidth),
153
+ ...this.tipLines(panelWidth, theme),
169
154
  ];
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));
176
- }
177
- lines.push(border("╰", "╯"));
178
155
  return lines;
179
156
  }
180
157
 
@@ -273,14 +250,20 @@ const ACTIVITY_WORDS = ["Brewing", "Pondering", "Tinkering", "Wrangling", "Noodl
273
250
  function registerShellUi(pi: ExtensionAPI): void {
274
251
  let activeHeader: PiStartupHeader | undefined;
275
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
+ };
276
258
 
277
259
  pi.on("session_start", (_event, ctx) => {
278
260
  if (ctx.mode !== "tui") return;
279
261
  try {
280
262
  ctx.ui.setTheme("killeros");
263
+ const startupTip = nextStartupTip();
281
264
  ctx.ui.setHeader(() => {
282
265
  activeHeader?.dispose();
283
- activeHeader = new PiStartupHeader(pi, ctx);
266
+ activeHeader = new PiStartupHeader(pi, ctx, startupTip);
284
267
  return activeHeader;
285
268
  });
286
269
  ctx.ui.setWorkingIndicator({
@@ -1006,10 +989,6 @@ export function formatCost(usd: number): string {
1006
989
  return `$${usd.toFixed(2)}`;
1007
990
  }
1008
991
 
1009
- export function resolveShortcutHint(): string {
1010
- return process.env.PI_SHORTCUT_HINT?.trim() || "/variants";
1011
- }
1012
-
1013
992
  function formatTime(milliseconds: number): string {
1014
993
  const totalSeconds = Math.max(0, Math.floor(milliseconds / 1_000));
1015
994
  if (totalSeconds < 60) return `${totalSeconds}s`;
@@ -1021,20 +1000,22 @@ function formatTime(milliseconds: number): string {
1021
1000
  function formatTokens(value: number): string {
1022
1001
  const amount = Math.max(0, value);
1023
1002
  if (amount < 1_000) return `${Math.round(amount)}`;
1024
- if (amount >= 1_000_000) return `${(amount / 1_000_000).toFixed(amount >= 10_000_000 ? 0 : 1)}M`;
1025
- 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`;
1026
1009
  }
1027
1010
 
1028
1011
  export function formatContextProgress(tokensUsed: number | null, contextWindow: number, theme: Theme): string {
1029
- if (tokensUsed === null) return theme.fg("dim", "[░░░░░░░░░░] —");
1012
+ if (tokensUsed === null) return theme.fg("dim", "—% left ()");
1030
1013
  const windowSize = contextWindow > 0 ? contextWindow : 128_000;
1031
1014
  const remaining = Math.max(0, Math.min(windowSize, windowSize - Math.max(0, tokensUsed)));
1032
1015
  const percentLeft = Math.max(0, Math.min(100, Math.round((remaining / windowSize) * 100)));
1033
- const filled = Math.round((percentLeft / 100) * 10);
1034
- const bar = "█".repeat(filled) + "░".repeat(10 - filled);
1035
1016
  const color: ThemeColor = percentLeft < 20 ? "error" : percentLeft <= 50 ? "warning" : "success";
1036
- const warning = percentLeft < 15 ? " /compact" : "";
1037
- 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}`);
1038
1019
  }
1039
1020
 
1040
1021
  function sumSessionCost(ctx: ExtensionContext): number {
@@ -1050,9 +1031,80 @@ function sumSessionCost(ctx: ExtensionContext): number {
1050
1031
  return total;
1051
1032
  }
1052
1033
 
1053
- function formatModel(model: ExtensionContext["model"], theme: Theme): string {
1054
- if (!model) return theme.fg("dim", "no model");
1055
- 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} `;
1056
1108
  }
1057
1109
 
1058
1110
  function registerFooter(pi: ExtensionAPI): void {
@@ -1084,27 +1136,39 @@ function registerFooter(pi: ExtensionAPI): void {
1084
1136
  const model = currentModel ?? ctx.model;
1085
1137
  const level = model?.reasoning === false
1086
1138
  ? theme.fg("thinkingOff", "no reasoning")
1087
- : theme.fg(LEVEL_COLORS[thinkingLevel], LEVEL_LABELS[thinkingLevel]);
1139
+ : theme.fg(LEVEL_COLORS[thinkingLevel], thinkingLevel);
1088
1140
  const usage = ctx.getContextUsage();
1089
- 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);
1090
1143
  const branch = footerData.getGitBranch();
1091
- const parts = [
1092
- 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,
1093
1149
  level,
1094
1150
  context,
1095
1151
  branch ? theme.fg("dim", branch) : "",
1096
1152
  theme.fg("dim", formatTime(Date.now() - sessionStart)),
1097
- ];
1098
- const cost = sumSessionCost(ctx);
1099
- if (cost > 0) parts.push(theme.fg("dim", formatCost(cost)));
1100
- const hint = resolveShortcutHint();
1101
- if (hint) parts.push(theme.fg("dim", hint));
1102
- const separator = theme.fg("dim", "·");
1103
- const left = ` ${parts.filter(Boolean).join(` ${separator} `)} `;
1104
- const rightBudget = Math.max(0, width - visibleWidth(left) - 1);
1105
- const right = theme.fg("dim", truncateToWidth(cwd, rightBudget, "…"));
1106
- const gap = " ".repeat(Math.max(1, width - visibleWidth(left) - visibleWidth(right)));
1107
- 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)];
1108
1172
  },
1109
1173
  };
1110
1174
  });
package/README.md CHANGED
@@ -29,18 +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.1.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
- - Compact KillerOS startup card with model, directory, context, and loaded capability state
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
40
  - Cohesive dark theme with coral accents and neutral tool-call containers across pending, success, and error states
41
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
- - 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
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
@@ -62,13 +62,7 @@ Supported reasoning levels are `off`, `minimal`, `low`, `medium`, `high`, `xhigh
62
62
 
63
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
64
 
65
- KillerOS displays provider costs in USD.
66
-
67
- Set a custom footer shortcut hint with:
68
-
69
- ```text
70
- PI_SHORTCUT_HINT=/variants
71
- ```
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.
72
66
 
73
67
  ## Behavior by mode
74
68
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "killeros",
3
- "version": "1.1.0",
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": [
@@ -4,14 +4,14 @@
4
4
  "vars": {
5
5
  "coral": "#d77757",
6
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",
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
15
  "success": "#8fa88b",
16
16
  "error": "#c8786c",
17
17
  "warning": "#bda36c",
@@ -78,8 +78,8 @@
78
78
  "bashMode": "warning"
79
79
  },
80
80
  "export": {
81
- "pageBg": "#090b0e",
82
- "cardBg": "#101419",
83
- "infoBg": "#151a20"
81
+ "pageBg": "#0a0a0a",
82
+ "cardBg": "#121212",
83
+ "infoBg": "#1a1a1a"
84
84
  }
85
85
  }