killeros 2.0.2 → 2.0.4
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 +34 -0
- package/Killeros.ts +17 -10
- package/README.md +21 -13
- package/killeros/commands.ts +2 -8
- package/killeros/concise.ts +12 -8
- package/killeros/footer.ts +46 -1
- package/killeros/goals.ts +118 -57
- package/killeros/hooks.ts +49 -17
- package/killeros/init-evidence.ts +240 -0
- package/killeros/init-target.ts +289 -0
- package/killeros/init.ts +139 -356
- package/killeros/notifications.ts +167 -0
- package/killeros/question.ts +16 -1
- package/killeros/runtime.ts +19 -31
- package/killeros/shell-ui.ts +18 -141
- package/package.json +2 -2
- package/killeros/context-compaction.ts +0 -614
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import type { StopReason } from "@earendil-works/pi-ai";
|
|
5
|
+
import {
|
|
6
|
+
getAgentDir,
|
|
7
|
+
type ExtensionAPI,
|
|
8
|
+
type ExtensionContext,
|
|
9
|
+
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
|
|
11
|
+
export interface NotificationPreferenceStore {
|
|
12
|
+
load(): boolean;
|
|
13
|
+
save(enabled: boolean): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CompletionNotificationDependencies {
|
|
17
|
+
store: NotificationPreferenceStore;
|
|
18
|
+
ring(): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const COMPLETION_BELL_GLYPH = "";
|
|
22
|
+
|
|
23
|
+
const defaultSettingsPath = (): string => join(getAgentDir(), "killeros.json");
|
|
24
|
+
|
|
25
|
+
type StoredSettings = Record<string, unknown>;
|
|
26
|
+
|
|
27
|
+
function readStoredSettings(settingsPath: string): StoredSettings {
|
|
28
|
+
try {
|
|
29
|
+
const parsed: unknown = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
30
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
31
|
+
throw new Error("KillerOS settings must contain a JSON object");
|
|
32
|
+
}
|
|
33
|
+
return parsed as StoredSettings;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createNotificationPreferenceStore(
|
|
41
|
+
settingsPath = defaultSettingsPath(),
|
|
42
|
+
): NotificationPreferenceStore {
|
|
43
|
+
return {
|
|
44
|
+
load: () => readStoredSettings(settingsPath).completionSound === true,
|
|
45
|
+
save: (enabled) => {
|
|
46
|
+
const current = readStoredSettings(settingsPath);
|
|
47
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
48
|
+
const temporaryPath = `${settingsPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
49
|
+
try {
|
|
50
|
+
writeFileSync(
|
|
51
|
+
temporaryPath,
|
|
52
|
+
`${JSON.stringify({ ...current, completionSound: enabled }, null, 2)}\n`,
|
|
53
|
+
{ encoding: "utf8", mode: 0o600 },
|
|
54
|
+
);
|
|
55
|
+
renameSync(temporaryPath, settingsPath);
|
|
56
|
+
} finally {
|
|
57
|
+
rmSync(temporaryPath, { force: true });
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function formatNotificationTitle(
|
|
64
|
+
cwd: string,
|
|
65
|
+
sessionName: string | undefined,
|
|
66
|
+
enabled: boolean,
|
|
67
|
+
): string {
|
|
68
|
+
const directory = basename(cwd);
|
|
69
|
+
const base = sessionName ? `π - ${sessionName} - ${directory}` : `π - ${directory}`;
|
|
70
|
+
return enabled ? `${base} ${COMPLETION_BELL_GLYPH}` : base;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function errorMessage(error: unknown): string {
|
|
74
|
+
return error instanceof Error ? error.message : String(error);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function registerCompletionNotifications(
|
|
78
|
+
pi: ExtensionAPI,
|
|
79
|
+
dependencies?: CompletionNotificationDependencies,
|
|
80
|
+
): void {
|
|
81
|
+
const runtime = dependencies ?? {
|
|
82
|
+
store: createNotificationPreferenceStore(),
|
|
83
|
+
ring: () => { process.stdout.write("\x07"); },
|
|
84
|
+
};
|
|
85
|
+
let enabled = false;
|
|
86
|
+
let requestPending = false;
|
|
87
|
+
let lastStopReason: StopReason | undefined;
|
|
88
|
+
|
|
89
|
+
const applyTitle = (ctx: ExtensionContext): void => {
|
|
90
|
+
if (ctx.mode !== "tui") return;
|
|
91
|
+
ctx.ui.setTitle(formatNotificationTitle(ctx.cwd, pi.getSessionName(), enabled));
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
pi.registerCommand("notification", {
|
|
95
|
+
description: "Configure the completion sound",
|
|
96
|
+
handler: async (_args, ctx) => {
|
|
97
|
+
if (ctx.mode !== "tui") {
|
|
98
|
+
ctx.ui.notify("/notification requires TUI mode", "error");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const selected = await ctx.ui.select("Completion sound", [
|
|
102
|
+
enabled ? "On ← current" : "On",
|
|
103
|
+
enabled ? "Off" : "Off ← current",
|
|
104
|
+
]);
|
|
105
|
+
if (!selected) return;
|
|
106
|
+
const nextEnabled = selected.startsWith("On");
|
|
107
|
+
try {
|
|
108
|
+
runtime.store.save(nextEnabled);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
ctx.ui.notify(`Completion sound setting could not be saved: ${errorMessage(error)}`, "error");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
enabled = nextEnabled;
|
|
114
|
+
applyTitle(ctx);
|
|
115
|
+
ctx.ui.notify(`Completion sound: ${enabled ? "On" : "Off"}`, "info");
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
pi.on("session_start", (_event, ctx) => {
|
|
120
|
+
requestPending = false;
|
|
121
|
+
lastStopReason = undefined;
|
|
122
|
+
if (ctx.mode !== "tui") return;
|
|
123
|
+
try {
|
|
124
|
+
enabled = runtime.store.load();
|
|
125
|
+
} catch (error) {
|
|
126
|
+
enabled = false;
|
|
127
|
+
ctx.ui.notify(`Completion sound settings could not be read: ${errorMessage(error)}`, "error");
|
|
128
|
+
}
|
|
129
|
+
applyTitle(ctx);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
pi.on("session_info_changed", (_event, ctx) => applyTitle(ctx));
|
|
133
|
+
|
|
134
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
135
|
+
if (ctx.mode !== "tui") return;
|
|
136
|
+
requestPending = true;
|
|
137
|
+
lastStopReason = undefined;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
pi.on("agent_end", (event, ctx) => {
|
|
141
|
+
if (ctx.mode !== "tui" || !requestPending) return;
|
|
142
|
+
lastStopReason = undefined;
|
|
143
|
+
for (let index = event.messages.length - 1; index >= 0; index -= 1) {
|
|
144
|
+
const message = event.messages[index];
|
|
145
|
+
if (message?.role !== "assistant") continue;
|
|
146
|
+
lastStopReason = message.stopReason;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
152
|
+
if (ctx.mode !== "tui" || !requestPending) return;
|
|
153
|
+
if (!ctx.isIdle() || ctx.hasPendingMessages()) return;
|
|
154
|
+
requestPending = false;
|
|
155
|
+
if (!enabled || lastStopReason === "aborted") return;
|
|
156
|
+
try {
|
|
157
|
+
runtime.ring();
|
|
158
|
+
} catch (error) {
|
|
159
|
+
ctx.ui.notify(`Completion sound failed: ${errorMessage(error)}`, "error");
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
pi.on("session_shutdown", () => {
|
|
164
|
+
requestPending = false;
|
|
165
|
+
lastStopReason = undefined;
|
|
166
|
+
});
|
|
167
|
+
}
|
package/killeros/question.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type ExtensionAPI, type ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import {
|
|
3
3
|
decodeKittyPrintable,
|
|
4
4
|
Editor,
|
|
@@ -174,6 +174,21 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
|
|
|
174
174
|
};
|
|
175
175
|
finishFromAbort = () => finish({ kind: "aborted" });
|
|
176
176
|
|
|
177
|
+
const keyHint = (
|
|
178
|
+
keybinding: Parameters<typeof keybindings.getKeys>[0],
|
|
179
|
+
description: string,
|
|
180
|
+
): string => {
|
|
181
|
+
const keyText = keybindings.getKeys(keybinding)
|
|
182
|
+
.join("/")
|
|
183
|
+
.split("/")
|
|
184
|
+
.map((key) => key
|
|
185
|
+
.split("+")
|
|
186
|
+
.map((part) => process.platform === "darwin" && part.toLocaleLowerCase() === "alt" ? "option" : part)
|
|
187
|
+
.join("+"))
|
|
188
|
+
.join("/");
|
|
189
|
+
return theme.fg("dim", keyText) + theme.fg("muted", ` ${description}`);
|
|
190
|
+
};
|
|
191
|
+
|
|
177
192
|
const editorTheme: EditorTheme = {
|
|
178
193
|
borderColor: (text) => theme.fg("accent", text),
|
|
179
194
|
selectList: {
|
package/killeros/runtime.ts
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
|
+
import type { InitEvidenceIndex } from "./init-evidence.ts";
|
|
2
|
+
import type { InitTargetBaseline } from "./init-target.ts";
|
|
3
|
+
|
|
4
|
+
export type InitOutcome =
|
|
5
|
+
| { kind: "pending" }
|
|
6
|
+
| { kind: "written" }
|
|
7
|
+
| { kind: "policy-conflict"; reason: string }
|
|
8
|
+
| { kind: "no-outcome" };
|
|
9
|
+
|
|
1
10
|
export interface InitRuntime {
|
|
2
11
|
active: boolean;
|
|
3
12
|
targetPath?: string;
|
|
4
|
-
writeAttempted: boolean;
|
|
5
|
-
writeSucceeded: boolean;
|
|
6
13
|
projectRoot?: string;
|
|
7
14
|
activeTools?: string[];
|
|
8
|
-
|
|
15
|
+
evidence?: InitEvidenceIndex;
|
|
16
|
+
baseline?: InitTargetBaseline;
|
|
17
|
+
outcome: InitOutcome;
|
|
18
|
+
settle?: (outcome: InitOutcome) => void;
|
|
9
19
|
}
|
|
10
20
|
|
|
11
21
|
export type GoalStatus = "active" | "paused" | "blocked" | "complete";
|
|
@@ -23,13 +33,13 @@ export interface GoalState {
|
|
|
23
33
|
blockedAuditStartTurn: number;
|
|
24
34
|
baselineTokens: number;
|
|
25
35
|
result?: string;
|
|
36
|
+
resumeAfterManualCompaction?: true;
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
export interface GoalRuntime {
|
|
29
40
|
state?: GoalState;
|
|
30
41
|
continuationScheduled: boolean;
|
|
31
42
|
continuationHeld: boolean;
|
|
32
|
-
continuationHeldForCompaction: boolean;
|
|
33
43
|
goalTurnInFlight: boolean;
|
|
34
44
|
agentEndObserved: boolean;
|
|
35
45
|
persistenceRetryNeeded: boolean;
|
|
@@ -39,14 +49,13 @@ export interface GoalRuntime {
|
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
export function createInitRuntime(): InitRuntime {
|
|
42
|
-
return { active: false,
|
|
52
|
+
return { active: false, outcome: { kind: "pending" } };
|
|
43
53
|
}
|
|
44
54
|
|
|
45
55
|
export function createGoalRuntime(): GoalRuntime {
|
|
46
56
|
return {
|
|
47
57
|
continuationScheduled: false,
|
|
48
58
|
continuationHeld: false,
|
|
49
|
-
continuationHeldForCompaction: false,
|
|
50
59
|
goalTurnInFlight: false,
|
|
51
60
|
agentEndObserved: false,
|
|
52
61
|
persistenceRetryNeeded: false,
|
|
@@ -56,31 +65,10 @@ export function createGoalRuntime(): GoalRuntime {
|
|
|
56
65
|
export function resetInitRuntime(state: InitRuntime): void {
|
|
57
66
|
state.active = false;
|
|
58
67
|
state.targetPath = undefined;
|
|
59
|
-
state.writeAttempted = false;
|
|
60
|
-
state.writeSucceeded = false;
|
|
61
68
|
state.projectRoot = undefined;
|
|
62
69
|
state.activeTools = undefined;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
automaticCompactionArmed: boolean;
|
|
68
|
-
automaticCompactionAwaitingHook: boolean;
|
|
69
|
-
automaticCompactionPending: boolean;
|
|
70
|
-
compactionOperationId: number;
|
|
71
|
-
sessionGeneration: number;
|
|
72
|
-
lastCompactionAt?: number;
|
|
73
|
-
thresholdPercent: number;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function createCompactionRuntime(): CompactionRuntime {
|
|
77
|
-
return {
|
|
78
|
-
compactionInFlight: false,
|
|
79
|
-
automaticCompactionArmed: true,
|
|
80
|
-
automaticCompactionAwaitingHook: false,
|
|
81
|
-
automaticCompactionPending: false,
|
|
82
|
-
compactionOperationId: 0,
|
|
83
|
-
sessionGeneration: 0,
|
|
84
|
-
thresholdPercent: 40,
|
|
85
|
-
};
|
|
70
|
+
state.evidence = undefined;
|
|
71
|
+
state.baseline = undefined;
|
|
72
|
+
state.outcome = { kind: "pending" };
|
|
73
|
+
state.settle = undefined;
|
|
86
74
|
}
|
package/killeros/shell-ui.ts
CHANGED
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
} from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import {
|
|
13
13
|
Container,
|
|
14
|
-
CURSOR_MARKER,
|
|
15
14
|
Text,
|
|
16
15
|
truncateToWidth,
|
|
17
16
|
visibleWidth,
|
|
@@ -19,7 +18,6 @@ import {
|
|
|
19
18
|
type EditorTheme,
|
|
20
19
|
type TUI,
|
|
21
20
|
} from "@earendil-works/pi-tui";
|
|
22
|
-
import { availableCommandNames } from "./commands.ts";
|
|
23
21
|
import { formatCwd, padRight } from "./display.ts";
|
|
24
22
|
import { reportError } from "./errors.ts";
|
|
25
23
|
import { formatModel } from "./footer.ts";
|
|
@@ -42,6 +40,7 @@ const STARTUP_TIPS = [
|
|
|
42
40
|
"Press Shift+Enter to insert a line break without sending.",
|
|
43
41
|
"Run /variants to tune the model's reasoning depth.",
|
|
44
42
|
"Type / to browse every command available in this session.",
|
|
43
|
+
"Run /notification to enable a terminal bell when work settles.",
|
|
45
44
|
] as const;
|
|
46
45
|
|
|
47
46
|
export function resolveGitBranch(cwd: string): Promise<string | undefined> {
|
|
@@ -150,113 +149,6 @@ class PiStartupHeader {
|
|
|
150
149
|
}
|
|
151
150
|
|
|
152
151
|
const ANSI_REGEX = /\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
|
|
153
|
-
const ANSI_SEQUENCE_AT_START = /^\x1b\[[0-?]*[ -/]*[@-~]/u;
|
|
154
|
-
const COMMAND_TOKEN_PATTERN = /(^|[ \t])(\/[A-Za-z0-9:_-]*)/gu;
|
|
155
|
-
|
|
156
|
-
function controlSequenceAt(text: string, index: number): string | undefined {
|
|
157
|
-
if (text.startsWith(CURSOR_MARKER, index)) return CURSOR_MARKER;
|
|
158
|
-
return text.slice(index).match(ANSI_SEQUENCE_AT_START)?.[0];
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
interface CommandToken {
|
|
162
|
-
text: string;
|
|
163
|
-
start: number;
|
|
164
|
-
end: number;
|
|
165
|
-
valid: boolean;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
interface EditorVisualLine {
|
|
169
|
-
logicalLine: number;
|
|
170
|
-
startCol: number;
|
|
171
|
-
length: number;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function commandTokens(text: string, normalizedNames: readonly string[]): CommandToken[] {
|
|
175
|
-
return [...text.matchAll(COMMAND_TOKEN_PATTERN)].map((match) => {
|
|
176
|
-
const token = match[2] ?? "";
|
|
177
|
-
const prefix = token.slice(1).toLocaleLowerCase();
|
|
178
|
-
const start = (match.index ?? 0) + (match[1]?.length ?? 0);
|
|
179
|
-
return {
|
|
180
|
-
text: token,
|
|
181
|
-
start,
|
|
182
|
-
end: start + token.length,
|
|
183
|
-
valid: normalizedNames.some((name) => name.startsWith(prefix)),
|
|
184
|
-
};
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function highlightTextRanges(
|
|
189
|
-
text: string,
|
|
190
|
-
ranges: Array<{ start: number; end: number }>,
|
|
191
|
-
color: (value: string) => string,
|
|
192
|
-
): string {
|
|
193
|
-
if (ranges.length === 0) return text;
|
|
194
|
-
|
|
195
|
-
let output = "";
|
|
196
|
-
let buffer = "";
|
|
197
|
-
let bufferHighlighted: boolean | undefined;
|
|
198
|
-
let plainIndex = 0;
|
|
199
|
-
const flush = (): void => {
|
|
200
|
-
if (!buffer) return;
|
|
201
|
-
output += bufferHighlighted ? color(buffer) : buffer;
|
|
202
|
-
buffer = "";
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
for (let index = 0; index < text.length;) {
|
|
206
|
-
const control = controlSequenceAt(text, index);
|
|
207
|
-
if (control) {
|
|
208
|
-
flush();
|
|
209
|
-
output += control;
|
|
210
|
-
index += control.length;
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const highlighted = ranges.some((range) => plainIndex >= range.start && plainIndex < range.end);
|
|
215
|
-
if (bufferHighlighted !== highlighted) {
|
|
216
|
-
flush();
|
|
217
|
-
bufferHighlighted = highlighted;
|
|
218
|
-
}
|
|
219
|
-
buffer += text[index];
|
|
220
|
-
plainIndex += 1;
|
|
221
|
-
index += 1;
|
|
222
|
-
}
|
|
223
|
-
flush();
|
|
224
|
-
return output;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function highlightEditorLines(
|
|
228
|
-
lines: string[],
|
|
229
|
-
sourceLines: string[],
|
|
230
|
-
visualLines: EditorVisualLine[],
|
|
231
|
-
scrollOffset: number,
|
|
232
|
-
commandNames: ReadonlySet<string>,
|
|
233
|
-
color: (value: string) => string,
|
|
234
|
-
): { lines: string[]; bottomBorderIndex: number } {
|
|
235
|
-
let bottomBorderIndex = -1;
|
|
236
|
-
for (let index = lines.length - 1; index >= 1; index -= 1) {
|
|
237
|
-
if (isBorderLine(lines[index] ?? "")) {
|
|
238
|
-
bottomBorderIndex = index;
|
|
239
|
-
break;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
if (bottomBorderIndex < 0) bottomBorderIndex = lines.length - 1;
|
|
243
|
-
|
|
244
|
-
const normalizedNames = [...commandNames].map((name) => name.toLocaleLowerCase());
|
|
245
|
-
for (let index = 1; index < bottomBorderIndex; index += 1) {
|
|
246
|
-
const visualLine = visualLines[scrollOffset + index - 1];
|
|
247
|
-
if (!visualLine) continue;
|
|
248
|
-
const visibleStart = visualLine.startCol;
|
|
249
|
-
const visibleEnd = visibleStart + visualLine.length;
|
|
250
|
-
const ranges = commandTokens(sourceLines[visualLine.logicalLine] ?? "", normalizedNames)
|
|
251
|
-
.filter((token) => token.valid && token.start < visibleEnd && token.end > visibleStart)
|
|
252
|
-
.map((token) => ({
|
|
253
|
-
start: Math.max(token.start, visibleStart) - visibleStart,
|
|
254
|
-
end: Math.min(token.end, visibleEnd) - visibleStart,
|
|
255
|
-
}));
|
|
256
|
-
lines[index] = highlightTextRanges(lines[index] ?? "", ranges, color);
|
|
257
|
-
}
|
|
258
|
-
return { lines, bottomBorderIndex };
|
|
259
|
-
}
|
|
260
152
|
|
|
261
153
|
function stripAnsi(text: string): string {
|
|
262
154
|
return text.replace(ANSI_REGEX, "").trim();
|
|
@@ -275,19 +167,16 @@ function isScrolledTopBorder(line: string): boolean {
|
|
|
275
167
|
class PiCodeEditor extends CustomEditor {
|
|
276
168
|
private readonly appKeybindings: KeybindingsManager;
|
|
277
169
|
private readonly runtimeTheme: Theme;
|
|
278
|
-
private readonly getCommandNames: () => ReadonlySet<string>;
|
|
279
170
|
|
|
280
171
|
constructor(
|
|
281
172
|
tui: TUI,
|
|
282
173
|
theme: EditorTheme,
|
|
283
174
|
appKeybindings: KeybindingsManager,
|
|
284
175
|
runtimeTheme: Theme,
|
|
285
|
-
getCommandNames: () => ReadonlySet<string>,
|
|
286
176
|
) {
|
|
287
177
|
super(tui, theme, appKeybindings);
|
|
288
178
|
this.appKeybindings = appKeybindings;
|
|
289
179
|
this.runtimeTheme = runtimeTheme;
|
|
290
|
-
this.getCommandNames = getCommandNames;
|
|
291
180
|
}
|
|
292
181
|
|
|
293
182
|
override handleInput(data: string): void {
|
|
@@ -304,37 +193,19 @@ class PiCodeEditor extends CustomEditor {
|
|
|
304
193
|
super.handleInput(data);
|
|
305
194
|
}
|
|
306
195
|
|
|
307
|
-
private renderWithCommandHighlighting(
|
|
308
|
-
width: number,
|
|
309
|
-
color: (value: string) => string,
|
|
310
|
-
): { lines: string[]; bottomBorderIndex: number } {
|
|
311
|
-
const lines = super.render(width);
|
|
312
|
-
const internals = this as unknown as {
|
|
313
|
-
lastWidth: number;
|
|
314
|
-
scrollOffset: number;
|
|
315
|
-
buildVisualLineMap: (layoutWidth: number) => EditorVisualLine[];
|
|
316
|
-
};
|
|
317
|
-
return highlightEditorLines(
|
|
318
|
-
lines,
|
|
319
|
-
this.getLines(),
|
|
320
|
-
internals.buildVisualLineMap(internals.lastWidth),
|
|
321
|
-
internals.scrollOffset,
|
|
322
|
-
this.getCommandNames(),
|
|
323
|
-
color,
|
|
324
|
-
);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
196
|
override render(width: number): string[] {
|
|
328
197
|
if (width <= 0) return [];
|
|
329
|
-
|
|
330
|
-
if (width < 4) {
|
|
331
|
-
return this.renderWithCommandHighlighting(width, colorCommand)
|
|
332
|
-
.lines.map((line) => truncateToWidth(line, width, ""));
|
|
333
|
-
}
|
|
198
|
+
if (width < 4) return super.render(width).map((line) => truncateToWidth(line, width, ""));
|
|
334
199
|
const innerWidth = width - 2;
|
|
335
|
-
const
|
|
336
|
-
const { lines, bottomBorderIndex } = highlighted;
|
|
200
|
+
const lines = super.render(innerWidth);
|
|
337
201
|
if (lines.length < 2) return lines.map((line) => truncateToWidth(line, width, ""));
|
|
202
|
+
let bottomBorderIndex = lines.length - 1;
|
|
203
|
+
for (let index = lines.length - 1; index >= 1; index -= 1) {
|
|
204
|
+
if (isBorderLine(lines[index] ?? "")) {
|
|
205
|
+
bottomBorderIndex = index;
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
338
209
|
|
|
339
210
|
const gray = (text: string): string => this.runtimeTheme.fg("dim", text);
|
|
340
211
|
const framed: string[] = [];
|
|
@@ -380,6 +251,8 @@ function formatActivityMessage(word: string, theme: Theme): string {
|
|
|
380
251
|
return `${theme.fg("accent", `${word}…`)} ${theme.fg("dim", `(${theme.bold("esc")} to interrupt · thinking)`)}`;
|
|
381
252
|
}
|
|
382
253
|
|
|
254
|
+
let killerosEditorFactory: ReturnType<ExtensionContext["ui"]["getEditorComponent"]>;
|
|
255
|
+
|
|
383
256
|
export function registerShellUi(pi: ExtensionAPI): void {
|
|
384
257
|
let activeHeader: PiStartupHeader | undefined;
|
|
385
258
|
let activityDeck: string[] = [];
|
|
@@ -427,8 +300,12 @@ export function registerShellUi(pi: ExtensionAPI): void {
|
|
|
427
300
|
intervalMs: ACTIVITY_FRAME_INTERVAL_MS,
|
|
428
301
|
});
|
|
429
302
|
ctx.ui.setHiddenThinkingLabel("└ Thinking…");
|
|
430
|
-
ctx.ui.
|
|
431
|
-
|
|
303
|
+
const existingEditorFactory = ctx.ui.getEditorComponent?.();
|
|
304
|
+
if (!existingEditorFactory || existingEditorFactory === killerosEditorFactory) {
|
|
305
|
+
killerosEditorFactory = (tui, editorTheme, keybindings) =>
|
|
306
|
+
new PiCodeEditor(tui, editorTheme, keybindings, ctx.ui.theme);
|
|
307
|
+
ctx.ui.setEditorComponent(killerosEditorFactory);
|
|
308
|
+
}
|
|
432
309
|
} catch (error) {
|
|
433
310
|
reportError(ctx, "Killeros UI failed to initialize", error);
|
|
434
311
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "killeros",
|
|
3
|
-
"version": "2.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.0.4",
|
|
4
|
+
"description": "TUI, goals, and workflow automation for the Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"pi-package",
|