@selesai/code 0.7.0 → 0.8.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +4 -2
  3. package/dist/extensions/agent-browser.test.ts +195 -0
  4. package/dist/extensions/agent-browser.ts +158 -0
  5. package/dist/extensions/inline-skills.test.ts +14 -0
  6. package/dist/extensions/inline-skills.ts +14 -7
  7. package/dist/extensions/package.json +2 -1
  8. package/dist/extensions/pi-powerline-footer/.github/workflows/test.yml +27 -0
  9. package/dist/extensions/pi-powerline-footer/CHANGELOG.md +153 -0
  10. package/dist/extensions/pi-powerline-footer/CONTRIBUTING.md +16 -0
  11. package/dist/extensions/pi-powerline-footer/README.md +192 -44
  12. package/dist/extensions/pi-powerline-footer/bash-mode/completion.ts +19 -4
  13. package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +80 -25
  14. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +81 -29
  15. package/dist/extensions/pi-powerline-footer/bash-mode/types.ts +1 -1
  16. package/dist/extensions/pi-powerline-footer/cd-command.ts +190 -0
  17. package/dist/extensions/pi-powerline-footer/colors.ts +2 -1
  18. package/dist/extensions/pi-powerline-footer/context-usage.ts +121 -6
  19. package/dist/extensions/pi-powerline-footer/currency-rates.ts +142 -0
  20. package/dist/extensions/pi-powerline-footer/editor-composition.ts +32 -0
  21. package/dist/extensions/pi-powerline-footer/git-status.ts +108 -6
  22. package/dist/extensions/pi-powerline-footer/icons.ts +11 -2
  23. package/dist/extensions/pi-powerline-footer/index.ts +1056 -324
  24. package/dist/extensions/pi-powerline-footer/lifecycle.ts +10 -0
  25. package/dist/extensions/pi-powerline-footer/package.json +10 -6
  26. package/dist/extensions/pi-powerline-footer/paths.ts +22 -0
  27. package/dist/extensions/pi-powerline-footer/powerline-config.ts +297 -15
  28. package/dist/extensions/pi-powerline-footer/presets.ts +6 -25
  29. package/dist/extensions/pi-powerline-footer/queue/store.ts +361 -0
  30. package/dist/extensions/pi-powerline-footer/queue/types.ts +54 -0
  31. package/dist/extensions/pi-powerline-footer/render-scheduler.ts +12 -7
  32. package/dist/extensions/pi-powerline-footer/segments.ts +114 -107
  33. package/dist/extensions/pi-powerline-footer/session-usage.ts +1 -1
  34. package/dist/extensions/pi-powerline-footer/shortcuts.ts +17 -3
  35. package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +189 -17
  36. package/dist/extensions/pi-powerline-footer/tests/cd-command.test.ts +144 -0
  37. package/dist/extensions/pi-powerline-footer/tests/context-usage.test.ts +120 -2
  38. package/dist/extensions/pi-powerline-footer/tests/custom-items.test.ts +245 -22
  39. package/dist/extensions/pi-powerline-footer/tests/editor-composition.test.ts +48 -0
  40. package/dist/extensions/pi-powerline-footer/tests/editor-responsiveness.test.ts +34 -0
  41. package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +39 -0
  42. package/dist/extensions/pi-powerline-footer/tests/git-status.test.ts +92 -0
  43. package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +4 -1
  44. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +33 -197
  45. package/dist/extensions/pi-powerline-footer/tests/mode-aware-trigger.test.ts +79 -0
  46. package/dist/extensions/pi-powerline-footer/tests/paths.test.ts +63 -0
  47. package/dist/extensions/pi-powerline-footer/tests/queue-store.test.ts +195 -0
  48. package/dist/extensions/pi-powerline-footer/tests/quit-cursor.test.ts +14 -0
  49. package/dist/extensions/pi-powerline-footer/tests/remaining-regressions.test.ts +167 -0
  50. package/dist/extensions/pi-powerline-footer/tests/stash-shortcut.test.ts +23 -18
  51. package/dist/extensions/pi-powerline-footer/tests/thinking-segment.test.ts +16 -1
  52. package/dist/extensions/pi-powerline-footer/tests/token-stats.test.ts +249 -0
  53. package/dist/extensions/pi-powerline-footer/tests/usage-display.test.ts +237 -0
  54. package/dist/extensions/pi-powerline-footer/tests/welcome.test.ts +199 -0
  55. package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +191 -100
  56. package/dist/extensions/pi-powerline-footer/theme.ts +32 -18
  57. package/dist/extensions/pi-powerline-footer/token-stats.ts +304 -0
  58. package/dist/extensions/pi-powerline-footer/tsconfig.json +13 -0
  59. package/dist/extensions/pi-powerline-footer/types.ts +74 -29
  60. package/dist/extensions/pi-powerline-footer/welcome-dismiss.ts +34 -0
  61. package/dist/extensions/pi-powerline-footer/welcome.ts +65 -10
  62. package/dist/extensions/pi-powerline-footer/working-vibes.ts +44 -11
  63. package/dist/extensions/pi-subagents/package-lock.json +2 -2
  64. package/dist/extensions/pi-subagents/package.json +1 -1
  65. package/dist/extensions/pi-subagents/src/slash/inline-subagents.ts +57 -24
  66. package/dist/extensions/pi-subagents/test/unit/inline-subagents.test.ts +82 -7
  67. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +1 -1
  68. package/dist/extensions/ponytail/index.js +1 -31
  69. package/dist/extensions/ponytail/test/extension.test.js +5 -22
  70. package/dist/extensions/rtk.test.ts +27 -12
  71. package/dist/extensions/rtk.ts +41 -35
  72. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  All notable changes to `@selesai/code` will be documented in this file.
4
4
 
5
+ ## [0.8.0] - 2026-08-11
6
+
7
+ ### Added
8
+ - **Powerline footer upgraded through upstream 0.12.3.** The bundled workspace now has a file-backed queue/inbox (`# <idea>`, `/idea`, `/ideas`, and `/queue`), compaction-safe prompt delivery, session-aware `/cd`, configurable layout/placement/separators, currency and subagent cost display, git-host icons, and faster cached token statistics.
9
+ - **Inline agent mentions work wherever they are intentional.** A known `#agent-name` can invoke an agent at the start, middle, or end of a message; ordinary mid-message hashes such as `issue #42` still pass through.
10
+ - **Agent Browser setup is bundled.** Selesai can guide and, after confirmation, provision the external browser-automation CLI without blocking startup.
11
+
12
+ ### Fixed
13
+ - **Reliable `#`/`$` inline autocomplete (also fixes WSL).** The `#` agent picker no longer runs full agent discovery (builtin/user/project/package dirs plus node_modules walks) synchronously on every keystroke — results are cached per cwd with a short TTL. This matters especially under WSL: running from a Linux-side directory gives the app a `\\wsl.localhost\...` UNC cwd, where the discovery walk costs ~1s per keystroke and the editor's serialized request pipeline then drops/delays `#` (and queued `$`/`@`) suggestions while typing. A failed discovery or `getCommands` error now also falls back to the base provider instead of rejecting the editor's shared autocomplete request chain, which previously permanently disabled `#`, `$`, `@`, and Tab completion for the rest of the session.
14
+ - **No startup-blocking tool downloads.** The `rtk` extension previously ran `ensureTool("rtk")` inside its extension factory, so a missing/mismatched rtk binary triggered a GitHub download (with 120s timeouts) that blocked the entire startup — repeated every launch while the managed binary failed to install. Provisioning now runs after startup completes; bash commands issued before the hook is ready pass through un-rewritten. `fd`/`rg` provisioning at interactive init (pre-existing) remains the only awaited download.
15
+
5
16
  ## [0.7.0] - 2026-08-07
6
17
 
7
18
  ### Added
package/README.md CHANGED
@@ -90,10 +90,11 @@ The bundled `pi-powerline-footer` improves the interactive display with:
90
90
 
91
91
  - a powerline status bar for model, thinking level, path, git state, context, tokens, cost, cache, and session data
92
92
  - a fixed-editor layout that keeps the editor and status area visible while chat scrolls
93
+ - a file-backed queue/inbox for deferred ideas and compaction-safe prompts (`# <idea>`, `/idea`, `/ideas`, and `/queue`), plus session-aware `/cd`
93
94
  - a recurring `/guide` tour with compact and remembered-dismissal modes, covering workflows, delegation/research prompts, skills, handoff, and recovery
94
95
  - persistent bash mode with shell state, transcript output, history, and ghost suggestions
95
96
  - editor stash/restore, prompt history, clipboard shortcuts, and message-jump shortcuts
96
- - themed working-vibe messages, Nerd Font detection, and configurable presets
97
+ - themed working-vibe messages, Nerd Font detection, configurable presets, layout, placement, and cost display
97
98
 
98
99
  ### Coordination between sessions
99
100
 
@@ -126,13 +127,14 @@ Skills are shipped with Selesai and loaded at boot. They include:
126
127
 
127
128
  - `ponytail` for minimal implementation mode and review helpers
128
129
  - `grill-me` for requirement and design stress-testing
130
+ - `agent-browser` for browser-automation setup and CLI workflows
129
131
  - `planger` and `implanger` for decomposed planning and execution
130
132
  - `handoff` and `handoff-text` for session continuity
131
133
  - `improve-codebase` for architecture and maintainability reviews
132
134
  - `ponytail-review`, `ponytail-audit`, `ponytail-gain`, `ponytail-debt`, and `ponytail-help`
133
135
  - `workflow-creation` for building durable workflow modes
134
136
 
135
- Invoke one with `/skill:<name>`.
137
+ Invoke one inline with `$skill-name` (for example, `$grill-me`).
136
138
 
137
139
  ## Why the extensions are bundled
138
140
 
@@ -0,0 +1,195 @@
1
+ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
2
+ import { existsSync, mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import agentBrowserSetupExtension, {
6
+ DECLINED_MARKER_NAME,
7
+ ensureAgentBrowser,
8
+ getDeclinedMarkerPath,
9
+ INSTALL_COMMAND,
10
+ isAgentBrowserInstalled,
11
+ SKIP_ENV,
12
+ } from "./agent-browser.ts";
13
+
14
+ type ExecResult = { code: number; stdout: string; stderr: string; killed: boolean };
15
+
16
+ function makePi(execImpl?: (command: string, args: string[], opts?: { timeout?: number }) => Promise<ExecResult>) {
17
+ const handlers = new Map<string, Function[]>();
18
+ const commands = new Map<string, any>();
19
+ const exec = vi.fn(
20
+ execImpl ?? (async () => ({ code: 0, stdout: "", stderr: "", killed: false }) as ExecResult),
21
+ );
22
+ const pi = {
23
+ exec,
24
+ on: (evt: string, h: Function) => {
25
+ handlers.set(evt, [...(handlers.get(evt) ?? []), h]);
26
+ },
27
+ registerCommand: (name: string, options: any) => {
28
+ commands.set(name, options);
29
+ },
30
+ };
31
+ return { pi: pi as any, handlers, commands, exec };
32
+ }
33
+
34
+ function makeCtx(overrides?: Partial<{ hasUI: boolean; confirmResult: boolean }>) {
35
+ const ui = {
36
+ confirm: vi.fn(async () => overrides?.confirmResult ?? true),
37
+ notify: vi.fn(),
38
+ };
39
+ return { hasUI: overrides?.hasUI ?? true, ui } as any;
40
+ }
41
+
42
+ function getSessionStartHandler(handlers: Map<string, Function[]>): Function {
43
+ const handler = handlers.get("session_start")?.[0];
44
+ if (!handler) throw new Error("no session_start handler registered");
45
+ return handler;
46
+ }
47
+
48
+ const ok: ExecResult = { code: 0, stdout: "", stderr: "", killed: false };
49
+ const fail: ExecResult = { code: 1, stdout: "", stderr: "command not found", killed: false };
50
+
51
+ let tmpDir: string;
52
+
53
+ beforeAll(() => {
54
+ tmpDir = mkdtempSync(join(tmpdir(), "agent-browser-test-"));
55
+ process.env.SELESAI_CODING_AGENT_DIR = tmpDir;
56
+ process.env.PI_CODING_AGENT_DIR = tmpDir;
57
+ });
58
+
59
+ afterEach(() => {
60
+ vi.restoreAllMocks();
61
+ // The decline marker persists in the shared temp agent dir; remove it so
62
+ // later tests (no-UI, npm-failure) don't short-circuit on isDeclined().
63
+ rmSync(getDeclinedMarkerPath(), { force: true });
64
+ });
65
+
66
+ afterAll(() => {
67
+ delete process.env.SELESAI_CODING_AGENT_DIR;
68
+ delete process.env.PI_CODING_AGENT_DIR;
69
+ rmSync(tmpDir, { recursive: true, force: true });
70
+ });
71
+
72
+ describe("agent-browser setup extension", () => {
73
+ it("registers a session_start hook and the setup-agent-browser command", async () => {
74
+ const { pi, handlers, commands } = makePi();
75
+ await agentBrowserSetupExtension(pi);
76
+ expect(handlers.has("session_start")).toBe(true);
77
+ expect(commands.has("setup-agent-browser")).toBe(true);
78
+ });
79
+
80
+ it("skips probing when the opt-out env var is set or session is not a startup", async () => {
81
+ process.env[SKIP_ENV] = "1";
82
+ try {
83
+ const { pi, handlers, exec } = makePi();
84
+ await agentBrowserSetupExtension(pi);
85
+ await getSessionStartHandler(handlers)({ type: "session_start", reason: "startup" }, makeCtx());
86
+ expect(exec).not.toHaveBeenCalled();
87
+ } finally {
88
+ delete process.env[SKIP_ENV];
89
+ }
90
+
91
+ const { pi, handlers, exec } = makePi();
92
+ await agentBrowserSetupExtension(pi);
93
+ await getSessionStartHandler(handlers)({ type: "session_start", reason: "reload" }, makeCtx());
94
+ expect(exec).not.toHaveBeenCalled();
95
+ });
96
+
97
+ it("does not prompt when the CLI is already installed", async () => {
98
+ const { pi, handlers, exec } = makePi();
99
+ await agentBrowserSetupExtension(pi);
100
+ const ctx = makeCtx();
101
+ await getSessionStartHandler(handlers)({ type: "session_start", reason: "startup" }, ctx);
102
+
103
+ expect(exec).toHaveBeenCalledTimes(1);
104
+ const args = exec.mock.calls[0][1] as string[];
105
+ expect(args.join(" ")).toContain("--version");
106
+ expect(ctx.ui.confirm).not.toHaveBeenCalled();
107
+ expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("already installed"), "info");
108
+ });
109
+
110
+ it("installs via npm when the user confirms, then provisions the browser", async () => {
111
+ const execImpl = vi
112
+ .fn()
113
+ .mockResolvedValueOnce(fail) // probe: missing
114
+ .mockResolvedValueOnce(ok) // npm i -g agent-browser
115
+ .mockResolvedValueOnce(ok) // probe: now present
116
+ .mockResolvedValueOnce(ok); // agent-browser install
117
+ const { pi, handlers, exec } = makePi(execImpl as any);
118
+ await agentBrowserSetupExtension(pi);
119
+ const ctx = makeCtx();
120
+ await getSessionStartHandler(handlers)({ type: "session_start", reason: "startup" }, ctx);
121
+
122
+ expect(ctx.ui.confirm).toHaveBeenCalledWith(expect.stringContaining("Install agent-browser?"), expect.any(String));
123
+ expect(exec).toHaveBeenCalledTimes(4);
124
+ const calls = exec.mock.calls.map(([command, args]) => [command, ...(args as string[])].join(" "));
125
+ expect(calls[0]).toContain("--version");
126
+ expect(calls[1]).toContain("npm");
127
+ expect(calls[1]).toContain("agent-browser");
128
+ expect(calls[2]).toContain("--version");
129
+ expect(calls[3]).toBe(`${process.platform === "win32" ? "cmd /c " : ""}agent-browser install`);
130
+ expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("browser provisioned"), "info");
131
+ });
132
+
133
+ it("records a decline marker and notifies when the user says no", async () => {
134
+ const execImpl = vi.fn().mockResolvedValue(fail);
135
+ const { pi, handlers, exec } = makePi(execImpl as any);
136
+ await agentBrowserSetupExtension(pi);
137
+ const ctx = makeCtx({ confirmResult: false });
138
+ await getSessionStartHandler(handlers)({ type: "session_start", reason: "startup" }, ctx);
139
+
140
+ expect(exec).toHaveBeenCalledTimes(1); // probe only
141
+ expect(existsSync(getDeclinedMarkerPath())).toBe(true);
142
+ expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("Skipped"), "info");
143
+ });
144
+
145
+ it("warns with install instructions when there is no UI", async () => {
146
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
147
+ const execImpl = vi.fn().mockResolvedValue(fail);
148
+ const { pi, handlers } = makePi(execImpl as any);
149
+ await agentBrowserSetupExtension(pi);
150
+ const ctx = makeCtx({ hasUI: false });
151
+ await getSessionStartHandler(handlers)({ type: "session_start", reason: "startup" }, ctx);
152
+
153
+ expect(ctx.ui.confirm).not.toHaveBeenCalled();
154
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining(INSTALL_COMMAND));
155
+ });
156
+
157
+ it("reports a failed npm install and skips browser provisioning", async () => {
158
+ const execImpl = vi
159
+ .fn()
160
+ .mockResolvedValueOnce(fail) // probe
161
+ .mockResolvedValueOnce({ code: 1, stdout: "", stderr: "EACCES", killed: false }); // npm fails
162
+ const { pi, handlers, exec } = makePi(execImpl as any);
163
+ await agentBrowserSetupExtension(pi);
164
+ const ctx = makeCtx();
165
+ await getSessionStartHandler(handlers)({ type: "session_start", reason: "startup" }, ctx);
166
+
167
+ expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("failed"), "error");
168
+ expect(exec).toHaveBeenCalledTimes(2);
169
+ expect(exec.mock.calls.some((c) => (c[1] as string[]).join(" ").endsWith("agent-browser install"))).toBe(false);
170
+ });
171
+
172
+ it("isAgentBrowserInstalled reports the probe result", async () => {
173
+ const { pi, exec } = makePi(async () => ok);
174
+ expect(await isAgentBrowserInstalled(pi)).toBe(true);
175
+ expect(exec).toHaveBeenCalledTimes(1);
176
+
177
+ const { pi: pi2, exec: exec2 } = makePi(async () => fail);
178
+ expect(await isAgentBrowserInstalled(pi2)).toBe(false);
179
+ expect(exec2).toHaveBeenCalledTimes(1);
180
+ });
181
+
182
+ it("setup-agent-browser command re-runs the flow", async () => {
183
+ const { pi, commands } = makePi(async () => ok);
184
+ await agentBrowserSetupExtension(pi);
185
+ const command = commands.get("setup-agent-browser");
186
+ expect(command).toBeDefined();
187
+ const ctx = makeCtx();
188
+ await command.handler("", ctx);
189
+ expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("already installed"), "info");
190
+ });
191
+
192
+ it("declined marker path lives in the agent dir", () => {
193
+ expect(getDeclinedMarkerPath()).toBe(join(tmpDir, DECLINED_MARKER_NAME));
194
+ });
195
+ });
@@ -0,0 +1,158 @@
1
+ /**
2
+ * agent-browser setup extension.
3
+ *
4
+ * The bundled `agent-browser` skill needs the `agent-browser` CLI (npm package)
5
+ * installed (`npm i -g agent-browser && agent-browser install`). This extension
6
+ * makes sure the user installs it when it is missing:
7
+ *
8
+ * - On the first startup where the CLI is not on PATH, ask the user to install
9
+ * it globally via npm and run the documented install steps when they agree.
10
+ * - Declining records a marker in the agent dir so we don't nag on every
11
+ * startup; the `/setup-agent-browser` command re-runs the flow any time.
12
+ * - With no UI (headless/CI), fall back to a console warning with the install
13
+ * command, mirroring the rtk extension's fail-open behavior.
14
+ *
15
+ * Mirrors src/extensions/rtk.ts (probe, opt-out env var, fail-open warning)
16
+ * and src/extensions/web-agent-onboarding.ts (confirm -> act -> marker).
17
+ */
18
+
19
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
20
+ import { dirname, join } from "node:path";
21
+ import { getAgentDir } from "@selesai/code";
22
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, SessionStartEvent } from "@selesai/code";
23
+
24
+ const PROBE_TIMEOUT_MS = 2_000;
25
+ const INSTALL_TIMEOUT_MS = 180_000; // `npm i -g` can be slow on a cold cache
26
+ const BROWSER_INSTALL_TIMEOUT_MS = 600_000; // `agent-browser install` provisions a browser
27
+
28
+ /** Set to "1" to skip the automated setup prompt (the skill still works if installed). */
29
+ export const SKIP_ENV = "SELESAI_SKIP_AGENT_BROWSER_SETUP";
30
+ export const DECLINED_MARKER_NAME = ".agentBrowserSetupDeclined";
31
+
32
+ export const INSTALL_COMMAND = "npm i -g agent-browser && agent-browser install";
33
+
34
+ // `pi.exec` spawns without a shell, so on Windows npm/agent-browser are .cmd
35
+ // shims that must be launched through cmd.exe.
36
+ function cliCommand(args: string[]): { command: string; args: string[] } {
37
+ if (process.platform === "win32") {
38
+ return { command: "cmd", args: ["/c", ...args] };
39
+ }
40
+ return { command: args[0]!, args: args.slice(1) };
41
+ }
42
+
43
+ export function getDeclinedMarkerPath(agentDir: string = getAgentDir()): string {
44
+ return join(agentDir, DECLINED_MARKER_NAME);
45
+ }
46
+
47
+ function isDeclined(agentDir: string = getAgentDir()): boolean {
48
+ return existsSync(getDeclinedMarkerPath(agentDir));
49
+ }
50
+
51
+ function markDeclined(agentDir: string = getAgentDir()): void {
52
+ const markerPath = getDeclinedMarkerPath(agentDir);
53
+ mkdirSync(dirname(markerPath), { recursive: true });
54
+ writeFileSync(markerPath, "1", "utf-8");
55
+ }
56
+
57
+ function notify(ctx: ExtensionContext, message: string, type?: "info" | "warning" | "error"): void {
58
+ if (ctx.hasUI) ctx.ui.notify(message, type);
59
+ }
60
+
61
+ /** True when the `agent-browser` CLI is on PATH and answers `--version`. */
62
+ export async function isAgentBrowserInstalled(pi: ExtensionAPI): Promise<boolean> {
63
+ const { command, args } = cliCommand(["agent-browser", "--version"]);
64
+ try {
65
+ const result = await pi.exec(command, args, { timeout: PROBE_TIMEOUT_MS });
66
+ return !result.killed && result.code === 0;
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+
72
+ /** Run `npm i -g agent-browser`; returns an error message or null on success. */
73
+ async function runNpmInstall(pi: ExtensionAPI): Promise<string | null> {
74
+ const { command, args } = cliCommand(["npm", "i", "-g", "agent-browser"]);
75
+ const result = await pi.exec(command, args, { timeout: INSTALL_TIMEOUT_MS });
76
+ if (result.killed) return "npm install timed out";
77
+ if (result.code !== 0) {
78
+ return (result.stderr || result.stdout || `exit status ${result.code}`).trim() || "npm install failed";
79
+ }
80
+ return null;
81
+ }
82
+
83
+ /** Best-effort `agent-browser install` (provisions the browser). Non-fatal. */
84
+ async function provisionBrowser(pi: ExtensionAPI): Promise<boolean> {
85
+ const { command, args } = cliCommand(["agent-browser", "install"]);
86
+ try {
87
+ const result = await pi.exec(command, args, { timeout: BROWSER_INSTALL_TIMEOUT_MS });
88
+ return !result.killed && result.code === 0;
89
+ } catch {
90
+ return false;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Probe for the CLI; when missing, ask the user to install it via npm and run
96
+ * the documented install steps if they agree. Returns whether the CLI is
97
+ * available afterwards.
98
+ */
99
+ export async function ensureAgentBrowser(pi: ExtensionAPI, ctx: ExtensionContext): Promise<boolean> {
100
+ if (await isAgentBrowserInstalled(pi)) {
101
+ notify(ctx, "agent-browser CLI is already installed.", "info");
102
+ return true;
103
+ }
104
+
105
+ // No dialog UI (headless/CI): warn with the install command, like rtk does.
106
+ if (!ctx.hasUI) {
107
+ console.warn(`[agent-browser] CLI not found; install with: ${INSTALL_COMMAND}`);
108
+ return false;
109
+ }
110
+
111
+ const wantsInstall = await ctx.ui.confirm(
112
+ "Install agent-browser?",
113
+ `The agent-browser skill needs the agent-browser CLI. Install it globally via npm?\n\n${INSTALL_COMMAND}`,
114
+ );
115
+ if (!wantsInstall) {
116
+ markDeclined();
117
+ ctx.ui.notify(`Skipped. Run /setup-agent-browser later or install manually: ${INSTALL_COMMAND}`, "info");
118
+ return false;
119
+ }
120
+
121
+ ctx.ui.notify("Installing agent-browser via npm…", "info");
122
+ const error = await runNpmInstall(pi);
123
+ if (error) {
124
+ ctx.ui.notify(`agent-browser install failed: ${error}`, "error");
125
+ ctx.ui.notify(`Install manually: ${INSTALL_COMMAND}`, "warning");
126
+ return false;
127
+ }
128
+
129
+ if (!(await isAgentBrowserInstalled(pi))) {
130
+ ctx.ui.notify("npm reported success, but agent-browser is still not on PATH.", "warning");
131
+ ctx.ui.notify(`Try running: ${INSTALL_COMMAND}`, "info");
132
+ return false;
133
+ }
134
+
135
+ if (await provisionBrowser(pi)) {
136
+ ctx.ui.notify("agent-browser installed and browser provisioned.", "info");
137
+ } else {
138
+ ctx.ui.notify("agent-browser installed. Run `agent-browser install` to provision the browser.", "info");
139
+ }
140
+ return true;
141
+ }
142
+
143
+ export default function agentBrowserSetupExtension(pi: ExtensionAPI): void {
144
+ pi.on("session_start", async (event: SessionStartEvent, ctx: ExtensionContext) => {
145
+ if (event.reason !== "startup") return;
146
+ if (process.env[SKIP_ENV] === "1") return;
147
+ if (isDeclined()) return;
148
+ await ensureAgentBrowser(pi, ctx);
149
+ });
150
+
151
+ // Manual re-run; also the escape hatch after a decline.
152
+ pi.registerCommand("setup-agent-browser", {
153
+ description: "Install the agent-browser CLI (npm) if missing, or re-check",
154
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
155
+ await ensureAgentBrowser(pi, ctx);
156
+ },
157
+ });
158
+ }
@@ -73,6 +73,20 @@ describe("inline-skills", () => {
73
73
  expect(text.endsWith("Use $research, then $research again")).toBe(true);
74
74
  });
75
75
 
76
+ it("falls back to the base provider instead of rejecting when getCommands throws", async () => {
77
+ const pi = {
78
+ getCommands: vi.fn(() => {
79
+ throw new Error("getCommands failed");
80
+ }),
81
+ on: vi.fn(),
82
+ } as unknown as ExtensionAPI;
83
+ const base = { getSuggestions: vi.fn(async () => null), applyCompletion: vi.fn() } as any;
84
+ const provider = createInlineSkillAutocompleteProvider(pi, base);
85
+
86
+ await expect(provider.getSuggestions(["$res"], 0, 4, {} as any)).resolves.toBeNull();
87
+ expect(base.getSuggestions).toHaveBeenCalled();
88
+ });
89
+
76
90
  it("registers autocomplete and transforms interactive input", () => {
77
91
  const dir = mkdtempSync(join(tmpdir(), "inline-skills-"));
78
92
  tempDirs.push(dir);
@@ -34,13 +34,20 @@ export function createInlineSkillAutocompleteProvider(pi: ExtensionAPI, current:
34
34
  return current.getSuggestions(lines, cursorLine, cursorCol, options);
35
35
  }
36
36
 
37
- const items: AutocompleteItem[] = getSkillCommands(pi)
38
- .filter((command) => getSkillName(command).includes(token.toLowerCase()))
39
- .map((command) => ({
40
- value: `$${getSkillName(command)}`,
41
- label: `$${getSkillName(command)}`,
42
- description: command.description,
43
- }));
37
+ let items: AutocompleteItem[];
38
+ try {
39
+ items = getSkillCommands(pi)
40
+ .filter((command) => getSkillName(command).includes(token.toLowerCase()))
41
+ .map((command) => ({
42
+ value: `$${getSkillName(command)}`,
43
+ label: `$${getSkillName(command)}`,
44
+ description: command.description,
45
+ }));
46
+ } catch {
47
+ // Never let a getCommands failure reject the editor's shared autocomplete
48
+ // request chain (a single rejection permanently disables all autocomplete).
49
+ return current.getSuggestions(lines, cursorLine, cursorCol, options);
50
+ }
44
51
 
45
52
  return items.length > 0 ? { prefix: `$${token}`, items } : current.getSuggestions(lines, cursorLine, cursorCol, options);
46
53
  },
@@ -5,7 +5,8 @@
5
5
  "description": "Built-in extensions shipped with @selesai/code, loaded via additionalExtensionPaths at boot.",
6
6
  "pi": {
7
7
  "extensions": [
8
- "./copy-turn.ts",
8
+ "./agent-browser.ts",
9
+ "./copy-turn.ts",
9
10
  "./context-compaction-reminder.ts",
10
11
  "./pi-intercom/index.ts",
11
12
  "./ponytail/index.js",
@@ -0,0 +1,27 @@
1
+ name: Test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ name: test (${{ matrix.os }})
11
+ runs-on: ${{ matrix.os }}
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ os: [ubuntu-latest, windows-latest]
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: actions/setup-node@v4
19
+ with:
20
+ node-version: 24
21
+ cache: npm
22
+ - name: Install devDependencies
23
+ run: npm ci --ignore-scripts
24
+ - name: Typecheck
25
+ run: npm run typecheck
26
+ - name: Unit tests
27
+ run: npm test