killeros 2.0.8 → 2.0.9

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 CHANGED
@@ -4,6 +4,12 @@ All notable changes to KillerOS are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.0.9] - 2026-08-15
8
+
9
+ ### Added
10
+
11
+ - Highlighted exact, currently registered slash commands in the TUI editor with the theme's shared command-blue link role while preserving autocomplete boundaries and ANSI cursor styling.
12
+
7
13
  ## [2.0.8] - 2026-08-15
8
14
 
9
15
  ### Added
package/Killeros.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { registerRequestActivity } from "./killeros/activity.ts";
3
- import { registerAliases, registerSlashAutocomplete } from "./killeros/commands.ts";
3
+ import {
4
+ createSlashCommandResolver,
5
+ registerAliases,
6
+ registerSlashAutocomplete,
7
+ } from "./killeros/commands.ts";
4
8
  import { registerFooter } from "./killeros/footer.ts";
5
9
  import { registerGoal, registerGoalSettlement } from "./killeros/goals.ts";
6
10
  import { registerLifecycleHooks } from "./killeros/hooks.ts";
@@ -42,7 +46,8 @@ export interface KillerosOptions {
42
46
  export default function Killeros(pi: ExtensionAPI, options: KillerosOptions = {}): void {
43
47
  const initRuntime = createInitRuntime();
44
48
  const goalRuntime = createGoalRuntime();
45
- registerShellUi(pi);
49
+ const commandResolver = createSlashCommandResolver(pi);
50
+ registerShellUi(pi, commandResolver);
46
51
  registerGoal(pi, goalRuntime, initRuntime);
47
52
  registerPersonalInstructions(pi, initRuntime);
48
53
  const questionRunner: QuestionRunner = registerQuestionTool(pi);
@@ -52,7 +57,7 @@ export default function Killeros(pi: ExtensionAPI, options: KillerosOptions = {}
52
57
  options.decisionGatedWorkflows ?? [createDecisionGatedWorkflowAdapter()],
53
58
  );
54
59
  registerAliases(pi);
55
- registerSlashAutocomplete(pi);
60
+ registerSlashAutocomplete(pi, commandResolver);
56
61
  registerFooter(pi, goalRuntime);
57
62
  registerVariants(pi);
58
63
  registerInitCommand(pi, initRuntime, goalRuntime);
package/README.md CHANGED
@@ -1,82 +1,82 @@
1
- # KillerOS
2
-
1
+ # KillerOS
2
+
3
3
  A production-hardened Pi extension that combines a custom TUI, repository initialization, long-running goals, reasoning controls, interactive questions, and command aliases.
4
-
5
- ## Requirements
6
-
7
- - Node.js `22.19.0` or later
4
+
5
+ ## Requirements
6
+
7
+ - Node.js `22.19.0` or later
8
8
  - Pi `0.84.2` or later
9
- - Interactive TUI mode for the custom header, editor, footer, `question` tool, and `/init`
10
-
11
- The extension is strict TypeScript. Pi provides the runtime modules.
12
-
13
- ## Install
14
-
15
- ### npm
16
-
17
- Install KillerOS:
18
-
19
- ```bash
20
- pi install npm:killeros
21
- ```
22
-
23
- ### Git
24
-
25
- Install the latest commit:
26
-
27
- ```bash
28
- pi install git:github.com/KyrosHendrix/pi-KillerOS
29
- ```
30
-
31
- Pin an install to a release:
32
-
33
- ```bash
34
- pi install git:github.com/KyrosHendrix/pi-KillerOS@v2.0.8
35
- ```
36
-
37
- Add `-l` to either command for a project-only install. Restart Pi after installing.
38
-
39
- ## Features
40
-
41
- - 52-column Compact startup card with inline version, polished model/provider identity, adjacent `/model`, directory, conditional Git branch, and a shuffled session-stable tip
42
- - Cohesive dark theme with coral accents and neutral tool-call containers across pending, success, and error states
9
+ - Interactive TUI mode for the custom header, editor, footer, `question` tool, and `/init`
10
+
11
+ The extension is strict TypeScript. Pi provides the runtime modules.
12
+
13
+ ## Install
14
+
15
+ ### npm
16
+
17
+ Install KillerOS:
18
+
19
+ ```bash
20
+ pi install npm:killeros
21
+ ```
22
+
23
+ ### Git
24
+
25
+ Install the latest commit:
26
+
27
+ ```bash
28
+ pi install git:github.com/KyrosHendrix/pi-KillerOS
29
+ ```
30
+
31
+ Pin an install to a release:
32
+
33
+ ```bash
34
+ pi install git:github.com/KyrosHendrix/pi-KillerOS@v2.0.9
35
+ ```
36
+
37
+ Add `-l` to either command for a project-only install. Restart Pi after installing.
38
+
39
+ ## Features
40
+
41
+ - 52-column Compact startup card with inline version, polished model/provider identity, adjacent `/model`, directory, conditional Git branch, and a shuffled session-stable tip
42
+ - Cohesive dark theme with coral accents and neutral tool-call containers across pending, success, and error states
43
43
  - Animated orange 12-frame activity glyph loop at 120 ms per frame with contextual copy derived from request, tool, result, and response events, plus a quiet hidden-thinking label
44
44
  - Frameless multiline editor with one focus-aware `❯`, a shuffled session-stable empty-state suggestion, overflow-only scroll indicators, Shift+Enter support, and slash-command autocomplete; KillerOS preserves an editor factory configured by another extension
45
45
  - One compact TUI transcript line reporting truthful `Done`, `Stopped`, or `Failed` settlement with elapsed time while preserving older `✻ Worked for …` entries
46
46
  - Compact two-deck footer with session state above workspace state; model, context, and active goals stay prioritized as reasoning, time, cost, branch, and path reduce by available width
47
- - Pi-owned context compaction with active goals continuing from Pi's settled boundary after manual, threshold, and overflow compaction
48
- - Optional completion sounds after successful or failed settled requests, excluding manual aborts
49
- - `/variants` selector and direct reasoning-level arguments
50
- - Codex-style `/goal` with an interactive status/action panel, durable objectives, immediate pause and clear cancellation, automatic continuation, explicit completion, and durable blocker audits
47
+ - Pi-owned context compaction with active goals continuing from Pi's settled boundary after manual, threshold, and overflow compaction
48
+ - Optional completion sounds after successful or failed settled requests, excluding manual aborts
49
+ - `/variants` selector and direct reasoning-level arguments
50
+ - Codex-style `/goal` with an interactive status/action panel, durable objectives, immediate pause and clear cancellation, automatic continuation, explicit completion, and durable blocker audits
51
51
  - Automatic `/init` guideline synthesis with a frozen safe evidence map, protected existing policy, and the four packaged behavioral sections adapted from `writing-great-guidelines`
52
52
  - Opt-in decision-gated workflows that ask a structured policy question before explicit skill expansion, preserve the selected allowlist, and fail closed across tool and session boundaries
53
- - `question` tool with single-select and opt-in bounded multi-select, height-bounded option windows, configured Pi keybindings, live option/input progress, proposal previews, custom answers, history, cancellation, and compact expandable transcript rendering
53
+ - `question` tool with single-select and opt-in bounded multi-select, height-bounded option windows, configured Pi keybindings, live option/input progress, proposal previews, custom answers, history, cancellation, and compact expandable transcript rendering
54
54
  - Mid-prompt slash completion with current Pi `0.84.2` commands, extensions, prompts, and skills; paths, URLs, and invalid commands remain plain text
55
- - Goal-aware `/clear` that confirms, aborts active work, waits for settlement, and starts a new session, plus `/exit` for graceful shutdown
56
-
57
- ## Commands
58
-
59
- ```text
60
- /init Generate root AGENTS.md from repository evidence
61
- /goal Open current goal status and valid actions
62
- /goal <objective> Set an objective and start working
63
- /goal edit Edit and reactivate the current goal
64
- /goal pause Stop the current goal turn and automatic continuation
65
- /goal resume Resume automatic continuation
66
- /goal clear Stop current goal work and remove the goal
67
- /variants Open the reasoning-level selector
68
- /variants high Set a reasoning level directly
69
- /notification Configure the completion sound
70
- /clear Start a new session after confirmation
71
- /exit Quit Pi gracefully
72
- ```
73
-
55
+ - Goal-aware `/clear` that confirms, aborts active work, waits for settlement, and starts a new session, plus `/exit` for graceful shutdown
56
+
57
+ ## Commands
58
+
59
+ ```text
60
+ /init Generate root AGENTS.md from repository evidence
61
+ /goal Open current goal status and valid actions
62
+ /goal <objective> Set an objective and start working
63
+ /goal edit Edit and reactivate the current goal
64
+ /goal pause Stop the current goal turn and automatic continuation
65
+ /goal resume Resume automatic continuation
66
+ /goal clear Stop current goal work and remove the goal
67
+ /variants Open the reasoning-level selector
68
+ /variants high Set a reasoning level directly
69
+ /notification Configure the completion sound
70
+ /clear Start a new session after confirmation
71
+ /exit Quit Pi gracefully
72
+ ```
73
+
74
74
  `/goal` requires a saved session in TUI or RPC mode. Goal state is stored in versioned session entries on the active branch and restored after reload, resume, fork, or tree navigation. Active goals inject their unchanged objective every turn and continue one settled turn at a time. The model must use KillerOS’s private goal tool to report completion. For an objective that clearly asks to create, write, save, or generate a named file-like deliverable at one quoted absolute path, KillerOS saves that exact path with the goal and verifies that a regular file exists there before accepting completion. Other objectives retain model-reported completion. Blocking requires one stable lowercase blocker key recorded on three consecutive goal turns; a changed key, skipped turn, resume, or edit resets the streak. Final prose alone does not end the loop.
75
-
76
- `/goal pause` and `/goal clear` save paused or cleared state before aborting current goal work, so settlement cannot restart it. Aborted turns, provider failures, and continuation failures otherwise pause safely. Failed edit and replacement writes dispatch no edited objective; an active prior objective pauses fail-closed, while inactive durable state remains unchanged. Replacing unfinished work requires confirmation, and `/goal edit` requires TUI mode.
77
-
78
- `/init` freezes a safe project-file map and exposes only dedicated read and list operations while it generates root `AGENTS.md`. Git-ignored files, known secret paths, private-key formats, other guidance, dependencies, links, non-regular files, and files outside that map are unavailable. Existing root `AGENTS.md` is separate protected policy: compatible rules are preserved, a real policy conflict leaves it unchanged with a reason, and any concurrent target change aborts installation without replacing the newer file.
79
-
75
+
76
+ `/goal pause` and `/goal clear` save paused or cleared state before aborting current goal work, so settlement cannot restart it. Aborted turns, provider failures, and continuation failures otherwise pause safely. Failed edit and replacement writes dispatch no edited objective; an active prior objective pauses fail-closed, while inactive durable state remains unchanged. Replacing unfinished work requires confirmation, and `/goal edit` requires TUI mode.
77
+
78
+ `/init` freezes a safe project-file map and exposes only dedicated read and list operations while it generates root `AGENTS.md`. Git-ignored files, known secret paths, private-key formats, other guidance, dependencies, links, non-regular files, and files outside that map are unavailable. Existing root `AGENTS.md` is separate protected policy: compatible rules are preserved, a real policy conflict leaves it unchanged with a reason, and any concurrent target change aborts installation without replacing the newer file.
79
+
80
80
  The generated file uses the four behavioral sections adapted from `writing-great-guidelines`; no external skill installation is required. `/init` asks no setup questions, starts no second model process, writes no other file, and reloads Pi resources only after a successful write.
81
81
 
82
82
  ### Decision-gated workflows
@@ -84,68 +84,68 @@ The generated file uses the four behavioral sections adapted from `writing-great
84
84
  Explicit `/skill:decision-gated-workflow` activation opens the shared question UI before Pi expands the skill. `Normal` allows only interview and read-only tools; `With docs` additionally permits agreed glossary, context-map, and ADR paths. The selected policy remains active until the workflow is explicitly finished or cancelled, and lifecycle changes clear it safely. Extensions can supply additional adapters through `KillerosOptions.decisionGatedWorkflows`.
85
85
 
86
86
  ### Interactive questions
87
-
87
+
88
88
  Single-select remains the default. Explicit `minSelections: 1` and `maxSelections: 1` are equivalent to omitting both bounds; other single-select bounds are rejected. An agent opts into multi-select with `mode: "multiple"` and may set `minSelections` and `maxSelections`; the custom answer counts as one selection.
89
-
90
- In multi-select, use Space or a visible number to toggle an option, `/` to filter, and Enter to submit. The filter accepts spaces; Enter applies it and Escape returns to the choices. Checked options remain selected when the filter changes. Select **Type a custom answer** with Enter to add or edit one custom item alongside checked options.
91
-
92
- Supported reasoning levels are `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`. KillerOS limits choices to levels supported by the current model.
93
-
94
- ## Configuration
95
-
96
- 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.
97
-
98
- The completion sound is a global user preference stored in Pi's agent directory and is off by default. Run `/notification` in TUI mode to enable or disable it; enabling does not play a preview. Enabled TUI tabs append `󰂚`, which requires a Nerd Font in the terminal tab UI. An unsupported font may show a box without affecting sound. KillerOS uses the terminal's audible bell and cannot produce sound when the terminal disables it.
99
-
100
- 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. An active goal replaces the right-side path with warning-yellow `/goal is active (...)` and keeps exact seconds in minute and hour formats. Paused and blocked goals retain their existing placement; completed goals remain in transcript history and `/goal` status rather than the footer. At narrow widths, context pressure and actionable goal state take priority.
101
-
102
- Pi decides when compaction runs and Pi writes the summary, applies manual focus instructions, tracks files, retries summarization, and handles overflow recovery. KillerOS does not add a second threshold or replace Pi's summary. Active `/goal` work continues from Pi's settled boundary, after Pi finishes retries, compaction, and queued work.
103
-
104
- Manual `/compact` aborts the current goal turn before summarization, so KillerOS records an honest temporary pause for that exact goal revision. After Pi saves the manual summary, KillerOS resumes that revision automatically. A failed or cancelled manual compaction stays paused; run `/goal pause` during the pause to cancel automatic recovery.
105
-
106
- For trusted projects, KillerOS loads `AGENTS.local.md` after Pi's shared repository context. A one-line `@path` or `@~/path` file imports personal guidance from another location.
107
-
108
- Lifecycle hooks are loaded from `.pi/killeros-hooks.json` at session start. Supported event keys are `tool_call`, `tool_result`, and `agent_settled`. Optional matchers are JavaScript regular expressions over Pi tool names, so they are valid only for `tool_call` and `tool_result`; KillerOS rejects an `agent_settled` hook that defines a matcher. Hook commands run from the repository root with `KILLEROS_EVENT`, `KILLEROS_TOOL`, and `KILLEROS_PAYLOAD` environment variables. Failed `tool_call` hooks block the tool, while later-event failures notify the user. Aborting the parent request stops the hook process tree with bounded graceful and forced cleanup without reporting cancellation as a hook failure.
109
-
110
- ## Behavior by mode
111
-
112
- | Mode | Behavior |
113
- |---|---|
114
- | TUI | All features are available, including the completion sound and tab-title indicator |
89
+
90
+ In multi-select, use Space or a visible number to toggle an option, `/` to filter, and Enter to submit. The filter accepts spaces; Enter applies it and Escape returns to the choices. Checked options remain selected when the filter changes. Select **Type a custom answer** with Enter to add or edit one custom item alongside checked options.
91
+
92
+ Supported reasoning levels are `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`. KillerOS limits choices to levels supported by the current model.
93
+
94
+ ## Configuration
95
+
96
+ 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.
97
+
98
+ The completion sound is a global user preference stored in Pi's agent directory and is off by default. Run `/notification` in TUI mode to enable or disable it; enabling does not play a preview. Enabled TUI tabs append `󰂚`, which requires a Nerd Font in the terminal tab UI. An unsupported font may show a box without affecting sound. KillerOS uses the terminal's audible bell and cannot produce sound when the terminal disables it.
99
+
100
+ 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. An active goal replaces the right-side path with warning-yellow `/goal is active (...)` and keeps exact seconds in minute and hour formats. Paused and blocked goals retain their existing placement; completed goals remain in transcript history and `/goal` status rather than the footer. At narrow widths, context pressure and actionable goal state take priority.
101
+
102
+ Pi decides when compaction runs and Pi writes the summary, applies manual focus instructions, tracks files, retries summarization, and handles overflow recovery. KillerOS does not add a second threshold or replace Pi's summary. Active `/goal` work continues from Pi's settled boundary, after Pi finishes retries, compaction, and queued work.
103
+
104
+ Manual `/compact` aborts the current goal turn before summarization, so KillerOS records an honest temporary pause for that exact goal revision. After Pi saves the manual summary, KillerOS resumes that revision automatically. A failed or cancelled manual compaction stays paused; run `/goal pause` during the pause to cancel automatic recovery.
105
+
106
+ For trusted projects, KillerOS loads `AGENTS.local.md` after Pi's shared repository context. A one-line `@path` or `@~/path` file imports personal guidance from another location.
107
+
108
+ Lifecycle hooks are loaded from `.pi/killeros-hooks.json` at session start. Supported event keys are `tool_call`, `tool_result`, and `agent_settled`. Optional matchers are JavaScript regular expressions over Pi tool names, so they are valid only for `tool_call` and `tool_result`; KillerOS rejects an `agent_settled` hook that defines a matcher. Hook commands run from the repository root with `KILLEROS_EVENT`, `KILLEROS_TOOL`, and `KILLEROS_PAYLOAD` environment variables. Failed `tool_call` hooks block the tool, while later-event failures notify the user. Aborting the parent request stops the hook process tree with bounded graceful and forced cleanup without reporting cancellation as a hook failure.
109
+
110
+ ## Behavior by mode
111
+
112
+ | Mode | Behavior |
113
+ |---|---|
114
+ | TUI | All features are available, including the completion sound and tab-title indicator |
115
115
  | RPC | Goal set/view/pause/resume/clear work; TUI components, `/goal edit`, `/init`, completion sounds, and the title indicator are disabled |
116
116
  | Print/JSON | Interactive questions, `/goal`, and `/init` fail explicitly; completion sounds and the title indicator are disabled |
117
-
118
- ## Validation
119
-
120
- Before release, run:
121
-
122
- ```bash
123
- npm ci
124
- npm run check
125
- npm test
126
- npm pack --dry-run
127
- pi -ne -e . --mode rpc
128
- ```
129
-
130
- The package manifest lists Pi’s built-in modules as peer dependencies, so npm does not bundle a second copy.
131
-
132
- ## Publish
133
-
134
- To create a GitHub release, update the version in `package.json` and `package-lock.json`, add the matching `CHANGELOG.md` section, and push the release commit to `main`. After the full CI workflow passes, the release workflow creates the matching tag and GitHub release from that verified commit.
135
-
136
- Do not manually tag a normal release. If automation must recover a missing GitHub release, push the matching version tag; the same workflow validates the tag against the package and changelog before creating the release.
137
-
138
- The [`pi-package`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md) keyword makes a published npm release visible in Pi’s package catalog. GitHub release automation does not publish to npm. Publish there separately after validation:
139
-
140
- ```bash
141
- npm login
142
- npm publish
143
- ```
144
-
145
- ## Security
146
-
147
- Pi extensions run with your user permissions. Review the source before installing KillerOS globally. KillerOS executes lifecycle hook commands only for projects Pi marks as trusted; review `.pi/killeros-hooks.json` before enabling project trust.
148
-
149
- ## License
150
-
151
- [MIT](LICENSE) © 2026 KyrosHendrix
117
+
118
+ ## Validation
119
+
120
+ Before release, run:
121
+
122
+ ```bash
123
+ npm ci
124
+ npm run check
125
+ npm test
126
+ npm pack --dry-run
127
+ pi -ne -e . --mode rpc
128
+ ```
129
+
130
+ The package manifest lists Pi’s built-in modules as peer dependencies, so npm does not bundle a second copy.
131
+
132
+ ## Publish
133
+
134
+ To create a GitHub release, update the version in `package.json` and `package-lock.json`, add the matching `CHANGELOG.md` section, and push the release commit to `main`. After the full CI workflow passes, the release workflow creates the matching tag and GitHub release from that verified commit.
135
+
136
+ Do not manually tag a normal release. If automation must recover a missing GitHub release, push the matching version tag; the same workflow validates the tag against the package and changelog before creating the release.
137
+
138
+ The [`pi-package`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md) keyword makes a published npm release visible in Pi’s package catalog. GitHub release automation does not publish to npm. Publish there separately after validation:
139
+
140
+ ```bash
141
+ npm login
142
+ npm publish
143
+ ```
144
+
145
+ ## Security
146
+
147
+ Pi extensions run with your user permissions. Review the source before installing KillerOS globally. KillerOS executes lifecycle hook commands only for projects Pi marks as trusted; review `.pi/killeros-hooks.json` before enabling project trust.
148
+
149
+ ## License
150
+
151
+ [MIT](LICENSE) © 2026 KyrosHendrix
@@ -30,6 +30,19 @@ interface CommandInfo {
30
30
  syntaxHint?: string;
31
31
  }
32
32
 
33
+ export interface SlashCommandToken {
34
+ name: string;
35
+ start: number;
36
+ end: number;
37
+ }
38
+
39
+ export interface SlashCommandResolver {
40
+ clearFallbackCommands(): void;
41
+ updateFallbackCommands(items: readonly AutocompleteItem[]): void;
42
+ getCommandCatalog(baseSuggestions?: readonly AutocompleteItem[]): ReadonlyMap<string, CommandInfo>;
43
+ isValidCommand(name: string): boolean;
44
+ }
45
+
33
46
  const BUILTIN_COMMANDS: ReadonlyArray<{ name: string; description: string }> = [
34
47
  { name: "settings", description: "Open settings menu" },
35
48
  { name: "model", description: "Select model" },
@@ -70,6 +83,87 @@ interface TaggedAutocompleteItem extends AutocompleteItem {
70
83
  killerosCommand?: string;
71
84
  }
72
85
 
86
+ const SLASH_COMMAND_PREFIX_PATTERN = /(?:^|[ \t])\/([^\s/]*)$/u;
87
+ const SLASH_COMMAND_TOKEN_PATTERN = /(?:^|[ \t])\/([^\s/]+)(?=$|[ \t])/gu;
88
+
89
+ export function getSlashCommandPrefix(line: string): { prefix: string; slashIndex: number } | undefined {
90
+ const match = SLASH_COMMAND_PREFIX_PATTERN.exec(line);
91
+ if (!match || match.index === undefined) return undefined;
92
+ const prefix = match[1] ?? "";
93
+ const slashIndex = match.index + (match[0].startsWith("/") ? 0 : 1);
94
+ return { prefix, slashIndex };
95
+ }
96
+
97
+ export function findSlashCommandTokens(line: string): SlashCommandToken[] {
98
+ const tokens: SlashCommandToken[] = [];
99
+ for (const match of line.matchAll(SLASH_COMMAND_TOKEN_PATTERN)) {
100
+ const name = match[1];
101
+ if (name === undefined || match.index === undefined) continue;
102
+ const start = match.index + (match[0].startsWith("/") ? 0 : 1);
103
+ tokens.push({ name, start, end: start + name.length + 1 });
104
+ }
105
+ return tokens;
106
+ }
107
+
108
+ function commandNameFromAutocompleteItem(item: AutocompleteItem): string {
109
+ return (item.value || item.label).replace(/^\//u, "").trim().split(/\s+/u)[0] ?? "";
110
+ }
111
+
112
+ export function createSlashCommandResolver(
113
+ pi: Pick<ExtensionAPI, "getCommands">,
114
+ ): SlashCommandResolver {
115
+ let fallbackCommands = new Map<string, string | undefined>();
116
+
117
+ const getCommandCatalog = (baseSuggestions: readonly AutocompleteItem[] = []): ReadonlyMap<string, CommandInfo> => {
118
+ const commands = new Map<string, CommandInfo>();
119
+ BUILTIN_COMMANDS.forEach((command) => commands.set(command.name, {
120
+ ...command,
121
+ category: "Built-in",
122
+ syntaxHint: COMMAND_SYNTAX_HINTS[command.name],
123
+ }));
124
+
125
+ for (const command of pi.getCommands()) {
126
+ const category: CommandInfo["category"] = command.source === "skill"
127
+ ? "Skill"
128
+ : command.source === "prompt"
129
+ ? "Prompt"
130
+ : "Extension";
131
+ commands.set(command.name, {
132
+ name: command.name,
133
+ description: command.description,
134
+ category,
135
+ syntaxHint: COMMAND_SYNTAX_HINTS[command.name],
136
+ });
137
+ }
138
+
139
+ const baseCommands = baseSuggestions.length > 0
140
+ ? new Map(baseSuggestions.map((item) => [commandNameFromAutocompleteItem(item), item.description] as const))
141
+ : fallbackCommands;
142
+ for (const [name, description] of baseCommands) {
143
+ if (name && !commands.has(name)) {
144
+ commands.set(name, { name, description, category: "Built-in" });
145
+ }
146
+ }
147
+ return commands;
148
+ };
149
+
150
+ return {
151
+ clearFallbackCommands() {
152
+ fallbackCommands = new Map<string, string | undefined>();
153
+ },
154
+ updateFallbackCommands(items) {
155
+ fallbackCommands = new Map(
156
+ items.map((item) => [commandNameFromAutocompleteItem(item), item.description] as const)
157
+ .filter(([name]) => Boolean(name)),
158
+ );
159
+ },
160
+ getCommandCatalog,
161
+ isValidCommand(name) {
162
+ return getCommandCatalog().has(name);
163
+ },
164
+ };
165
+ }
166
+
73
167
  function scoreCommandMatch(name: string, prefix: string): number {
74
168
  if (!prefix) return 1;
75
169
  const normalizedName = name.toLocaleLowerCase();
@@ -80,47 +174,26 @@ function scoreCommandMatch(name: string, prefix: string): number {
80
174
  return 0;
81
175
  }
82
176
 
83
- export function registerSlashAutocomplete(pi: ExtensionAPI): void {
177
+ export function registerSlashAutocomplete(
178
+ pi: ExtensionAPI,
179
+ resolver: SlashCommandResolver = createSlashCommandResolver(pi),
180
+ ): SlashCommandResolver {
84
181
  const usage = new Map<string, number>();
85
182
  pi.on("session_start", (_event, ctx) => {
86
183
  if (ctx.mode !== "tui") return;
184
+ resolver.clearFallbackCommands();
87
185
  ctx.ui.addAutocompleteProvider((current) => ({
88
186
  triggerCharacters: ["/"],
89
187
  async getSuggestions(lines, cursorLine, cursorCol, options) {
90
188
  const line = lines[cursorLine] ?? "";
91
189
  const beforeCursor = line.slice(0, cursorCol);
92
- const match = beforeCursor.match(/(?:^|[ \t])\/([^\s/]*)$/);
93
- if (!match) return current.getSuggestions(lines, cursorLine, cursorCol, options);
190
+ const prefixMatch = getSlashCommandPrefix(beforeCursor);
191
+ if (!prefixMatch) return current.getSuggestions(lines, cursorLine, cursorCol, options);
94
192
 
95
- const prefix = (match[1] ?? "").toLocaleLowerCase();
193
+ const prefix = prefixMatch.prefix.toLocaleLowerCase();
96
194
  const baseSuggestions = await current.getSuggestions(lines, cursorLine, cursorCol, options);
97
- const commands = new Map<string, CommandInfo>();
98
- BUILTIN_COMMANDS.forEach((command) => commands.set(command.name, {
99
- ...command,
100
- category: "Built-in",
101
- syntaxHint: COMMAND_SYNTAX_HINTS[command.name],
102
- }));
103
-
104
- for (const command of pi.getCommands()) {
105
- const category: CommandInfo["category"] = command.source === "skill"
106
- ? "Skill"
107
- : command.source === "prompt"
108
- ? "Prompt"
109
- : "Extension";
110
- commands.set(command.name, {
111
- name: command.name,
112
- description: command.description,
113
- category,
114
- syntaxHint: COMMAND_SYNTAX_HINTS[command.name],
115
- });
116
- }
117
-
118
- for (const item of baseSuggestions?.items ?? []) {
119
- const name = (item.value || item.label).replace(/^\//, "").trim().split(/\s+/)[0] ?? "";
120
- if (name && !commands.has(name)) {
121
- commands.set(name, { name, description: item.description, category: "Built-in" });
122
- }
123
- }
195
+ resolver.updateFallbackCommands(baseSuggestions?.items ?? []);
196
+ const commands = resolver.getCommandCatalog(baseSuggestions?.items ?? []);
124
197
 
125
198
  const ranked = [...commands.values()]
126
199
  .map((command) => ({
@@ -151,9 +224,9 @@ export function registerSlashAutocomplete(pi: ExtensionAPI): void {
151
224
  const line = lines[cursorLine] ?? "";
152
225
  const beforeCursor = line.slice(0, cursorCol);
153
226
  const afterCursor = line.slice(cursorCol);
154
- const match = beforeCursor.match(/(?:^|[ \t])\/([^\s/]*)$/);
155
- if (!match || match.index === undefined) return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
156
- const slashIndex = match.index + (match[0].startsWith("/") ? 0 : 1);
227
+ const prefixMatch = getSlashCommandPrefix(beforeCursor);
228
+ if (!prefixMatch) return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
229
+ const slashIndex = prefixMatch.slashIndex;
157
230
  const newBefore = beforeCursor.slice(0, slashIndex) + item.value;
158
231
  const nextLines = [...lines];
159
232
  nextLines[cursorLine] = newBefore + afterCursor;
@@ -164,4 +237,5 @@ export function registerSlashAutocomplete(pi: ExtensionAPI): void {
164
237
  },
165
238
  }));
166
239
  });
240
+ return resolver;
167
241
  }
@@ -10,6 +10,7 @@ import {
10
10
  } from "@earendil-works/pi-coding-agent";
11
11
  import {
12
12
  CURSOR_MARKER,
13
+ stripTerminalSequences,
13
14
  truncateToWidth,
14
15
  visibleWidth,
15
16
  wrapTextWithAnsi,
@@ -17,6 +18,11 @@ import {
17
18
  type TUI,
18
19
  } from "@earendil-works/pi-tui";
19
20
  import { formatCwd, padRight } from "./display.ts";
21
+ import {
22
+ createSlashCommandResolver,
23
+ findSlashCommandTokens,
24
+ type SlashCommandResolver,
25
+ } from "./commands.ts";
20
26
  import { reportError } from "./errors.ts";
21
27
  import { formatModel } from "./footer.ts";
22
28
  import { LEVEL_COLORS, type ThinkingLevel } from "./variants.ts";
@@ -166,10 +172,8 @@ class PiStartupHeader {
166
172
  }
167
173
  }
168
174
 
169
- const ANSI_REGEX = /\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
170
-
171
175
  function stripAnsi(text: string): string {
172
- return text.replace(ANSI_REGEX, "").trim();
176
+ return stripTerminalSequences(text).trim();
173
177
  }
174
178
 
175
179
  function isBorderLine(line: string): boolean {
@@ -182,10 +186,98 @@ function isScrolledTopBorder(line: string): boolean {
182
186
  return unstyled.includes("↑");
183
187
  }
184
188
 
189
+ interface RenderChunk {
190
+ text: string;
191
+ plainStart: number;
192
+ isAnsi: boolean;
193
+ }
194
+
195
+ function extractTerminalSequence(text: string, position: number): { code: string; length: number } | undefined {
196
+ if (text[position] !== "\x1B") return undefined;
197
+ const next = text[position + 1];
198
+ if (next === "[") {
199
+ let end = position + 2;
200
+ while (end < text.length && !/[\x40-\x7E]/u.test(text[end] ?? "")) end += 1;
201
+ if (end < text.length) return { code: text.slice(position, end + 1), length: end + 1 - position };
202
+ return undefined;
203
+ }
204
+ if (next === "]" || next === "_") {
205
+ let end = position + 2;
206
+ while (end < text.length) {
207
+ if (text[end] === "\x07") return { code: text.slice(position, end + 1), length: end + 1 - position };
208
+ if (text[end] === "\x1B" && text[end + 1] === "\\") {
209
+ return { code: text.slice(position, end + 2), length: end + 2 - position };
210
+ }
211
+ end += 1;
212
+ }
213
+ return undefined;
214
+ }
215
+ if (next !== undefined) return { code: text.slice(position, position + 2), length: 2 };
216
+ return undefined;
217
+ }
218
+
219
+ export function highlightSlashCommands(
220
+ line: string,
221
+ isValidCommand: (name: string) => boolean,
222
+ styleCommand: (token: string) => string,
223
+ ): string {
224
+ const chunks: RenderChunk[] = [];
225
+ let plain = "";
226
+ let index = 0;
227
+ while (index < line.length) {
228
+ const ansi = extractTerminalSequence(line, index);
229
+ if (ansi) {
230
+ chunks.push({ text: ansi.code, plainStart: plain.length, isAnsi: true });
231
+ index += ansi.length;
232
+ continue;
233
+ }
234
+
235
+ const start = index;
236
+ const plainStart = plain.length;
237
+ while (index < line.length && !extractTerminalSequence(line, index)) {
238
+ plain += line[index] ?? "";
239
+ index += 1;
240
+ }
241
+ chunks.push({
242
+ text: line.slice(start, index),
243
+ plainStart,
244
+ isAnsi: false,
245
+ });
246
+ }
247
+
248
+ const tokens = findSlashCommandTokens(plain).filter((token) => isValidCommand(token.name));
249
+ if (tokens.length === 0) return line;
250
+
251
+ return chunks.map((chunk) => {
252
+ if (chunk.isAnsi) return chunk.text;
253
+ let output = "";
254
+ let offset = 0;
255
+ while (offset < chunk.text.length) {
256
+ const plainIndex = chunk.plainStart + offset;
257
+ const token = tokens.find(({ start, end }) => plainIndex >= start && plainIndex < end);
258
+ if (!token) {
259
+ const nextTokenStart = tokens.find(({ start }) => start > plainIndex)?.start;
260
+ const end = nextTokenStart === undefined
261
+ ? chunk.text.length
262
+ : Math.min(chunk.text.length, nextTokenStart - chunk.plainStart);
263
+ output += chunk.text.slice(offset, end);
264
+ offset = end;
265
+ continue;
266
+ }
267
+
268
+ const tokenEnd = Math.min(chunk.text.length, token.end - chunk.plainStart);
269
+ output += styleCommand(chunk.text.slice(offset, tokenEnd));
270
+ offset = tokenEnd;
271
+ }
272
+ return output;
273
+ }).join("");
274
+ }
275
+
185
276
  class PiCodeEditor extends CustomEditor {
186
277
  private readonly appKeybindings: KeybindingsManager;
187
278
  private readonly runtimeTheme: Theme;
188
279
  private readonly suggestion: string;
280
+ private readonly commandResolver: SlashCommandResolver;
189
281
 
190
282
  constructor(
191
283
  tui: TUI,
@@ -193,11 +285,13 @@ class PiCodeEditor extends CustomEditor {
193
285
  appKeybindings: KeybindingsManager,
194
286
  runtimeTheme: Theme,
195
287
  suggestion: string,
288
+ commandResolver: SlashCommandResolver,
196
289
  ) {
197
290
  super(tui, theme, appKeybindings);
198
291
  this.appKeybindings = appKeybindings;
199
292
  this.runtimeTheme = runtimeTheme;
200
293
  this.suggestion = suggestion;
294
+ this.commandResolver = commandResolver;
201
295
  }
202
296
 
203
297
  override handleInput(data: string): void {
@@ -248,6 +342,13 @@ class PiCodeEditor extends CustomEditor {
248
342
  const cursorMarker = this.focused ? CURSOR_MARKER : "";
249
343
  content = `${cursorMarker}\x1B[7m${dim(first)}\x1B[27m${dim(rest)}`;
250
344
  }
345
+ if (this.getText() !== "") {
346
+ content = highlightSlashCommands(
347
+ content,
348
+ (name) => this.commandResolver.isValidCommand(name),
349
+ (token) => this.runtimeTheme.fg("mdLink", token),
350
+ );
351
+ }
251
352
  rendered.push(`${prefix}${padRight(content, innerWidth)}`);
252
353
  }
253
354
 
@@ -272,7 +373,10 @@ const ACTIVITY_FRAME_INTERVAL_MS = 120;
272
373
 
273
374
  let killerosEditorFactory: ReturnType<ExtensionContext["ui"]["getEditorComponent"]>;
274
375
 
275
- export function registerShellUi(pi: ExtensionAPI): void {
376
+ export function registerShellUi(
377
+ pi: ExtensionAPI,
378
+ commandResolver: SlashCommandResolver = createSlashCommandResolver(pi),
379
+ ): void {
276
380
  let activeHeader: PiStartupHeader | undefined;
277
381
 
278
382
  pi.on("session_start", (_event, ctx) => {
@@ -294,7 +398,7 @@ export function registerShellUi(pi: ExtensionAPI): void {
294
398
  if (!existingEditorFactory || existingEditorFactory === killerosEditorFactory) {
295
399
  const editorSuggestion = nextEditorSuggestion();
296
400
  killerosEditorFactory = (tui, editorTheme, keybindings) =>
297
- new PiCodeEditor(tui, editorTheme, keybindings, ctx.ui.theme, editorSuggestion);
401
+ new PiCodeEditor(tui, editorTheme, keybindings, ctx.ui.theme, editorSuggestion, commandResolver);
298
402
  ctx.ui.setEditorComponent(killerosEditorFactory);
299
403
  }
300
404
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "killeros",
3
- "version": "2.0.8",
3
+ "version": "2.0.9",
4
4
  "description": "TUI, goals, and workflow automation for the Pi coding agent",
5
5
  "type": "module",
6
6
  "keywords": [