killeros 2.1.24 → 2.1.26
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 +26 -0
- package/Killeros.ts +1 -3
- package/README.md +2 -4
- package/killeros/auto-compaction.ts +2 -3
- package/killeros/change-receipt.ts +7 -31
- package/killeros/commands.ts +0 -1
- package/killeros/display.ts +4 -4
- package/killeros/footer.ts +15 -30
- package/killeros/goal-interface.ts +2 -0
- package/killeros/goal-settlement.ts +0 -4
- package/killeros/goal-state.ts +10 -1
- package/killeros/init-target.ts +16 -5
- package/killeros/init.ts +9 -5
- package/killeros/runtime.ts +1 -1
- package/killeros/shell-ui.ts +22 -47
- package/killeros/variants.ts +1 -171
- package/killeros/worked-for.ts +17 -17
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ All notable changes to KillerOS are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.1.26] - 2026-09-08
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Restored Pi lifecycle loading, blocked footer Git status from invoking configured filesystem monitors, and kept replaced `/init` guidance at a disclosed recovery path so late writes remain recoverable.
|
|
12
|
+
- Preserved Unicode blocker evidence when restoring goals, rejected cancelled goal updates before completion is saved, and read untracked symlink destinations instead of their targets in change receipts.
|
|
13
|
+
- Kept `/init` isolated from proactive compaction, refreshed Git filter safeguards for each receipt scan, and preserved compaction recovery after cancelled session replacements.
|
|
14
|
+
|
|
15
|
+
## [2.1.25] - 2026-09-06
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Left-aligned settled `✓ Done` receipt lines so every row shares the assistant text edge.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Replaced the boxed startup card with a three-row masthead: coral fade mark, versions, model and reasoning level, directory and branch, and an italic tip.
|
|
24
|
+
- Swapped the working indicator to the 10-frame Braille orbit at 80 ms per frame.
|
|
25
|
+
- Restored the leading `❯` prompt marker in white in the chat editor.
|
|
26
|
+
- Rendered footer model names and the `fast` badge in non-bold white.
|
|
27
|
+
- Showed the current Pi version beside the KillerOS version in the startup header.
|
|
28
|
+
|
|
29
|
+
### Removed
|
|
30
|
+
|
|
31
|
+
- Removed the `/variants` reasoning-level command in favor of Pi's native `/thinking` selector.
|
|
32
|
+
|
|
7
33
|
## [2.1.24] - 2026-09-05
|
|
8
34
|
|
|
9
35
|
### Added
|
package/Killeros.ts
CHANGED
|
@@ -22,7 +22,6 @@ import { registerPersonalInstructions } from "./killeros/personal-instructions.t
|
|
|
22
22
|
import { registerQuestionTool } from "./killeros/question.ts";
|
|
23
23
|
import { createGoalRuntime, createInitRuntime } from "./killeros/runtime.ts";
|
|
24
24
|
import { registerShellUi } from "./killeros/shell-ui.ts";
|
|
25
|
-
import { registerVariants } from "./killeros/variants.ts";
|
|
26
25
|
import { registerWorkedFor } from "./killeros/worked-for.ts";
|
|
27
26
|
|
|
28
27
|
export { contextPercentRemaining, formatCost, formatContextProgress } from "./killeros/footer.ts";
|
|
@@ -49,13 +48,12 @@ export default function Killeros(pi: ExtensionAPI, options: KillerosOptions = {}
|
|
|
49
48
|
registerHandoff(pi, goalRuntime, options.handoffMaxTokens);
|
|
50
49
|
registerSlashAutocomplete(pi, commandResolver);
|
|
51
50
|
registerFooter(pi, goalRuntime);
|
|
52
|
-
registerVariants(pi);
|
|
53
51
|
registerCodexFastMode(pi);
|
|
54
52
|
registerInitCommand(pi, initRuntime, goalRuntime);
|
|
55
53
|
registerLifecycleHooks(pi);
|
|
56
54
|
registerWorkedFor(pi);
|
|
57
55
|
const goalCompaction = registerGoalSettlement(pi, goalRuntime, initRuntime);
|
|
58
|
-
registerAutoCompaction(pi, { goal: goalCompaction });
|
|
56
|
+
registerAutoCompaction(pi, { goal: goalCompaction, isInitActive: () => initRuntime.active });
|
|
59
57
|
registerInitSettlement(pi, initRuntime);
|
|
60
58
|
registerRequestActivity(pi);
|
|
61
59
|
registerCompletionNotifications(pi, options.completionNotifications);
|
package/README.md
CHANGED
|
@@ -4,10 +4,9 @@ A TypeScript extension for the [Pi coding agent](https://github.com/earendil-wor
|
|
|
4
4
|
|
|
5
5
|
## What you get
|
|
6
6
|
|
|
7
|
-
- A custom TUI: startup
|
|
7
|
+
- A custom TUI: startup masthead with versions, model, working directory, and Git branch; a dark theme with coral accents; a multiline editor with slash-command completion; a footer that tracks model, context, and goal state; settled task receipts with duration and token usage.
|
|
8
8
|
- `/goal`: set an objective and Pi keeps working toward it across turns, compaction, reloads, and branch navigation. New goals pause after 20 turns; `/goal resume` grants another 20.
|
|
9
9
|
- `/init`: generates a root `AGENTS.md` from repository evidence, preserving compatible existing rules.
|
|
10
|
-
- `/variants`: pick a reasoning level supported by the active model.
|
|
11
10
|
- `/codex-fast`: toggles the `priority` service tier on Codex requests.
|
|
12
11
|
- `/handoff`: starts a fresh linked session carrying visible continuation context.
|
|
13
12
|
- Automatic context compaction when remaining tokens drop below 15% of the window (configurable).
|
|
@@ -34,7 +33,7 @@ Or from GitHub:
|
|
|
34
33
|
pi install git:github.com/KyrosHendrix/pi-KillerOS
|
|
35
34
|
```
|
|
36
35
|
|
|
37
|
-
Pin a release by appending its tag, for example `@v2.1.
|
|
36
|
+
Pin a release by appending its tag, for example `@v2.1.26`. Add `-l` to install only for the current project. Restart Pi after installing.
|
|
38
37
|
|
|
39
38
|
## Commands
|
|
40
39
|
|
|
@@ -45,7 +44,6 @@ Pin a release by appending its tag, for example `@v2.1.24`. Add `-l` to install
|
|
|
45
44
|
/goal pause Stop automatic continuation
|
|
46
45
|
/goal resume Resume automatic continuation
|
|
47
46
|
/goal clear Remove the current goal
|
|
48
|
-
/variants Reasoning-level selector (/variants high sets directly)
|
|
49
47
|
/codex-fast Toggle Codex fast mode
|
|
50
48
|
/notification Configure the completion sound
|
|
51
49
|
/handoff [focus] Fresh session with continuation context
|
|
@@ -33,6 +33,7 @@ export interface AutoCompactionDependencies {
|
|
|
33
33
|
loadPreference?: (ctx: ExtensionContext) => AutoCompactionPreference;
|
|
34
34
|
getCompactionSettings?: (ctx: ExtensionContext) => CompactionSettings;
|
|
35
35
|
goal?: AutoCompactionGoalHandlers;
|
|
36
|
+
isInitActive?: () => boolean;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
type AutoCompactionRequest = {
|
|
@@ -192,7 +193,7 @@ export function registerAutoCompaction(
|
|
|
192
193
|
};
|
|
193
194
|
|
|
194
195
|
pi.on("turn_end", (_event, ctx) => {
|
|
195
|
-
if (!supportedMode(ctx) || request) return;
|
|
196
|
+
if (!supportedMode(ctx) || dependencies.isInitActive?.() === true || request) return;
|
|
196
197
|
|
|
197
198
|
let preference: AutoCompactionPreference;
|
|
198
199
|
let compactionSettings: CompactionSettings;
|
|
@@ -288,6 +289,4 @@ export function registerAutoCompaction(
|
|
|
288
289
|
pi.on("session_start", resetForLifecycle);
|
|
289
290
|
pi.on("session_shutdown", resetForLifecycle);
|
|
290
291
|
pi.on("session_tree", resetForLifecycle);
|
|
291
|
-
pi.on("session_before_switch", resetForLifecycle);
|
|
292
|
-
pi.on("session_before_fork", resetForLifecycle);
|
|
293
292
|
}
|
|
@@ -119,47 +119,23 @@ function missingFile(error: unknown): boolean {
|
|
|
119
119
|
return error !== null && typeof error === "object" && "code" in error && error.code === "ENOENT";
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
type FilterConfiguration = { names: readonly string[]; sources: ReadonlyMap<string, Buffer> };
|
|
123
122
|
type Repository = {
|
|
124
123
|
root: string;
|
|
125
124
|
gitDirectory: string;
|
|
126
125
|
commonDirectory: string;
|
|
127
126
|
objectDirectory: string;
|
|
128
|
-
filterConfiguration: FilterConfiguration;
|
|
129
127
|
blobCache: Map<string, Buffer>;
|
|
130
128
|
blobCacheBytes: number;
|
|
131
129
|
};
|
|
132
130
|
const repositoryCache = new Map<string, Promise<Repository>>();
|
|
133
131
|
|
|
134
|
-
async function
|
|
135
|
-
const records = decode(await runGit(root, ["config", "--null", "--
|
|
132
|
+
async function loadFilterNames(root: string): Promise<readonly string[]> {
|
|
133
|
+
const records = decode(await runGit(root, ["config", "--null", "--name-only", "--list"])).split("\0");
|
|
136
134
|
const names = new Set<string>();
|
|
137
|
-
const
|
|
138
|
-
for (let index = 0; index + 1 < records.length; index += 2) {
|
|
139
|
-
const origin = records[index];
|
|
140
|
-
const key = records[index + 1];
|
|
141
|
-
if (origin?.startsWith("file:")) sourcePaths.add(path.resolve(root, origin.slice("file:".length)));
|
|
135
|
+
for (const key of records) {
|
|
142
136
|
if (key && /^filter\..*\.(clean|process)$/u.test(key)) names.add(key.slice("filter.".length, key.lastIndexOf(".")));
|
|
143
137
|
}
|
|
144
|
-
|
|
145
|
-
for (const sourcePath of sourcePaths) sources.set(sourcePath, await readBoundedFile(sourcePath, GIT_OUTPUT_LIMIT));
|
|
146
|
-
return { names: [...names], sources };
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
async function currentFilterNames(repo: Repository): Promise<readonly string[]> {
|
|
150
|
-
for (const [sourcePath, previous] of repo.filterConfiguration.sources) {
|
|
151
|
-
try {
|
|
152
|
-
if (!(await readBoundedFile(sourcePath, GIT_OUTPUT_LIMIT)).equals(previous)) {
|
|
153
|
-
repo.filterConfiguration = await loadFilterConfiguration(repo.root, repo.gitDirectory);
|
|
154
|
-
break;
|
|
155
|
-
}
|
|
156
|
-
} catch (error) {
|
|
157
|
-
if (!missingFile(error)) throw error;
|
|
158
|
-
repo.filterConfiguration = await loadFilterConfiguration(repo.root, repo.gitDirectory);
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return repo.filterConfiguration.names;
|
|
138
|
+
return [...names];
|
|
163
139
|
}
|
|
164
140
|
|
|
165
141
|
async function repository(cwd: string): Promise<Repository> {
|
|
@@ -174,7 +150,6 @@ async function repository(cwd: string): Promise<Repository> {
|
|
|
174
150
|
gitDirectory,
|
|
175
151
|
commonDirectory,
|
|
176
152
|
objectDirectory: path.join(commonDirectory, "objects"),
|
|
177
|
-
filterConfiguration: await loadFilterConfiguration(root, gitDirectory),
|
|
178
153
|
blobCache: new Map(),
|
|
179
154
|
blobCacheBytes: 0,
|
|
180
155
|
};
|
|
@@ -278,7 +253,7 @@ function discardMonitor(monitor: RepositoryMonitor): void {
|
|
|
278
253
|
}
|
|
279
254
|
|
|
280
255
|
async function snapshot(repo: Repository, paths?: readonly string[]): Promise<Snapshot> {
|
|
281
|
-
const filterNames = await
|
|
256
|
+
const filterNames = await loadFilterNames(repo.root);
|
|
282
257
|
const output = decode(await runGit(repo.root, [
|
|
283
258
|
"-c", "core.fsmonitor=false",
|
|
284
259
|
...filterNames.flatMap((name) => ["-c", `filter.${name}.clean=`, "-c", `filter.${name}.process=`, "-c", `filter.${name}.required=false`]),
|
|
@@ -295,12 +270,13 @@ async function snapshot(repo: Repository, paths?: readonly string[]): Promise<Sn
|
|
|
295
270
|
if (record.startsWith("? ")) {
|
|
296
271
|
const filePath = record.slice(2);
|
|
297
272
|
if (!filePath || filePath.endsWith("/")) continue;
|
|
273
|
+
const stats = await lstat(path.join(repo.root, ...filePath.split("/")));
|
|
298
274
|
files.set(filePath, {
|
|
299
275
|
...files.get(filePath),
|
|
300
276
|
path: filePath,
|
|
301
277
|
indexMode: undefined,
|
|
302
278
|
indexObjectId: undefined,
|
|
303
|
-
mode: "100644",
|
|
279
|
+
mode: stats.isSymbolicLink() ? "120000" : stats.mode & 0o111 ? "100755" : "100644",
|
|
304
280
|
contentObjectId: undefined,
|
|
305
281
|
});
|
|
306
282
|
continue;
|
package/killeros/commands.ts
CHANGED
|
@@ -72,7 +72,6 @@ const BUILTIN_COMMANDS: ReadonlyArray<{ name: string; description: string }> = [
|
|
|
72
72
|
const COMMAND_SYNTAX_HINTS: Readonly<Record<string, string>> = {
|
|
73
73
|
goal: "/goal [objective|pause|resume|clear]",
|
|
74
74
|
handoff: "/handoff [next-session focus]",
|
|
75
|
-
variants: "/variants [level]",
|
|
76
75
|
model: "/model [provider/model]",
|
|
77
76
|
"scoped-models": "/scoped-models",
|
|
78
77
|
login: "/login [provider]",
|
package/killeros/display.ts
CHANGED
|
@@ -52,9 +52,9 @@ export function formatTokens(value: number): string {
|
|
|
52
52
|
return `${inK}k`;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
/** Resolves a
|
|
55
|
+
/** Resolves a lowercase model id for display, falling back to the name. */
|
|
56
56
|
export function modelDisplayName(model: { name?: string; id?: string }): string {
|
|
57
|
-
const
|
|
58
|
-
if (
|
|
59
|
-
return safeTerminalText(model.
|
|
57
|
+
const id = safeTerminalText(model.id ?? "").replaceAll("\n", "").trim();
|
|
58
|
+
if (id) return id.toLowerCase();
|
|
59
|
+
return safeTerminalText(model.name ?? "").replaceAll("\n", "").trim().toLowerCase();
|
|
60
60
|
}
|
package/killeros/footer.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { watch } from "node:fs";
|
|
|
3
3
|
import { type ExtensionAPI, type ExtensionContext, type Theme, type ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { truncateToWidth, visibleWidth, type TUI } from "@earendil-works/pi-tui";
|
|
5
5
|
import { isCodexFastEnabled, subscribeCodexFast } from "./codex-fast-state.ts";
|
|
6
|
-
import { formatCwd, formatTime,
|
|
6
|
+
import { formatCwd, formatTime, modelDisplayName, padRight } from "./display.ts";
|
|
7
7
|
import { goalElapsedMilliseconds } from "./goal-state.ts";
|
|
8
8
|
import type { GoalRuntime, GoalState } from "./runtime.ts";
|
|
9
9
|
import { safeTerminalText } from "./safe-terminal-text.ts";
|
|
@@ -14,7 +14,7 @@ const GIT_STATUS_TIMEOUT_MS = 5_000;
|
|
|
14
14
|
const GIT_STATUS_WATCH_DEBOUNCE_MS = 250;
|
|
15
15
|
const GIT_STATUS_WATCH_INTERVAL_MS = 5_000;
|
|
16
16
|
const CODEX_PROVIDER = "openai-codex";
|
|
17
|
-
const colorDirectory = (text: string): string => `\x1B[38;2;240;248;154m${text}\x1B[39m`;
|
|
17
|
+
export const colorDirectory = (text: string): string => `\x1B[38;2;240;248;154m${text}\x1B[39m`;
|
|
18
18
|
|
|
19
19
|
export interface GitFileChanges {
|
|
20
20
|
modified: number;
|
|
@@ -43,7 +43,7 @@ export function resolveGitFileChanges(
|
|
|
43
43
|
return new Promise((resolve) => {
|
|
44
44
|
execute(
|
|
45
45
|
"git",
|
|
46
|
-
["-C", cwd, "status", "--porcelain=v1", "-z", "--untracked-files=all"],
|
|
46
|
+
["-C", cwd, "-c", "core.fsmonitor=false", "status", "--porcelain=v1", "-z", "--untracked-files=all"],
|
|
47
47
|
{
|
|
48
48
|
encoding: "utf8",
|
|
49
49
|
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
|
|
@@ -239,13 +239,13 @@ export function contextPercentRemaining(ctx: ExtensionContext): number | null {
|
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
export function formatContextProgress(tokensUsed: number | null, contextWindow: number, theme: Theme): string {
|
|
242
|
-
if (tokensUsed === null || !Number.isFinite(tokensUsed)) return theme.fg("dim", "—%
|
|
242
|
+
if (tokensUsed === null || !Number.isFinite(tokensUsed)) return theme.fg("dim", "ctx —%");
|
|
243
243
|
const windowSize = Number.isFinite(contextWindow) && contextWindow > 0 ? contextWindow : 128_000;
|
|
244
|
-
const
|
|
245
|
-
const
|
|
246
|
-
const color: ThemeColor =
|
|
247
|
-
const action =
|
|
248
|
-
return theme.fg(color,
|
|
244
|
+
const used = Math.max(0, Math.min(windowSize, Math.max(0, tokensUsed)));
|
|
245
|
+
const percentUsed = Math.max(0, Math.min(100, Math.round((used / windowSize) * 100)));
|
|
246
|
+
const color: ThemeColor = percentUsed > 80 ? "error" : percentUsed >= 50 ? "warning" : "success";
|
|
247
|
+
const action = percentUsed >= 85 ? " · /compact" : "";
|
|
248
|
+
return theme.fg(color, `ctx ${percentUsed}%${action}`);
|
|
249
249
|
}
|
|
250
250
|
|
|
251
251
|
function sumSessionCost(ctx: ExtensionContext): number {
|
|
@@ -275,25 +275,11 @@ const PROVIDER_LABELS: Readonly<Record<string, string>> = {
|
|
|
275
275
|
openrouter: "OpenRouter",
|
|
276
276
|
};
|
|
277
277
|
|
|
278
|
-
const PROVIDER_WORDS: Readonly<Record<string, string>> = {
|
|
279
|
-
ai: "AI",
|
|
280
|
-
api: "API",
|
|
281
|
-
deepseek: "DeepSeek",
|
|
282
|
-
github: "GitHub",
|
|
283
|
-
llm: "LLM",
|
|
284
|
-
openai: "OpenAI",
|
|
285
|
-
openrouter: "OpenRouter",
|
|
286
|
-
};
|
|
287
|
-
|
|
288
278
|
function formatProviderName(provider: string): string {
|
|
289
279
|
const normalized = safeTerminalText(provider).replaceAll("\n", "").trim();
|
|
290
280
|
const known = PROVIDER_LABELS[normalized.toLowerCase()];
|
|
291
|
-
if (known) return known;
|
|
292
|
-
return normalized
|
|
293
|
-
.split(/[-_]+/u)
|
|
294
|
-
.filter(Boolean)
|
|
295
|
-
.map((word) => PROVIDER_WORDS[word.toLowerCase()] ?? `${word.charAt(0).toUpperCase()}${word.slice(1)}`)
|
|
296
|
-
.join(" ") || "Unknown provider";
|
|
281
|
+
if (known) return known.toLowerCase();
|
|
282
|
+
return normalized.toLowerCase().split(/[-_]+/u).filter(Boolean).join(" ") || "unknown provider";
|
|
297
283
|
}
|
|
298
284
|
|
|
299
285
|
export function formatModel(
|
|
@@ -302,11 +288,10 @@ export function formatModel(
|
|
|
302
288
|
includeProvider = true,
|
|
303
289
|
showCodexFast = false,
|
|
304
290
|
): string {
|
|
305
|
-
if (!model) return theme.fg("dim", "
|
|
306
|
-
const
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
: "";
|
|
291
|
+
if (!model) return theme.fg("dim", "no model");
|
|
292
|
+
const displayName = modelDisplayName(model) || "unknown model";
|
|
293
|
+
const name = theme.fg("text", displayName);
|
|
294
|
+
const fast = showCodexFast && model.provider === CODEX_PROVIDER ? theme.fg("text", "fast") : "";
|
|
310
295
|
const provider = includeProvider ? theme.fg("dim", formatProviderName(model.provider)) : "";
|
|
311
296
|
return [name, fast, provider].filter(Boolean).join(" ");
|
|
312
297
|
}
|
|
@@ -90,6 +90,7 @@ export function registerGoalInterface(
|
|
|
90
90
|
parameters: GoalUpdateParams,
|
|
91
91
|
executionMode: "sequential",
|
|
92
92
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
93
|
+
signal?.throwIfAborted();
|
|
93
94
|
if (!isGoalModeSupported(ctx)) throw new Error("KillerOS goals require TUI or RPC mode");
|
|
94
95
|
if (!isSavedSession(ctx)) throw new Error("KillerOS goals require a saved session");
|
|
95
96
|
const state = runtime.state;
|
|
@@ -98,6 +99,7 @@ export function registerGoalInterface(
|
|
|
98
99
|
if (!evidence) throw new Error("Goal evidence must not be empty");
|
|
99
100
|
if (params.status === "complete") {
|
|
100
101
|
if (state.verification) await verifyGoalDeliverable(state.verification);
|
|
102
|
+
signal?.throwIfAborted();
|
|
101
103
|
if (runtime.state !== state) throw new Error("Goal changed while completion was being verified");
|
|
102
104
|
const verification = state.verification ? "file" : "model-reported";
|
|
103
105
|
transitionGoal(pi, runtime, "complete", "complete", evidence, { resetBlockedAudit: true });
|
|
@@ -224,10 +224,6 @@ export function registerGoalSettlement(
|
|
|
224
224
|
recoverGoalAfterManualCompaction(pi, runtime, initState, ctx);
|
|
225
225
|
});
|
|
226
226
|
|
|
227
|
-
const resetAutomaticRecovery = (): void => { runtime.automaticCompaction = undefined; };
|
|
228
|
-
pi.on("session_before_switch", resetAutomaticRecovery);
|
|
229
|
-
pi.on("session_before_fork", resetAutomaticRecovery);
|
|
230
|
-
|
|
231
227
|
return {
|
|
232
228
|
isActive: (ctx: ExtensionContext): boolean => isGoalModeSupported(ctx)
|
|
233
229
|
&& isSavedSession(ctx)
|
package/killeros/goal-state.ts
CHANGED
|
@@ -10,6 +10,7 @@ export const GOAL_MAX_TURNS = 10_000;
|
|
|
10
10
|
export const GOAL_VERSION = 1;
|
|
11
11
|
const FILE_HASH_CHUNK_SIZE = 64 * 1024;
|
|
12
12
|
export const FILE_HASH_LIMIT = 64 * 1024 * 1024;
|
|
13
|
+
const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
13
14
|
type OpenGoalFile = (filePath: string) => Promise<FileHandle>;
|
|
14
15
|
const openGoalFile: OpenGoalFile = (filePath) => open(filePath, "r");
|
|
15
16
|
|
|
@@ -84,6 +85,14 @@ function isMaxTurns(value: unknown): value is number {
|
|
|
84
85
|
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= GOAL_MAX_TURNS;
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
function exceedsBlockerEvidenceLimit(value: string): boolean {
|
|
89
|
+
let length = 0;
|
|
90
|
+
for (const _ of graphemeSegmenter.segment(value)) {
|
|
91
|
+
if (++length > 2_000) return true;
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
87
96
|
function isGoalBlockerAudit(value: unknown, turns: number, status: GoalStatus): value is GoalBlockerAudit {
|
|
88
97
|
if (!isUnknownRecord(value)
|
|
89
98
|
|| typeof value.key !== "string"
|
|
@@ -91,7 +100,7 @@ function isGoalBlockerAudit(value: unknown, turns: number, status: GoalStatus):
|
|
|
91
100
|
|| typeof value.streak !== "number" || !Number.isInteger(value.streak) || value.streak < 1 || value.streak > 3
|
|
92
101
|
|| typeof value.lastTurn !== "number" || !Number.isInteger(value.lastTurn) || value.lastTurn < 1 || value.lastTurn > turns
|
|
93
102
|
|| value.evidence !== undefined && (typeof value.evidence !== "string"
|
|
94
|
-
|| value.evidence !== value.evidence.trim() || !value.evidence || value.evidence
|
|
103
|
+
|| value.evidence !== value.evidence.trim() || !value.evidence || exceedsBlockerEvidenceLimit(value.evidence))) {
|
|
95
104
|
return false;
|
|
96
105
|
}
|
|
97
106
|
if (status === "complete") return false;
|
package/killeros/init-target.ts
CHANGED
|
@@ -155,12 +155,12 @@ async function pathExists(filePath: string): Promise<boolean> {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
/** Installs generated guidance atomically and preserves any target changed after baseline capture. */
|
|
158
|
-
export async function
|
|
158
|
+
export async function installInitAgentsFileWithRecovery(
|
|
159
159
|
targetPath: string,
|
|
160
160
|
content: string,
|
|
161
161
|
baseline: InitTargetBaseline,
|
|
162
162
|
operations: InitInstallOperations = {},
|
|
163
|
-
): Promise<
|
|
163
|
+
): Promise<string | undefined> {
|
|
164
164
|
const validationError = validateGeneratedGuidance(content);
|
|
165
165
|
if (validationError) throw new Error(validationError);
|
|
166
166
|
const renameFile = operations.renameFile ?? fs.rename;
|
|
@@ -208,8 +208,8 @@ export async function installInitAgentsFile(
|
|
|
208
208
|
return;
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
// Node cannot lock arbitrary external writers
|
|
212
|
-
//
|
|
211
|
+
// Node cannot lock arbitrary external writers, so keep the original inode named
|
|
212
|
+
// after commit. Writers with an open handle then remain recoverable.
|
|
213
213
|
await renameFile(targetPath, heldPath);
|
|
214
214
|
held = true;
|
|
215
215
|
const moved = await captureExistingTarget(heldPath);
|
|
@@ -242,10 +242,12 @@ export async function installInitAgentsFile(
|
|
|
242
242
|
if (!sameBaseline(finalHeld, baseline) || !await installedCandidateMatches(targetPath, candidate)) {
|
|
243
243
|
throw new Error("/init target changed while /init was generating; the newer AGENTS.md was preserved");
|
|
244
244
|
}
|
|
245
|
-
|
|
245
|
+
retainedRecovery = recoveryPath(targetPath);
|
|
246
|
+
await renameFile(heldPath, retainedRecovery);
|
|
246
247
|
held = false;
|
|
247
248
|
await removeCandidateName(candidatePath, unlinkFile);
|
|
248
249
|
installed = false;
|
|
250
|
+
return retainedRecovery;
|
|
249
251
|
} catch (error) {
|
|
250
252
|
if (held) {
|
|
251
253
|
if (installed && candidate && await installedCandidateMatches(targetPath, candidate)) {
|
|
@@ -288,6 +290,15 @@ export async function installInitAgentsFile(
|
|
|
288
290
|
});
|
|
289
291
|
}
|
|
290
292
|
|
|
293
|
+
export async function installInitAgentsFile(
|
|
294
|
+
targetPath: string,
|
|
295
|
+
content: string,
|
|
296
|
+
baseline: InitTargetBaseline,
|
|
297
|
+
operations: InitInstallOperations = {},
|
|
298
|
+
): Promise<void> {
|
|
299
|
+
await installInitAgentsFileWithRecovery(targetPath, content, baseline, operations);
|
|
300
|
+
}
|
|
301
|
+
|
|
291
302
|
export async function writeInitAgentsFile(
|
|
292
303
|
targetPath: string,
|
|
293
304
|
content: string,
|
package/killeros/init.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "./init-evidence.ts";
|
|
14
14
|
import {
|
|
15
15
|
captureInitTargetBaseline,
|
|
16
|
-
|
|
16
|
+
installInitAgentsFileWithRecovery,
|
|
17
17
|
validateGeneratedGuidance,
|
|
18
18
|
} from "./init-target.ts";
|
|
19
19
|
import { resetInitRuntime, type GoalRuntime, type InitOutcome, type InitRuntime } from "./runtime.ts";
|
|
@@ -109,11 +109,12 @@ export function registerInitCommand(pi: ExtensionAPI, initState: InitRuntime, go
|
|
|
109
109
|
if (!initState.targetPath || !initState.baseline) throw new Error("/init target baseline is unavailable");
|
|
110
110
|
const validationError = validateGeneratedGuidance(content);
|
|
111
111
|
if (validationError) throw new Error(validationError);
|
|
112
|
-
await
|
|
113
|
-
initState.outcome = { kind: "written" };
|
|
112
|
+
const recoveryPath = await installInitAgentsFileWithRecovery(initState.targetPath, content, initState.baseline);
|
|
113
|
+
initState.outcome = { kind: "written", ...(recoveryPath ? { recoveryPath } : {}) };
|
|
114
|
+
const recoveryNotice = recoveryPath ? ` Previous AGENTS.md preserved at ${safeTerminalText(recoveryPath)}.` : "";
|
|
114
115
|
return {
|
|
115
|
-
content: [{ type: "text" as const, text:
|
|
116
|
-
details: { path: initState.targetPath },
|
|
116
|
+
content: [{ type: "text" as const, text: `Generated root AGENTS.md.${recoveryNotice} Read it once with killeros_init_read.` }],
|
|
117
|
+
details: { path: initState.targetPath, ...(recoveryPath ? { recoveryPath } : {}) },
|
|
117
118
|
};
|
|
118
119
|
},
|
|
119
120
|
});
|
|
@@ -244,6 +245,9 @@ export function registerInitCommand(pi: ExtensionAPI, initState: InitRuntime, go
|
|
|
244
245
|
const outcome = await settled;
|
|
245
246
|
switch (outcome.kind) {
|
|
246
247
|
case "written":
|
|
248
|
+
if (outcome.recoveryPath) {
|
|
249
|
+
ctx.ui.notify(`/init preserved the previous AGENTS.md at ${safeTerminalText(outcome.recoveryPath)}`, "info");
|
|
250
|
+
}
|
|
247
251
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
248
252
|
try {
|
|
249
253
|
await ctx.reload();
|
package/killeros/runtime.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { InitTargetBaseline } from "./init-target.ts";
|
|
|
3
3
|
|
|
4
4
|
export type InitOutcome =
|
|
5
5
|
| { kind: "pending" }
|
|
6
|
-
| { kind: "written" }
|
|
6
|
+
| { kind: "written"; recoveryPath?: string }
|
|
7
7
|
| { kind: "policy-conflict"; reason: string }
|
|
8
8
|
| { kind: "cancelled" }
|
|
9
9
|
| { kind: "no-outcome" };
|
package/killeros/shell-ui.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import {
|
|
4
4
|
CustomEditor,
|
|
5
|
+
VERSION as PI_VERSION,
|
|
5
6
|
type ExtensionAPI,
|
|
6
7
|
type ExtensionContext,
|
|
7
8
|
type KeybindingsManager,
|
|
@@ -11,22 +12,18 @@ import {
|
|
|
11
12
|
CURSOR_MARKER,
|
|
12
13
|
stripTerminalSequences,
|
|
13
14
|
truncateToWidth,
|
|
14
|
-
visibleWidth,
|
|
15
15
|
wrapTextWithAnsi,
|
|
16
16
|
type EditorTheme,
|
|
17
17
|
type TUI,
|
|
18
18
|
} from "@earendil-works/pi-tui";
|
|
19
|
-
import { formatCwd, padRight } from "./display.ts";
|
|
19
|
+
import { formatCwd, modelDisplayName, padRight } from "./display.ts";
|
|
20
|
+
import { colorDirectory } from "./footer.ts";
|
|
20
21
|
import {
|
|
21
22
|
createSlashCommandResolver,
|
|
22
23
|
findSlashCommandTokens,
|
|
23
24
|
type SlashCommandResolver,
|
|
24
25
|
} from "./commands.ts";
|
|
25
26
|
import { reportError } from "./errors.ts";
|
|
26
|
-
import { formatModel } from "./footer.ts";
|
|
27
|
-
import { LEVEL_COLORS } from "./variants.ts";
|
|
28
|
-
|
|
29
|
-
const COMPACT_HEADER_MAX_WIDTH = 52;
|
|
30
27
|
|
|
31
28
|
function readPackageVersion(path: string | URL): string | undefined {
|
|
32
29
|
try {
|
|
@@ -46,7 +43,7 @@ const KILLEROS_VERSION = readPackageVersion(new URL("../package.json", import.me
|
|
|
46
43
|
|
|
47
44
|
const STARTUP_TIPS = [
|
|
48
45
|
"Press Shift+Enter to insert a line break without sending.",
|
|
49
|
-
"Run /
|
|
46
|
+
"Run /thinking to tune the model's reasoning depth.",
|
|
50
47
|
"Type / to browse every command available in this session.",
|
|
51
48
|
"Run /notification to enable a terminal bell when work settles.",
|
|
52
49
|
"Run /goal <objective> to keep long-running work moving across turns.",
|
|
@@ -119,10 +116,8 @@ function nextEditorSuggestion(): string {
|
|
|
119
116
|
return editorSuggestionDeck.pop() ?? EDITOR_SUGGESTIONS[0];
|
|
120
117
|
}
|
|
121
118
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return `${theme.fg("dim", "│")} ${padRight(content, width - 4)} ${theme.fg("dim", "│")}`;
|
|
125
|
-
}
|
|
119
|
+
const MASTHEAD_MARK = ["██████", "▓▓▓▓▓▓", "░░░░░░"];
|
|
120
|
+
const MASTHEAD_GAP = " ";
|
|
126
121
|
|
|
127
122
|
class PiStartupHeader {
|
|
128
123
|
private readonly pi: ExtensionAPI;
|
|
@@ -145,10 +140,8 @@ class PiStartupHeader {
|
|
|
145
140
|
}
|
|
146
141
|
|
|
147
142
|
private tipLines(width: number, theme: Theme): string[] {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
return wrapTextWithAnsi(text, width - indent.length)
|
|
151
|
-
.map((line) => padRight(`${indent}${line}`, width));
|
|
143
|
+
const text = theme.italic(`${theme.fg("text", theme.bold("Tip:"))}${theme.fg("dim", ` ${this.tip}`)}`);
|
|
144
|
+
return wrapTextWithAnsi(text, width);
|
|
152
145
|
}
|
|
153
146
|
|
|
154
147
|
render(width: number): string[] {
|
|
@@ -156,34 +149,18 @@ class PiStartupHeader {
|
|
|
156
149
|
const theme = this.ctx.ui.theme;
|
|
157
150
|
if (width < 28) return [truncateToWidth(theme.fg("text", theme.bold("KillerOS")), width, "")];
|
|
158
151
|
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
const version = KILLEROS_VERSION ? theme.fg("dim", ` (v${KILLEROS_VERSION})`) : "";
|
|
162
|
-
const identity = `${theme.fg("dim", "›")} ${theme.fg("text", theme.bold("KillerOS"))}${version}`;
|
|
152
|
+
const textWidth = Math.max(0, width - MASTHEAD_MARK[0].length - MASTHEAD_GAP.length);
|
|
153
|
+
const identity = `${theme.fg("text", "Pi")}${theme.fg("dim", ` ${PI_VERSION} | `)}${theme.fg("text", "KillerOS")}${KILLEROS_VERSION ? theme.fg("dim", ` ${KILLEROS_VERSION}`) : ""}`;
|
|
163
154
|
const thinkingLevel = this.pi.getThinkingLevel();
|
|
164
|
-
const reasoning = this.ctx.model?.reasoning === false
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const agent = `${formatModel(this.ctx.model, theme)}${theme.fg("dim", " · ")}${reasoning}`;
|
|
155
|
+
const reasoning = this.ctx.model?.reasoning === false ? "no reasoning" : thinkingLevel;
|
|
156
|
+
const modelName = this.ctx.model ? modelDisplayName(this.ctx.model) || "unknown model" : "no model";
|
|
157
|
+
const agent = `${theme.fg("dim", "model: ")}${theme.fg("text", `${modelName} ${reasoning}`)}`;
|
|
168
158
|
const directory = formatCwd(this.ctx.cwd);
|
|
169
|
-
const repository = this.branch
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const agentCommand = `${truncateToWidth(agent, agentWidth, "…")} ${modelCommand}`;
|
|
175
|
-
const border = (left: string, right: string): string => theme.fg("dim", `${left}${"─".repeat(panelWidth - 2)}${right}`);
|
|
176
|
-
const lines = [
|
|
177
|
-
border("╭", "╮"),
|
|
178
|
-
compactBoxLine(identity, panelWidth, theme),
|
|
179
|
-
compactBoxLine("", panelWidth, theme),
|
|
180
|
-
compactBoxLine(agentCommand, panelWidth, theme),
|
|
181
|
-
compactBoxLine(repository, panelWidth, theme),
|
|
182
|
-
border("╰", "╯"),
|
|
183
|
-
" ".repeat(panelWidth),
|
|
184
|
-
...this.tipLines(panelWidth, theme),
|
|
185
|
-
];
|
|
186
|
-
return lines;
|
|
159
|
+
const repository = `${theme.fg("dim", "directory: ")}${theme.fg("text", directory)}${this.branch ? ` ${colorDirectory(this.branch)}` : ""}`;
|
|
160
|
+
const rows = [identity, agent, repository].map((text, index) =>
|
|
161
|
+
`${theme.fg("accent", MASTHEAD_MARK[index] ?? "")}${MASTHEAD_GAP}${truncateToWidth(text, textWidth, "…")}`,
|
|
162
|
+
);
|
|
163
|
+
return [...rows, "", ...this.tipLines(width, theme)];
|
|
187
164
|
}
|
|
188
165
|
|
|
189
166
|
invalidate(): void {}
|
|
@@ -353,9 +330,7 @@ class PiCodeEditor extends CustomEditor {
|
|
|
353
330
|
|
|
354
331
|
for (let index = 1; index < bottomBorderIndex; index += 1) {
|
|
355
332
|
const isPromptLine = index === 1 && !isScrolledHeader;
|
|
356
|
-
const prefix = isPromptLine
|
|
357
|
-
? this.runtimeTheme.fg(this.focused ? "accent" : "dim", "❯\u00A0")
|
|
358
|
-
: " ";
|
|
333
|
+
const prefix = isPromptLine ? this.runtimeTheme.fg("text", "❯\u00A0") : " ";
|
|
359
334
|
let content = lines[index] ?? "";
|
|
360
335
|
if (isPromptLine && this.getText() === "") {
|
|
361
336
|
const first = this.suggestion.slice(0, 1);
|
|
@@ -387,10 +362,10 @@ class PiCodeEditor extends CustomEditor {
|
|
|
387
362
|
}
|
|
388
363
|
|
|
389
364
|
const ACTIVITY_FRAMES = [
|
|
390
|
-
"
|
|
391
|
-
"
|
|
365
|
+
"⠋", "⠙", "⠹", "⠸", "⠼",
|
|
366
|
+
"⠴", "⠦", "⠧", "⠇", "⠏",
|
|
392
367
|
] as const;
|
|
393
|
-
const ACTIVITY_FRAME_INTERVAL_MS =
|
|
368
|
+
const ACTIVITY_FRAME_INTERVAL_MS = 80;
|
|
394
369
|
|
|
395
370
|
let killerosEditorFactory: ReturnType<ExtensionContext["ui"]["getEditorComponent"]>;
|
|
396
371
|
|
package/killeros/variants.ts
CHANGED
|
@@ -1,28 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { SelectList, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
-
import { safeTerminalText } from "./safe-terminal-text.ts";
|
|
1
|
+
import type { ExtensionAPI, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
4
2
|
|
|
5
3
|
export type ThinkingLevel = ReturnType<ExtensionAPI["getThinkingLevel"]>;
|
|
6
4
|
|
|
7
|
-
const ALL_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const satisfies readonly ThinkingLevel[];
|
|
8
|
-
const LEVEL_LABELS: Readonly<Record<ThinkingLevel, string>> = {
|
|
9
|
-
off: "Off",
|
|
10
|
-
minimal: "Minimal",
|
|
11
|
-
low: "Low",
|
|
12
|
-
medium: "Medium",
|
|
13
|
-
high: "High",
|
|
14
|
-
xhigh: "Extra High",
|
|
15
|
-
max: "Maximum",
|
|
16
|
-
};
|
|
17
|
-
const LEVEL_DESCRIPTIONS: Readonly<Record<ThinkingLevel, string>> = {
|
|
18
|
-
off: "No extended reasoning",
|
|
19
|
-
minimal: "Brief reasoning",
|
|
20
|
-
low: "Light reasoning",
|
|
21
|
-
medium: "Balanced reasoning",
|
|
22
|
-
high: "Deep reasoning",
|
|
23
|
-
xhigh: "Extensive reasoning",
|
|
24
|
-
max: "Maximum supported reasoning",
|
|
25
|
-
};
|
|
26
5
|
export const LEVEL_COLORS: Readonly<Record<ThinkingLevel, ThemeColor>> = {
|
|
27
6
|
off: "thinkingOff",
|
|
28
7
|
minimal: "thinkingMinimal",
|
|
@@ -32,152 +11,3 @@ export const LEVEL_COLORS: Readonly<Record<ThinkingLevel, ThemeColor>> = {
|
|
|
32
11
|
xhigh: "thinkingXhigh",
|
|
33
12
|
max: "thinkingMax",
|
|
34
13
|
};
|
|
35
|
-
const LEVEL_ALIASES: Readonly<Record<string, ThinkingLevel>> = {
|
|
36
|
-
quick: "minimal",
|
|
37
|
-
fast: "minimal",
|
|
38
|
-
light: "low",
|
|
39
|
-
balanced: "medium",
|
|
40
|
-
deep: "high",
|
|
41
|
-
maximum: "max",
|
|
42
|
-
none: "off",
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
function isThinkingLevel(value: string): value is ThinkingLevel {
|
|
46
|
-
return ALL_LEVELS.some((level) => level === value);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function resolveThinkingLevel(input: string): ThinkingLevel | undefined {
|
|
50
|
-
const normalized = input.trim().toLowerCase();
|
|
51
|
-
return isThinkingLevel(normalized) ? normalized : LEVEL_ALIASES[normalized];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function supportedLevels(model: ExtensionContext["model"]): ThinkingLevel[] {
|
|
55
|
-
if (!model?.reasoning) return ["off"];
|
|
56
|
-
return ALL_LEVELS.filter((level) => {
|
|
57
|
-
const mapped = model.thinkingLevelMap?.[level];
|
|
58
|
-
if (mapped === null) return false;
|
|
59
|
-
return level !== "xhigh" && level !== "max" || mapped !== undefined;
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function modelLabel(model: ExtensionContext["model"]): string {
|
|
64
|
-
return model ? safeTerminalText(`${model.provider}/${model.id}`).replaceAll("\n", "") : "unknown model";
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function registerVariants(pi: ExtensionAPI): void {
|
|
68
|
-
const setLevel = (ctx: ExtensionContext, level: ThinkingLevel): void => {
|
|
69
|
-
const supported = supportedLevels(ctx.model);
|
|
70
|
-
if (!supported.includes(level)) {
|
|
71
|
-
ctx.ui.notify(`${LEVEL_LABELS[level]} is not supported by ${modelLabel(ctx.model)}. Supported: ${supported.join(", ")}`, "warning");
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
pi.setThinkingLevel(level);
|
|
75
|
-
ctx.ui.notify(`Thinking: ${LEVEL_LABELS[level]}`, "info");
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
pi.registerCommand("variants", {
|
|
79
|
-
description: "Set reasoning level: off, minimal, low, medium, high, xhigh, or max",
|
|
80
|
-
handler: async (args, ctx) => {
|
|
81
|
-
if (args.trim()) {
|
|
82
|
-
const level = resolveThinkingLevel(args);
|
|
83
|
-
if (!level) {
|
|
84
|
-
ctx.ui.notify(`Unknown reasoning level "${safeTerminalText(args.trim()).replaceAll("\n", "")}". Use: ${ALL_LEVELS.join(", ")}`, "error");
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
setLevel(ctx, level);
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
if (ctx.mode !== "tui") {
|
|
91
|
-
ctx.ui.notify("Use /variants <level> outside TUI mode", "error");
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const supported = supportedLevels(ctx.model);
|
|
96
|
-
if (supported.length === 1) {
|
|
97
|
-
ctx.ui.notify(`${modelLabel(ctx.model)} does not support extended reasoning`, "info");
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
const current = pi.getThinkingLevel();
|
|
101
|
-
const items = supported.map((level) => ({
|
|
102
|
-
value: level,
|
|
103
|
-
label: level === current ? `${LEVEL_LABELS[level]} ← current` : LEVEL_LABELS[level],
|
|
104
|
-
description: LEVEL_DESCRIPTIONS[level],
|
|
105
|
-
}));
|
|
106
|
-
const selected = await ctx.ui.custom<ThinkingLevel | null>((tui, theme, keybindings, done) => {
|
|
107
|
-
const listTheme = {
|
|
108
|
-
selectedPrefix: (text: string) => theme.fg("accent", text),
|
|
109
|
-
selectedText: (text: string) => theme.fg("accent", text),
|
|
110
|
-
description: (text: string) => theme.fg("muted", text),
|
|
111
|
-
scrollInfo: (text: string) => theme.fg("dim", text),
|
|
112
|
-
noMatch: (text: string) => theme.fg("warning", text),
|
|
113
|
-
};
|
|
114
|
-
let selectList: SelectList | undefined;
|
|
115
|
-
let visibleOptionRows = 0;
|
|
116
|
-
|
|
117
|
-
const chromeFor = (rowBudget: number): "full" | "compact" | "none" => (
|
|
118
|
-
rowBudget >= 8 ? "full" : rowBudget >= 4 ? "compact" : "none"
|
|
119
|
-
);
|
|
120
|
-
const visibleRowsFor = (rowBudget: number): number => {
|
|
121
|
-
const chrome = chromeFor(rowBudget);
|
|
122
|
-
const chromeRows = chrome === "full" ? 5 : chrome === "compact" ? 2 : 0;
|
|
123
|
-
const availableListRows = Math.max(1, rowBudget - chromeRows);
|
|
124
|
-
return availableListRows >= items.length
|
|
125
|
-
? items.length
|
|
126
|
-
: Math.max(1, availableListRows - 1);
|
|
127
|
-
};
|
|
128
|
-
const ensureSelectList = (nextVisibleOptionRows: number): SelectList => {
|
|
129
|
-
if (selectList && visibleOptionRows === nextVisibleOptionRows) return selectList;
|
|
130
|
-
const selectedValue = selectList?.getSelectedItem()?.value ?? current;
|
|
131
|
-
const nextSelectList = new SelectList(items, nextVisibleOptionRows, listTheme);
|
|
132
|
-
const selectedIndex = items.findIndex((item) => item.value === selectedValue);
|
|
133
|
-
nextSelectList.setSelectedIndex(Math.max(0, selectedIndex));
|
|
134
|
-
nextSelectList.onSelect = (item) => done(isThinkingLevel(item.value) ? item.value : null);
|
|
135
|
-
nextSelectList.onCancel = () => done(null);
|
|
136
|
-
selectList = nextSelectList;
|
|
137
|
-
visibleOptionRows = nextVisibleOptionRows;
|
|
138
|
-
return nextSelectList;
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const border = new DynamicBorder((text: string) => theme.fg("accent", text));
|
|
142
|
-
const title = ` ${theme.fg("accent", theme.bold("Thinking variants"))}`;
|
|
143
|
-
const model = ` ${theme.fg("dim", `Model: ${modelLabel(ctx.model)}`)}`;
|
|
144
|
-
const keyHint = (keybinding: Parameters<typeof keybindings.getKeys>[0], description: string): string => {
|
|
145
|
-
const keyText = keybindings.getKeys(keybinding)
|
|
146
|
-
.join("/")
|
|
147
|
-
.split("/")
|
|
148
|
-
.map((key) => key.split("+").map((part) => process.platform === "darwin" && part.toLowerCase() === "alt" ? "option" : part).join("+"))
|
|
149
|
-
.join("/");
|
|
150
|
-
return theme.fg("dim", keyText) + theme.fg("muted", ` ${description}`);
|
|
151
|
-
};
|
|
152
|
-
const controls = ` ${theme.fg("dim", `${keyHint("tui.select.up", "up")} • ${keyHint("tui.select.down", "down")} • ${keyHint("tui.select.confirm", "select")} • ${keyHint("tui.select.cancel", "cancel")}`)}`;
|
|
153
|
-
|
|
154
|
-
const render = (width: number): string[] => {
|
|
155
|
-
const rowBudget = Math.max(0, tui.terminal.rows);
|
|
156
|
-
if (width <= 0 || rowBudget === 0) return [];
|
|
157
|
-
const chrome = chromeFor(rowBudget);
|
|
158
|
-
const list = ensureSelectList(visibleRowsFor(rowBudget));
|
|
159
|
-
const lines: string[] = [];
|
|
160
|
-
|
|
161
|
-
if (chrome === "full") lines.push(...border.render(width));
|
|
162
|
-
if (chrome !== "none") lines.push(title);
|
|
163
|
-
if (chrome === "full") lines.push(model);
|
|
164
|
-
lines.push(...list.render(width));
|
|
165
|
-
if (chrome !== "none") lines.push(controls);
|
|
166
|
-
if (chrome === "full") lines.push(...border.render(width));
|
|
167
|
-
|
|
168
|
-
return lines.slice(0, rowBudget).map((line) => truncateToWidth(line, width, ""));
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
render,
|
|
173
|
-
invalidate: () => selectList?.invalidate(),
|
|
174
|
-
handleInput: (data) => {
|
|
175
|
-
ensureSelectList(visibleRowsFor(Math.max(1, tui.terminal.rows))).handleInput(data);
|
|
176
|
-
tui.requestRender();
|
|
177
|
-
},
|
|
178
|
-
};
|
|
179
|
-
});
|
|
180
|
-
if (selected) setLevel(ctx, selected);
|
|
181
|
-
},
|
|
182
|
-
});
|
|
183
|
-
}
|
package/killeros/worked-for.ts
CHANGED
|
@@ -53,7 +53,7 @@ export interface WorkedForEntryDataV4 {
|
|
|
53
53
|
type WorkedForEntryData = WorkedForEntryDataV1 | WorkedForEntryDataV2 | WorkedForEntryDataV3 | WorkedForEntryDataV4;
|
|
54
54
|
|
|
55
55
|
const OUTCOMES = {
|
|
56
|
-
done: { label: "Done", color: "success" },
|
|
56
|
+
done: { label: "✓ Done", color: "success" },
|
|
57
57
|
stopped: { label: "■ Stopped", color: "warning" },
|
|
58
58
|
failed: { label: "× Failed", color: "error" },
|
|
59
59
|
} as const satisfies Record<WorkedForOutcome, { label: string; color: string }>;
|
|
@@ -224,47 +224,47 @@ class WorkedForV4Component implements Component {
|
|
|
224
224
|
const lines = [
|
|
225
225
|
`${headline}${theme.fg("dim", ` · ${formatWorkedForDuration(data.milliseconds)} · ↑ ${formatTokens(data.tokens)} tokens${modelSuffix}`)}`,
|
|
226
226
|
];
|
|
227
|
-
if (data.changes.state === "unavailable") lines.push(theme.fg("dim", "
|
|
228
|
-
else if (data.changes.totalFiles === 0) lines.push(theme.fg("dim", "
|
|
227
|
+
if (data.changes.state === "unavailable") lines.push(theme.fg("dim", "Changes unavailable"));
|
|
228
|
+
else if (data.changes.totalFiles === 0) lines.push(theme.fg("dim", "No files changed"));
|
|
229
229
|
else {
|
|
230
230
|
const count = `${data.changes.totalFiles} ${data.changes.totalFiles === 1 ? "file" : "files"}`;
|
|
231
|
-
lines.push(`${theme.fg("accent",
|
|
231
|
+
lines.push(`${theme.fg("accent", `${width < 40 ? count : `Changed ${count}`}`)}${theme.fg("dim", " · ")}${theme.fg("success", `+${data.changes.additions}`)} ${theme.fg("error", `−${data.changes.deletions}`)}`);
|
|
232
232
|
}
|
|
233
233
|
const passed = data.checks.filter((check) => check.outcome === "passed").length + data.omittedChecks.passed;
|
|
234
234
|
const failed = data.checks.filter((check) => check.outcome === "failed").length + data.omittedChecks.failed;
|
|
235
235
|
const totalChecks = passed + failed;
|
|
236
236
|
if (totalChecks === 0) {
|
|
237
|
-
if (data.changes.state === "available" && data.changes.totalFiles > 0) lines.push(theme.fg("warning", "
|
|
237
|
+
if (data.changes.state === "available" && data.changes.totalFiles > 0) lines.push(theme.fg("warning", "No check recorded"));
|
|
238
238
|
} else if (totalChecks === 1) {
|
|
239
239
|
const check = data.checks[0];
|
|
240
|
-
if (check?.outcome === "passed") lines.push(theme.fg("success", `
|
|
241
|
-
else if (check) lines.push(theme.fg("error", `
|
|
240
|
+
if (check?.outcome === "passed") lines.push(theme.fg("success", `Check passed: ${check.label} ✓`));
|
|
241
|
+
else if (check) lines.push(theme.fg("error", `Check failed: ${check.label} ×`));
|
|
242
242
|
} else if (failed === 0) {
|
|
243
|
-
lines.push(theme.fg("success", `
|
|
243
|
+
lines.push(theme.fg("success", `Checks: ${passed} passed`));
|
|
244
244
|
} else {
|
|
245
|
-
lines.push(
|
|
245
|
+
lines.push(`${theme.fg("accent", "Checks:")} ${theme.fg("success", `${passed} passed`)}${theme.fg("dim", " · ")}${theme.fg("error", `${failed} failed`)}`);
|
|
246
246
|
}
|
|
247
247
|
if (this.expanded && data.changes.state === "available") {
|
|
248
248
|
for (const file of data.changes.files) {
|
|
249
249
|
const marker = file.kind === "added" ? "A" : file.kind === "deleted" ? "D" : file.kind === "renamed" ? "R" : "M";
|
|
250
250
|
const label = file.kind === "renamed" ? `${safePath(file.previousPath)} → ${safePath(file.path)}` : safePath(file.path);
|
|
251
|
-
const prefix =
|
|
251
|
+
const prefix = `${marker} `;
|
|
252
252
|
const detail = file.detail ? ` ${file.detail}` : ` +${file.additions} −${file.deletions}`;
|
|
253
|
-
const labelWidth = width - visibleWidth(prefix) - visibleWidth(detail);
|
|
253
|
+
const labelWidth = width - 1 - visibleWidth(prefix) - visibleWidth(detail);
|
|
254
254
|
const fittedLabel = labelWidth > 0 ? truncateToWidth(label, labelWidth, "…") : "";
|
|
255
255
|
const styledDetail = file.detail
|
|
256
256
|
? theme.fg("dim", detail)
|
|
257
257
|
: `${theme.fg("success", ` +${file.additions}`)} ${theme.fg("error", `−${file.deletions}`)}`;
|
|
258
258
|
lines.push(`${theme.fg("accent", `${prefix}${fittedLabel}`)}${styledDetail}`);
|
|
259
259
|
}
|
|
260
|
-
if (data.changes.omittedFiles > 0) lines.push(theme.fg("dim",
|
|
260
|
+
if (data.changes.omittedFiles > 0) lines.push(theme.fg("dim", `… ${data.changes.omittedFiles} more files`));
|
|
261
261
|
}
|
|
262
262
|
if (this.expanded) {
|
|
263
|
-
for (const check of data.checks) lines.push(theme.fg(check.outcome === "passed" ? "success" : "error",
|
|
263
|
+
for (const check of data.checks) lines.push(theme.fg(check.outcome === "passed" ? "success" : "error", `${check.outcome === "passed" ? "✓" : "×"} ${check.label}`));
|
|
264
264
|
const omitted = data.omittedChecks.passed + data.omittedChecks.failed;
|
|
265
|
-
if (omitted > 0) lines.push(theme.fg("dim",
|
|
265
|
+
if (omitted > 0) lines.push(theme.fg("dim", `… ${omitted} more checks`));
|
|
266
266
|
}
|
|
267
|
-
return lines.map((line) => truncateToWidth(line
|
|
267
|
+
return lines.map((line) => truncateToWidth(` ${line}`, width, "…"));
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
invalidate(): void {}
|
|
@@ -321,12 +321,12 @@ export function registerWorkedFor(
|
|
|
321
321
|
pi.registerEntryRenderer<WorkedForEntryData>(WORKED_FOR_ENTRY_TYPE, (entry, options, theme) => {
|
|
322
322
|
const data = parseWorkedForEntryData(entry.data);
|
|
323
323
|
if (!data) return undefined;
|
|
324
|
-
if (data.version === 1) return new Text(theme.fg("dim", `✻ Worked for ${formatWorkedForDuration(data.milliseconds)}`),
|
|
324
|
+
if (data.version === 1) return new Text(theme.fg("dim", `✻ Worked for ${formatWorkedForDuration(data.milliseconds)}`), 1, 0);
|
|
325
325
|
if (data.version === 4) return new WorkedForV4Component(data, options.expanded, theme);
|
|
326
326
|
const outcome = OUTCOMES[data.outcome];
|
|
327
327
|
const tokens = data.version === 3 ? ` · ↑ ${formatTokens(data.tokens)} tokens` : "";
|
|
328
328
|
const headline = theme.fg(outcome.color, outcome.label);
|
|
329
|
-
return new Text(`${headline}${theme.fg("dim", ` · ${formatWorkedForDuration(data.milliseconds)}${tokens}`)}`,
|
|
329
|
+
return new Text(`${headline}${theme.fg("dim", ` · ${formatWorkedForDuration(data.milliseconds)}${tokens}`)}`, 1, 0);
|
|
330
330
|
});
|
|
331
331
|
|
|
332
332
|
pi.on("session_start", async () => {
|