killeros 1.4.8 → 1.5.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 +21 -4
- package/Killeros.ts +25 -2542
- package/README.md +23 -8
- package/killeros/commands.ts +164 -0
- package/killeros/concise.ts +23 -0
- package/killeros/display.ts +39 -0
- package/killeros/errors.ts +6 -0
- package/killeros/footer.ts +209 -0
- package/killeros/goals.ts +720 -0
- package/killeros/hooks.ts +219 -0
- package/killeros/init.ts +470 -0
- package/killeros/personal-instructions.ts +66 -0
- package/killeros/question.ts +468 -0
- package/killeros/runtime.ts +61 -0
- package/killeros/shell-ui.ts +270 -0
- package/killeros/subagent-lifecycle.ts +532 -0
- package/killeros/subagent-process.ts +601 -0
- package/killeros/subagent-ui.ts +227 -0
- package/killeros/subagents.ts +1917 -0
- package/killeros/variants.ts +136 -0
- package/package.json +7 -6
- package/subagent-lifecycle.ts +1 -506
- package/subagent-process.ts +1 -595
- package/subagent-ui.ts +1 -227
- package/subagents.ts +1 -1556
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import {
|
|
4
|
+
CustomEditor,
|
|
5
|
+
DynamicBorder,
|
|
6
|
+
VERSION,
|
|
7
|
+
type ExtensionAPI,
|
|
8
|
+
type ExtensionContext,
|
|
9
|
+
type KeybindingsManager,
|
|
10
|
+
type Theme,
|
|
11
|
+
} from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import {
|
|
13
|
+
Container,
|
|
14
|
+
Text,
|
|
15
|
+
truncateToWidth,
|
|
16
|
+
visibleWidth,
|
|
17
|
+
wrapTextWithAnsi,
|
|
18
|
+
type EditorTheme,
|
|
19
|
+
type TUI,
|
|
20
|
+
} from "@earendil-works/pi-tui";
|
|
21
|
+
import { formatCwd, padRight } from "./display.ts";
|
|
22
|
+
import { reportError } from "./errors.ts";
|
|
23
|
+
import { formatModel } from "./footer.ts";
|
|
24
|
+
import { LEVEL_COLORS, type ThinkingLevel } from "./variants.ts";
|
|
25
|
+
|
|
26
|
+
const COMMAND_BLUE_RGB = "120;169;255";
|
|
27
|
+
const COMPACT_HEADER_MAX_WIDTH = 52;
|
|
28
|
+
|
|
29
|
+
const commandBlue = (text: string): string => `\x1B[38;2;${COMMAND_BLUE_RGB}m${text}\x1B[39m`;
|
|
30
|
+
|
|
31
|
+
function readPackageVersion(path: string | URL): string | undefined {
|
|
32
|
+
try {
|
|
33
|
+
const value = JSON.parse(readFileSync(path, "utf8")) as { version?: unknown };
|
|
34
|
+
return typeof value.version === "string" ? value.version : undefined;
|
|
35
|
+
} catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const KILLEROS_VERSION = readPackageVersion(new URL("../package.json", import.meta.url));
|
|
41
|
+
|
|
42
|
+
const STARTUP_TIPS = [
|
|
43
|
+
"Press Shift+Enter to insert a line break without sending.",
|
|
44
|
+
"Run /variants to tune the model's reasoning depth.",
|
|
45
|
+
"Type / to browse every command available in this session.",
|
|
46
|
+
] as const;
|
|
47
|
+
|
|
48
|
+
function resolveGitBranch(cwd: string): string | undefined {
|
|
49
|
+
try {
|
|
50
|
+
const branch = execFileSync("git", ["-C", cwd, "rev-parse", "--abbrev-ref", "HEAD"], {
|
|
51
|
+
encoding: "utf8",
|
|
52
|
+
maxBuffer: 64 * 1024,
|
|
53
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
54
|
+
timeout: 500,
|
|
55
|
+
windowsHide: true,
|
|
56
|
+
}).trim();
|
|
57
|
+
if (!branch) return undefined;
|
|
58
|
+
return branch === "HEAD" ? "detached" : branch;
|
|
59
|
+
} catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function shuffledTips(): string[] {
|
|
65
|
+
const tips = [...STARTUP_TIPS];
|
|
66
|
+
for (let index = tips.length - 1; index > 0; index -= 1) {
|
|
67
|
+
const swapIndex = Math.floor(Math.random() * (index + 1));
|
|
68
|
+
[tips[index], tips[swapIndex]] = [tips[swapIndex]!, tips[index]!];
|
|
69
|
+
}
|
|
70
|
+
return tips;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function compactBoxLine(content: string, width: number, theme: Theme): string {
|
|
74
|
+
if (width < 4) return truncateToWidth(content, width, "");
|
|
75
|
+
return `${theme.fg("dim", "│")} ${padRight(content, width - 4)} ${theme.fg("dim", "│")}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
class PiStartupHeader {
|
|
79
|
+
private readonly pi: ExtensionAPI;
|
|
80
|
+
private readonly ctx: ExtensionContext;
|
|
81
|
+
private readonly branch: string | undefined;
|
|
82
|
+
private readonly tip: string;
|
|
83
|
+
|
|
84
|
+
constructor(pi: ExtensionAPI, ctx: ExtensionContext, tip: string) {
|
|
85
|
+
this.pi = pi;
|
|
86
|
+
this.ctx = ctx;
|
|
87
|
+
this.branch = resolveGitBranch(ctx.cwd);
|
|
88
|
+
this.tip = tip;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private tipLines(width: number, theme: Theme): string[] {
|
|
92
|
+
const indent = " ";
|
|
93
|
+
const text = `${theme.fg("text", theme.bold("Tip:"))}${theme.fg("dim", ` ${this.tip}`)}`;
|
|
94
|
+
return wrapTextWithAnsi(text, width - indent.length)
|
|
95
|
+
.map((line) => padRight(`${indent}${line}`, width));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
render(width: number): string[] {
|
|
99
|
+
if (width <= 0) return [];
|
|
100
|
+
const theme = this.ctx.ui.theme;
|
|
101
|
+
if (width < 28) return [truncateToWidth(theme.fg("text", theme.bold("KillerOS")), width, "")];
|
|
102
|
+
|
|
103
|
+
const panelWidth = Math.min(width, COMPACT_HEADER_MAX_WIDTH);
|
|
104
|
+
const innerWidth = panelWidth - 4;
|
|
105
|
+
const version = KILLEROS_VERSION ? theme.fg("dim", ` (v${KILLEROS_VERSION})`) : "";
|
|
106
|
+
const identity = `${theme.fg("dim", "›")} ${theme.fg("text", theme.bold("KillerOS"))}${version}`;
|
|
107
|
+
const thinkingLevel = this.pi.getThinkingLevel() as ThinkingLevel;
|
|
108
|
+
const reasoning = this.ctx.model?.reasoning === false
|
|
109
|
+
? theme.fg("thinkingOff", "no reasoning")
|
|
110
|
+
: theme.fg(LEVEL_COLORS[thinkingLevel], thinkingLevel);
|
|
111
|
+
const agent = `${formatModel(this.ctx.model, theme)}${theme.fg("dim", " · ")}${reasoning}`;
|
|
112
|
+
const directory = formatCwd(this.ctx.cwd);
|
|
113
|
+
const repository = this.branch
|
|
114
|
+
? `${directory} ${theme.fg("dim", `· ${this.branch}`)}`
|
|
115
|
+
: directory;
|
|
116
|
+
const modelCommand = commandBlue("/model");
|
|
117
|
+
const agentWidth = Math.max(0, innerWidth - visibleWidth(modelCommand) - 1);
|
|
118
|
+
const agentCommand = `${truncateToWidth(agent, agentWidth, "…")} ${modelCommand}`;
|
|
119
|
+
const border = (left: string, right: string): string => theme.fg("dim", `${left}${"─".repeat(panelWidth - 2)}${right}`);
|
|
120
|
+
const lines = [
|
|
121
|
+
border("╭", "╮"),
|
|
122
|
+
compactBoxLine(identity, panelWidth, theme),
|
|
123
|
+
compactBoxLine("", panelWidth, theme),
|
|
124
|
+
compactBoxLine(agentCommand, panelWidth, theme),
|
|
125
|
+
compactBoxLine(repository, panelWidth, theme),
|
|
126
|
+
border("╰", "╯"),
|
|
127
|
+
" ".repeat(panelWidth),
|
|
128
|
+
...this.tipLines(panelWidth, theme),
|
|
129
|
+
];
|
|
130
|
+
return lines;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
invalidate(): void {}
|
|
134
|
+
dispose(): void {}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const ANSI_REGEX = /\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
|
|
138
|
+
|
|
139
|
+
function stripAnsi(text: string): string {
|
|
140
|
+
return text.replace(ANSI_REGEX, "").trim();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function isBorderLine(line: string): boolean {
|
|
144
|
+
const unstyled = stripAnsi(line);
|
|
145
|
+
return /^[─━═]+$/.test(unstyled) || /^───\s*[↓↑]/.test(unstyled) || /^─{3,}/.test(unstyled);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
class PiCodeEditor extends CustomEditor {
|
|
149
|
+
private readonly appKeybindings: KeybindingsManager;
|
|
150
|
+
|
|
151
|
+
constructor(tui: TUI, theme: EditorTheme, appKeybindings: KeybindingsManager) {
|
|
152
|
+
super(tui, theme, appKeybindings);
|
|
153
|
+
this.appKeybindings = appKeybindings;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
override handleInput(data: string): void {
|
|
157
|
+
const isShiftEnter = data === "\x1B[13;2u"
|
|
158
|
+
|| data === "\x1B[13;2~"
|
|
159
|
+
|| data === "\x1B[27;2;13~"
|
|
160
|
+
|| data === "\x1B\r"
|
|
161
|
+
|| data === "\x1B\n"
|
|
162
|
+
|| this.appKeybindings.matches(data, "tui.input.newLine");
|
|
163
|
+
if (isShiftEnter) {
|
|
164
|
+
this.insertTextAtCursor("\n");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
super.handleInput(data);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
override render(width: number): string[] {
|
|
171
|
+
if (width < 4) return super.render(width);
|
|
172
|
+
const innerWidth = width - 2;
|
|
173
|
+
const lines = super.render(innerWidth);
|
|
174
|
+
if (lines.length < 2) return lines.map((line) => truncateToWidth(line, width, ""));
|
|
175
|
+
|
|
176
|
+
const gray = (text: string): string => `\x1B[90m${text}\x1B[39m`;
|
|
177
|
+
let bottomBorderIndex = -1;
|
|
178
|
+
for (let index = lines.length - 1; index >= 1; index -= 1) {
|
|
179
|
+
if (isBorderLine(lines[index] ?? "")) {
|
|
180
|
+
bottomBorderIndex = index;
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (bottomBorderIndex < 0) bottomBorderIndex = lines.length - 1;
|
|
185
|
+
|
|
186
|
+
const framed: string[] = [];
|
|
187
|
+
const top = stripAnsi(lines[0] ?? "");
|
|
188
|
+
const isScrolledHeader = top.includes("↑");
|
|
189
|
+
if (isScrolledHeader) {
|
|
190
|
+
const count = top.match(/↑\s*(\d+)/)?.[1] ?? "";
|
|
191
|
+
const indicator = `${gray("─── ↑ ")}${count}${gray(" more ")}${gray("─".repeat(Math.max(0, width - 12 - count.length)))}`;
|
|
192
|
+
framed.push(truncateToWidth(indicator, width, ""));
|
|
193
|
+
} else {
|
|
194
|
+
framed.push(gray("─".repeat(width)));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for (let index = 1; index < bottomBorderIndex; index += 1) {
|
|
198
|
+
const prefix = index === 1 && !isScrolledHeader ? gray("❯ ") : " ";
|
|
199
|
+
framed.push(`${prefix}${padRight(lines[index] ?? "", innerWidth)}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const bottom = stripAnsi(lines[bottomBorderIndex] ?? "");
|
|
203
|
+
if (bottom.includes("↓")) {
|
|
204
|
+
const count = bottom.match(/↓\s*(\d+)/)?.[1] ?? "";
|
|
205
|
+
const indicator = `${gray("─── ↓ ")}${count}${gray(" more ")}${gray("─".repeat(Math.max(0, width - 12 - count.length)))}`;
|
|
206
|
+
framed.push(truncateToWidth(indicator, width, ""));
|
|
207
|
+
} else {
|
|
208
|
+
framed.push(gray("─".repeat(width)));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (let index = bottomBorderIndex + 1; index < lines.length; index += 1) {
|
|
212
|
+
framed.push(` ${padRight(lines[index] ?? "", innerWidth)}`);
|
|
213
|
+
}
|
|
214
|
+
return framed.map((line) => truncateToWidth(line, width, ""));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const ACTIVITY_WORDS = ["Brewing", "Pondering", "Tinkering", "Wrangling", "Noodling", "Cooking"] as const;
|
|
219
|
+
|
|
220
|
+
export function registerShellUi(pi: ExtensionAPI): void {
|
|
221
|
+
let activeHeader: PiStartupHeader | undefined;
|
|
222
|
+
let activityWordIndex = 0;
|
|
223
|
+
let tipDeck: string[] = [];
|
|
224
|
+
const nextStartupTip = (): string => {
|
|
225
|
+
if (tipDeck.length === 0) tipDeck = shuffledTips();
|
|
226
|
+
return tipDeck.pop() ?? STARTUP_TIPS[0];
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
pi.on("session_start", (_event, ctx) => {
|
|
230
|
+
if (ctx.mode !== "tui") return;
|
|
231
|
+
try {
|
|
232
|
+
ctx.ui.setTheme("killeros");
|
|
233
|
+
const startupTip = nextStartupTip();
|
|
234
|
+
ctx.ui.setHeader(() => {
|
|
235
|
+
activeHeader?.dispose();
|
|
236
|
+
activeHeader = new PiStartupHeader(pi, ctx, startupTip);
|
|
237
|
+
return activeHeader;
|
|
238
|
+
});
|
|
239
|
+
ctx.ui.setWorkingIndicator({
|
|
240
|
+
frames: [
|
|
241
|
+
ctx.ui.theme.fg("dim", "✻"),
|
|
242
|
+
ctx.ui.theme.fg("muted", "✻"),
|
|
243
|
+
ctx.ui.theme.fg("accent", "✻"),
|
|
244
|
+
ctx.ui.theme.fg("muted", "✻"),
|
|
245
|
+
],
|
|
246
|
+
intervalMs: 180,
|
|
247
|
+
});
|
|
248
|
+
ctx.ui.setHiddenThinkingLabel("└ Thinking…");
|
|
249
|
+
ctx.ui.setEditorComponent((tui, theme, keybindings) => new PiCodeEditor(tui, theme, keybindings));
|
|
250
|
+
} catch (error) {
|
|
251
|
+
reportError(ctx, "Killeros UI failed to initialize", error);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
256
|
+
if (ctx.mode !== "tui") return;
|
|
257
|
+
ctx.ui.setWorkingMessage(`${ACTIVITY_WORDS[activityWordIndex]}…`);
|
|
258
|
+
activityWordIndex = (activityWordIndex + 1) % ACTIVITY_WORDS.length;
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
pi.on("agent_end", (_event, ctx) => {
|
|
262
|
+
if (ctx.mode === "tui") ctx.ui.setWorkingMessage();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
pi.on("session_shutdown", () => {
|
|
266
|
+
activeHeader?.dispose();
|
|
267
|
+
activeHeader = undefined;
|
|
268
|
+
activityWordIndex = 0;
|
|
269
|
+
});
|
|
270
|
+
}
|