killeros 2.0.3 → 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.
@@ -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";
@@ -151,113 +149,6 @@ class PiStartupHeader {
151
149
  }
152
150
 
153
151
  const ANSI_REGEX = /\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
154
- const ANSI_SEQUENCE_AT_START = /^\x1b\[[0-?]*[ -/]*[@-~]/u;
155
- const COMMAND_TOKEN_PATTERN = /(^|[ \t])(\/[A-Za-z0-9:_-]*)/gu;
156
-
157
- function controlSequenceAt(text: string, index: number): string | undefined {
158
- if (text.startsWith(CURSOR_MARKER, index)) return CURSOR_MARKER;
159
- return text.slice(index).match(ANSI_SEQUENCE_AT_START)?.[0];
160
- }
161
-
162
- interface CommandToken {
163
- text: string;
164
- start: number;
165
- end: number;
166
- valid: boolean;
167
- }
168
-
169
- interface EditorVisualLine {
170
- logicalLine: number;
171
- startCol: number;
172
- length: number;
173
- }
174
-
175
- function commandTokens(text: string, normalizedNames: readonly string[]): CommandToken[] {
176
- return [...text.matchAll(COMMAND_TOKEN_PATTERN)].map((match) => {
177
- const token = match[2] ?? "";
178
- const prefix = token.slice(1).toLocaleLowerCase();
179
- const start = (match.index ?? 0) + (match[1]?.length ?? 0);
180
- return {
181
- text: token,
182
- start,
183
- end: start + token.length,
184
- valid: normalizedNames.some((name) => name.startsWith(prefix)),
185
- };
186
- });
187
- }
188
-
189
- function highlightTextRanges(
190
- text: string,
191
- ranges: Array<{ start: number; end: number }>,
192
- color: (value: string) => string,
193
- ): string {
194
- if (ranges.length === 0) return text;
195
-
196
- let output = "";
197
- let buffer = "";
198
- let bufferHighlighted: boolean | undefined;
199
- let plainIndex = 0;
200
- const flush = (): void => {
201
- if (!buffer) return;
202
- output += bufferHighlighted ? color(buffer) : buffer;
203
- buffer = "";
204
- };
205
-
206
- for (let index = 0; index < text.length;) {
207
- const control = controlSequenceAt(text, index);
208
- if (control) {
209
- flush();
210
- output += control;
211
- index += control.length;
212
- continue;
213
- }
214
-
215
- const highlighted = ranges.some((range) => plainIndex >= range.start && plainIndex < range.end);
216
- if (bufferHighlighted !== highlighted) {
217
- flush();
218
- bufferHighlighted = highlighted;
219
- }
220
- buffer += text[index];
221
- plainIndex += 1;
222
- index += 1;
223
- }
224
- flush();
225
- return output;
226
- }
227
-
228
- function highlightEditorLines(
229
- lines: string[],
230
- sourceLines: string[],
231
- visualLines: EditorVisualLine[],
232
- scrollOffset: number,
233
- commandNames: ReadonlySet<string>,
234
- color: (value: string) => string,
235
- ): { lines: string[]; bottomBorderIndex: number } {
236
- let bottomBorderIndex = -1;
237
- for (let index = lines.length - 1; index >= 1; index -= 1) {
238
- if (isBorderLine(lines[index] ?? "")) {
239
- bottomBorderIndex = index;
240
- break;
241
- }
242
- }
243
- if (bottomBorderIndex < 0) bottomBorderIndex = lines.length - 1;
244
-
245
- const normalizedNames = [...commandNames].map((name) => name.toLocaleLowerCase());
246
- for (let index = 1; index < bottomBorderIndex; index += 1) {
247
- const visualLine = visualLines[scrollOffset + index - 1];
248
- if (!visualLine) continue;
249
- const visibleStart = visualLine.startCol;
250
- const visibleEnd = visibleStart + visualLine.length;
251
- const ranges = commandTokens(sourceLines[visualLine.logicalLine] ?? "", normalizedNames)
252
- .filter((token) => token.valid && token.start < visibleEnd && token.end > visibleStart)
253
- .map((token) => ({
254
- start: Math.max(token.start, visibleStart) - visibleStart,
255
- end: Math.min(token.end, visibleEnd) - visibleStart,
256
- }));
257
- lines[index] = highlightTextRanges(lines[index] ?? "", ranges, color);
258
- }
259
- return { lines, bottomBorderIndex };
260
- }
261
152
 
262
153
  function stripAnsi(text: string): string {
263
154
  return text.replace(ANSI_REGEX, "").trim();
@@ -276,19 +167,16 @@ function isScrolledTopBorder(line: string): boolean {
276
167
  class PiCodeEditor extends CustomEditor {
277
168
  private readonly appKeybindings: KeybindingsManager;
278
169
  private readonly runtimeTheme: Theme;
279
- private readonly getCommandNames: () => ReadonlySet<string>;
280
170
 
281
171
  constructor(
282
172
  tui: TUI,
283
173
  theme: EditorTheme,
284
174
  appKeybindings: KeybindingsManager,
285
175
  runtimeTheme: Theme,
286
- getCommandNames: () => ReadonlySet<string>,
287
176
  ) {
288
177
  super(tui, theme, appKeybindings);
289
178
  this.appKeybindings = appKeybindings;
290
179
  this.runtimeTheme = runtimeTheme;
291
- this.getCommandNames = getCommandNames;
292
180
  }
293
181
 
294
182
  override handleInput(data: string): void {
@@ -305,37 +193,19 @@ class PiCodeEditor extends CustomEditor {
305
193
  super.handleInput(data);
306
194
  }
307
195
 
308
- private renderWithCommandHighlighting(
309
- width: number,
310
- color: (value: string) => string,
311
- ): { lines: string[]; bottomBorderIndex: number } {
312
- const lines = super.render(width);
313
- const internals = this as unknown as {
314
- lastWidth: number;
315
- scrollOffset: number;
316
- buildVisualLineMap: (layoutWidth: number) => EditorVisualLine[];
317
- };
318
- return highlightEditorLines(
319
- lines,
320
- this.getLines(),
321
- internals.buildVisualLineMap(internals.lastWidth),
322
- internals.scrollOffset,
323
- this.getCommandNames(),
324
- color,
325
- );
326
- }
327
-
328
196
  override render(width: number): string[] {
329
197
  if (width <= 0) return [];
330
- const colorCommand = (value: string): string => this.runtimeTheme.fg("mdLink", value);
331
- if (width < 4) {
332
- return this.renderWithCommandHighlighting(width, colorCommand)
333
- .lines.map((line) => truncateToWidth(line, width, ""));
334
- }
198
+ if (width < 4) return super.render(width).map((line) => truncateToWidth(line, width, ""));
335
199
  const innerWidth = width - 2;
336
- const highlighted = this.renderWithCommandHighlighting(innerWidth, colorCommand);
337
- const { lines, bottomBorderIndex } = highlighted;
200
+ const lines = super.render(innerWidth);
338
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
+ }
339
209
 
340
210
  const gray = (text: string): string => this.runtimeTheme.fg("dim", text);
341
211
  const framed: string[] = [];
@@ -381,6 +251,8 @@ function formatActivityMessage(word: string, theme: Theme): string {
381
251
  return `${theme.fg("accent", `${word}…`)} ${theme.fg("dim", `(${theme.bold("esc")} to interrupt · thinking)`)}`;
382
252
  }
383
253
 
254
+ let killerosEditorFactory: ReturnType<ExtensionContext["ui"]["getEditorComponent"]>;
255
+
384
256
  export function registerShellUi(pi: ExtensionAPI): void {
385
257
  let activeHeader: PiStartupHeader | undefined;
386
258
  let activityDeck: string[] = [];
@@ -428,8 +300,12 @@ export function registerShellUi(pi: ExtensionAPI): void {
428
300
  intervalMs: ACTIVITY_FRAME_INTERVAL_MS,
429
301
  });
430
302
  ctx.ui.setHiddenThinkingLabel("└ Thinking…");
431
- ctx.ui.setEditorComponent((tui, editorTheme, keybindings) =>
432
- new PiCodeEditor(tui, editorTheme, keybindings, ctx.ui.theme, () => availableCommandNames(pi)));
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
+ }
433
309
  } catch (error) {
434
310
  reportError(ctx, "Killeros UI failed to initialize", error);
435
311
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "killeros",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
4
4
  "description": "TUI, goals, and workflow automation for the Pi coding agent",
5
5
  "type": "module",
6
6
  "keywords": [