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 +23 -0
- package/Killeros.ts +228 -247
- package/README.md +8 -10
- package/package.json +7 -2
- package/themes/killeros.json +85 -0
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
|
|
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
|
|
36
|
+
const commandBlue = (text: string): string => `\x1B[38;2;${COMMAND_BLUE_RGB}m${text}\x1B[39m`;
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
52
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
198
|
-
private
|
|
199
|
-
|
|
200
|
-
|
|
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.
|
|
212
|
-
this.
|
|
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
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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 <
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
const
|
|
245
|
-
const
|
|
246
|
-
const
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
const
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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.
|
|
262
|
+
ctx.ui.setTheme("killeros");
|
|
263
|
+
const startupTip = nextStartupTip();
|
|
264
|
+
ctx.ui.setHeader(() => {
|
|
382
265
|
activeHeader?.dispose();
|
|
383
|
-
activeHeader = new PiStartupHeader(pi, ctx,
|
|
266
|
+
activeHeader = new PiStartupHeader(pi, ctx, startupTip);
|
|
384
267
|
return activeHeader;
|
|
385
268
|
});
|
|
386
269
|
ctx.ui.setWorkingIndicator({
|
|
387
|
-
frames: [
|
|
388
|
-
|
|
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)
|
|
1108
|
-
|
|
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
|
|
1120
|
-
return theme.fg(color,
|
|
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
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
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],
|
|
1139
|
+
: theme.fg(LEVEL_COLORS[thinkingLevel], thinkingLevel);
|
|
1171
1140
|
const usage = ctx.getContextUsage();
|
|
1172
|
-
const
|
|
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
|
|
1175
|
-
|
|
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
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
if (
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
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.
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
"
|
|
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
|
+
}
|