@xynogen/pix-search 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,8 +18,9 @@ file picker with its own query input:
18
18
  - **Git recency** — recently modified files (from `git log`) rank higher.
19
19
  - **Depth penalty** — shallower entries win ties.
20
20
 
21
- Picking a result inserts the path into the prompt (quoted automatically when it
22
- contains a space). Keys: `↑`/`↓` move, `⏎` insert, `Esc` cancel (a cancelled
21
+ Picking a result inserts `<path>…</path>` into the prompt; the model receives
22
+ that span verbatim, and `@xynogen/pix-display` renders it as an `@name` chip
23
+ when installed. Keys: `↑`/`↓` move, `⏎` insert, `Esc` cancel (a cancelled
23
24
  pick inserts a literal `@`, so the keystroke is never swallowed).
24
25
 
25
26
  `@` mid-token (e.g. an email `user@host`) is left as a literal `@` — only a
@@ -31,6 +32,6 @@ boundary `@` opens the picker.
31
32
  pi install npm:@xynogen/pix-search
32
33
  ```
33
34
 
34
- Standalone — **not** bundled by `@xynogen/pix-core`. It replaces the input
35
- editor component (via `setEditorComponent`), so run it alone if another
36
- extension also customizes the editor.
35
+ Standalone — **not** bundled by `@xynogen/pix-core`. It wraps whatever editor
36
+ is already installed (via `getEditorComponent`/`setEditorComponent`), so it
37
+ composes with `pix-display` in either load order.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-search",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Pi extension — @ file picker overlay with fuzzy + git recency ranking",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/editor.ts CHANGED
@@ -1,60 +1,51 @@
1
- /**
2
- * Custom editor that opens the file picker when `@` is typed at a token
3
- * boundary (line start or after whitespace), instead of inserting a literal
4
- * `@` and relying on the inline autocomplete dropdown.
5
- *
6
- * The editor stays dumb about UI plumbing: it calls the injected `openPicker`
7
- * callback (wired to `ctx.ui.custom` in the extension) and inserts whatever
8
- * token comes back. A cancelled pick inserts a literal `@` so the keystroke is
9
- * never silently swallowed.
10
- */
1
+ /** Add the @ picker to an editor without replacing its paste state or rendering. */
2
+ import type { CustomEditor } from "@earendil-works/pi-coding-agent";
3
+ import type { TUI } from "@earendil-works/pi-tui";
11
4
 
12
- import { CustomEditor } from "@earendil-works/pi-coding-agent";
13
- import type { EditorTheme, TUI } from "@earendil-works/pi-tui";
14
- import { atToken } from "./rank.ts";
15
-
16
- // The CustomEditor constructor wants pi-coding-agent's KeybindingsManager,
17
- // which is a structural superset of pi-tui's. The factory hands us a value the
18
- // host already typed correctly, so we take it as the constructor's own param
19
- // type rather than re-importing a non-exported type.
20
- type KbManager = ConstructorParameters<typeof CustomEditor>[2];
5
+ /** Picked paths are marked as `<path>…</path>`; pix-display renders the span as a chip. */
6
+ export function pathToken(path: string): string {
7
+ return `<path>${path}</path> `;
8
+ }
21
9
 
22
10
  export type OpenPicker = () => Promise<string | null>;
23
11
 
24
- /** True when a literal `@` at the cursor should start an @-mention: the cursor
25
- * is at line start or the preceding character is whitespace. */
12
+ /** A mention starts at the beginning of a line or after whitespace. */
26
13
  export function atStartsMention(before: string): boolean {
27
14
  return before === "" || /\s$/.test(before);
28
15
  }
29
16
 
30
- export class AtEditor extends CustomEditor {
31
- constructor(
32
- tui: TUI,
33
- theme: EditorTheme,
34
- keybindings: KbManager,
35
- private readonly openPicker: OpenPicker,
36
- ) {
37
- super(tui, theme, keybindings);
38
- }
39
-
40
- handleInput(data: string): void {
41
- if (data === "@") {
42
- const cursor = this.getCursor();
43
- const line = this.getLines()[cursor.line] ?? "";
17
+ export function attachPicker(
18
+ editor: CustomEditor,
19
+ tui: TUI,
20
+ openPicker: OpenPicker,
21
+ onError: (message: string) => void,
22
+ ): void {
23
+ const handleInput = editor.handleInput.bind(editor);
24
+ let inPaste = false;
25
+ const runPicker = async () => {
26
+ try {
27
+ const path = await openPicker();
28
+ editor.insertTextAtCursor(path ? pathToken(path) : "@");
29
+ } catch (error) {
30
+ editor.insertTextAtCursor("@");
31
+ onError(`File picker failed: ${error instanceof Error ? error.message : String(error)}`);
32
+ } finally {
33
+ tui.requestRender();
34
+ }
35
+ };
36
+ editor.handleInput = (data: string) => {
37
+ // Bracketed paste can arrive in separate chunks; a pasted @ is not a shortcut.
38
+ for (const marker of data.matchAll(/\x1b\[(200|201)~/g)) {
39
+ inPaste = marker[1] === "200";
40
+ }
41
+ if (!inPaste && data === "@") {
42
+ const cursor = editor.getCursor();
43
+ const line = editor.getLines()[cursor.line] ?? "";
44
44
  if (atStartsMention(line.slice(0, cursor.col))) {
45
- void this.runPicker();
45
+ void runPicker();
46
46
  return;
47
47
  }
48
48
  }
49
- super.handleInput(data);
50
- }
51
-
52
- private async runPicker(): Promise<void> {
53
- const path = await this.openPicker();
54
- this.insertTextAtCursor(path ? `${atToken(path)} ` : "@");
55
- // The insert resolves outside the TUI input loop (after the overlay
56
- // promise), so nothing schedules a frame — request one or the inserted
57
- // text stays invisible until the next keystroke.
58
- this.tui.requestRender();
59
- }
49
+ handleInput(data);
50
+ };
60
51
  }
package/src/extension.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  /**
2
- * pix-search — replaces the input editor with one that opens a file-picker
2
+ * pix-search — adds input handling that opens a file-picker
3
3
  * overlay when you type `@` at a token boundary. The picker owns its own query
4
4
  * input, so spaces in the query work natively (no `@"…"` quoting). It lists
5
5
  * both files and their containing folders (so you can @-mention a directory),
6
6
  * ranked by fuzzy score + git recency, with a live preview of the selection.
7
7
  */
8
8
 
9
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
- import { AtEditor } from "./editor.ts";
9
+ import { CustomEditor, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
+ import { showTransientMessage } from "@xynogen/pix-pretty/transient-error";
11
+ import { attachPicker } from "./editor.ts";
11
12
  import { FilePicker } from "./picker.ts";
12
13
  import { withDirectories } from "./rank.ts";
13
14
  import { loadRecency, type RecencyMap } from "./recency.ts";
@@ -15,6 +16,7 @@ import { rgFiles } from "./rg.ts";
15
16
 
16
17
  export default function (pi: ExtensionAPI): void {
17
18
  pi.on("session_start", async (_event, ctx) => {
19
+ if (ctx.mode !== "tui") return;
18
20
  const cwd = ctx.cwd;
19
21
 
20
22
  // Warm the git-recency cache in the background; picker reads whatever is
@@ -51,6 +53,23 @@ export default function (pi: ExtensionAPI): void {
51
53
  { overlay: true, overlayOptions: { anchor: "center", width: "60%", maxHeight: "60%" } },
52
54
  );
53
55
 
54
- ctx.ui.setEditorComponent((tui, theme, kb) => new AtEditor(tui, theme, kb, openPicker));
56
+ const previous = ctx.ui.getEditorComponent();
57
+ ctx.ui.setEditorComponent((tui, theme, kb) => {
58
+ const editor = previous?.(tui, theme, kb) ?? new CustomEditor(tui, theme, kb);
59
+ // ponytail: cursor access requires CustomEditor. Keep other editors intact;
60
+ // add a public cursor adapter if a non-CustomEditor integration is needed.
61
+ if (editor instanceof CustomEditor) {
62
+ attachPicker(editor, tui, openPicker, (message) =>
63
+ showTransientMessage(ctx.ui, message, "error"),
64
+ );
65
+ } else {
66
+ showTransientMessage(
67
+ ctx.ui,
68
+ "pix-search: @ picker requires a CustomEditor; existing editor kept.",
69
+ "warning",
70
+ );
71
+ }
72
+ return editor;
73
+ });
55
74
  });
56
75
  }
package/src/picker.ts CHANGED
@@ -233,6 +233,9 @@ export class FilePicker {
233
233
  lines: rows,
234
234
  color: (s) => theme.fg("accent", s),
235
235
  fg: (s) => theme.fg("text", s),
236
+ // Solid modal fill so the terminal doesn't bleed through, matching the
237
+ // gate/confirm/ask overlays. Skipped when the theme lacks bg (mock themes).
238
+ bg: theme.bg ? (s) => theme.bg?.("customMessageBg", s) ?? s : undefined,
236
239
  });
237
240
  this.cachedWidth = width;
238
241
  return this.cachedLines;
package/src/rank.ts CHANGED
@@ -50,11 +50,6 @@ export function rankFiles(
50
50
  return ranked.slice(0, limit);
51
51
  }
52
52
 
53
- /** Quote a path for insertion into the prompt only when it contains a space. */
54
- export function atToken(path: string): string {
55
- return path.includes(" ") ? `@"${path}"` : `@${path}`;
56
- }
57
-
58
53
  /**
59
54
  * Expand a flat file list into files + the directories that contain them, so
60
55
  * the picker can offer folders as selectable targets. Directories keep a