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 +9 -6
- package/Killeros.ts +187 -123
- package/README.md +4 -10
- package/package.json +1 -1
- package/themes/killeros.json +11 -11
package/CHANGELOG.md
CHANGED
|
@@ -2,19 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to KillerOS are documented here.
|
|
4
4
|
|
|
5
|
-
## [1.
|
|
5
|
+
## [1.2.0] - 2026-07-30
|
|
6
6
|
|
|
7
7
|
### Added
|
|
8
8
|
|
|
9
|
-
- Compact startup card with model, reasoning level, working directory,
|
|
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
|
|
16
|
+
- Responsive header and footer tests across narrow terminal widths.
|
|
15
17
|
|
|
16
18
|
### Changed
|
|
17
19
|
|
|
18
|
-
- Replaced the animated startup illustration and
|
|
19
|
-
- Standardized product branding on mixed-case `KillerOS` and the `› KillerOS` lockup.
|
|
20
|
-
-
|
|
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
|
|
32
|
+
const COMMAND_BLUE_RGB = "120;169;255";
|
|
33
33
|
const FOOTER_REFRESH_INTERVAL_MS = 1_000;
|
|
34
|
-
const COMPACT_HEADER_MAX_WIDTH =
|
|
34
|
+
const COMPACT_HEADER_MAX_WIDTH = 52;
|
|
35
35
|
|
|
36
|
-
const
|
|
36
|
+
const commandBlue = (text: string): string => `\x1B[38;2;${COMMAND_BLUE_RGB}m${text}\x1B[39m`;
|
|
37
37
|
|
|
38
|
-
function
|
|
38
|
+
function readPackageVersion(path: string | URL): string | undefined {
|
|
39
39
|
try {
|
|
40
|
-
const value = JSON.parse(readFileSync(path, "utf8")) as {
|
|
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 =
|
|
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
|
|
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.
|
|
112
|
+
this.branch = resolveGitBranch(ctx.cwd);
|
|
113
|
+
this.tip = tip;
|
|
140
114
|
}
|
|
141
115
|
|
|
142
|
-
private
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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(
|
|
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 = `${
|
|
159
|
-
const
|
|
160
|
-
const
|
|
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(
|
|
147
|
+
compactBoxLine(identity, panelWidth, theme),
|
|
166
148
|
compactBoxLine("", panelWidth, theme),
|
|
167
|
-
compactBoxLine(
|
|
168
|
-
compactBoxLine(
|
|
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)
|
|
1025
|
-
|
|
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
|
|
1037
|
-
return theme.fg(color,
|
|
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
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
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],
|
|
1139
|
+
: theme.fg(LEVEL_COLORS[thinkingLevel], thinkingLevel);
|
|
1088
1140
|
const usage = ctx.getContextUsage();
|
|
1089
|
-
const
|
|
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
|
|
1092
|
-
|
|
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
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
if (
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
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.
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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
package/themes/killeros.json
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
"vars": {
|
|
5
5
|
"coral": "#d77757",
|
|
6
6
|
"coralBright": "#e58b6d",
|
|
7
|
-
"canvas": "#
|
|
8
|
-
"surface": "#
|
|
9
|
-
"surfaceRaised": "#
|
|
10
|
-
"line": "#
|
|
11
|
-
"lineMuted": "#
|
|
12
|
-
"text": "#
|
|
13
|
-
"muted": "#
|
|
14
|
-
"dim": "#
|
|
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": "#
|
|
82
|
-
"cardBg": "#
|
|
83
|
-
"infoBg": "#
|
|
81
|
+
"pageBg": "#0a0a0a",
|
|
82
|
+
"cardBg": "#121212",
|
|
83
|
+
"infoBg": "#1a1a1a"
|
|
84
84
|
}
|
|
85
85
|
}
|