@oh-my-pi/pi-coding-agent 8.12.1 → 8.12.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [8.12.2] - 2026-01-28
6
+
7
+ ### Changed
8
+ - Replaced ripgrep-based file listing with fs.glob for project scans and find/read tooling
9
+
5
10
  ## [8.11.14] - 2026-01-28
6
11
 
7
12
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "8.12.1",
3
+ "version": "8.12.4",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -58,10 +58,6 @@
58
58
  "types": "./src/internal-urls/*.ts",
59
59
  "import": "./src/internal-urls/*.ts"
60
60
  },
61
- "./vendor/photon/*": {
62
- "types": "./src/vendor/photon/*",
63
- "import": "./src/vendor/photon/*"
64
- },
65
61
  "./*": {
66
62
  "types": "./src/*.ts",
67
63
  "import": "./src/*.ts"
@@ -83,11 +79,12 @@
83
79
  "test": "bun test"
84
80
  },
85
81
  "dependencies": {
86
- "@oh-my-pi/omp-stats": "8.12.1",
87
- "@oh-my-pi/pi-agent-core": "8.12.1",
88
- "@oh-my-pi/pi-ai": "8.12.1",
89
- "@oh-my-pi/pi-tui": "8.12.1",
90
- "@oh-my-pi/pi-utils": "8.12.1",
82
+ "@oh-my-pi/omp-stats": "8.12.4",
83
+ "@oh-my-pi/pi-agent-core": "8.12.4",
84
+ "@oh-my-pi/pi-ai": "8.12.4",
85
+ "@oh-my-pi/pi-natives": "8.12.4",
86
+ "@oh-my-pi/pi-tui": "8.12.4",
87
+ "@oh-my-pi/pi-utils": "8.12.4",
91
88
  "@openai/agents": "^0.4.4",
92
89
  "@sinclair/typebox": "^0.34.48",
93
90
  "ajv": "^8.17.1",
@@ -72,6 +72,13 @@ export interface NotificationSettings {
72
72
  onComplete?: NotificationMethod; // default: "auto"
73
73
  }
74
74
 
75
+ export interface AskSettings {
76
+ /** Timeout in seconds for ask tool selections (0 or null to disable, default: 30) */
77
+ timeout?: number | null;
78
+ /** Notification method when ask tool is waiting for input (default: "auto") */
79
+ notification?: NotificationMethod;
80
+ }
81
+
75
82
  export interface ExaSettings {
76
83
  enabled?: boolean; // default: true (master toggle for all Exa tools)
77
84
  enableSearch?: boolean; // default: true (search, deep, code, crawl)
@@ -242,6 +249,7 @@ export interface Settings {
242
249
  showHardwareCursor?: boolean; // Show terminal cursor while still positioning it for IME
243
250
  normativeRewrite?: boolean; // default: false (rewrite tool call arguments to normalized format in session history)
244
251
  readLineNumbers?: boolean; // default: false (prepend line numbers to read tool output by default)
252
+ ask?: AskSettings;
245
253
  }
246
254
 
247
255
  export const DEFAULT_BASH_INTERCEPTOR_RULES: BashInterceptorRule[] = [
@@ -307,6 +315,7 @@ const DEFAULT_SETTINGS: Settings = {
307
315
  terminal: { showImages: true },
308
316
  images: { autoResize: true },
309
317
  notifications: { onComplete: "auto" },
318
+ ask: { timeout: 30, notification: "auto" },
310
319
  exa: {
311
320
  enabled: true,
312
321
  enableSearch: true,
@@ -1117,6 +1126,33 @@ export class SettingsManager {
1117
1126
  await this.save();
1118
1127
  }
1119
1128
 
1129
+ /** Get ask tool timeout in milliseconds (0 or null = disabled) */
1130
+ getAskTimeout(): number | null {
1131
+ const timeout = this.settings.ask?.timeout;
1132
+ if (timeout === null || timeout === 0) return null;
1133
+ return (timeout ?? 30) * 1000;
1134
+ }
1135
+
1136
+ async setAskTimeout(seconds: number | null): Promise<void> {
1137
+ if (!this.globalSettings.ask) {
1138
+ this.globalSettings.ask = {};
1139
+ }
1140
+ this.globalSettings.ask.timeout = seconds;
1141
+ await this.save();
1142
+ }
1143
+
1144
+ getAskNotification(): NotificationMethod {
1145
+ return this.settings.ask?.notification ?? "auto";
1146
+ }
1147
+
1148
+ async setAskNotification(method: NotificationMethod): Promise<void> {
1149
+ if (!this.globalSettings.ask) {
1150
+ this.globalSettings.ask = {};
1151
+ }
1152
+ this.globalSettings.ask.notification = method;
1153
+ await this.save();
1154
+ }
1155
+
1120
1156
  getImageAutoResize(): boolean {
1121
1157
  return this.settings.images?.autoResize ?? true;
1122
1158
  }
package/src/config.ts CHANGED
@@ -93,11 +93,6 @@ export function getToolsDir(): string {
93
93
  return path.join(getAgentDir(), "tools");
94
94
  }
95
95
 
96
- /** Get path to managed binaries directory (fd, rg) */
97
- export function getBinDir(): string {
98
- return path.join(getAgentDir(), "bin");
99
- }
100
-
101
96
  /** Get path to slash commands directory */
102
97
  export function getCommandsDir(): string {
103
98
  return path.join(getAgentDir(), "commands");
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Provides unified bash execution for AgentSession.executeBash() and direct calls.
5
5
  */
6
- import { cspawn, Exception, ptree } from "@oh-my-pi/pi-utils";
6
+ import { Exception, ptree } from "@oh-my-pi/pi-utils";
7
7
  import { OutputSink } from "../session/streaming-output";
8
8
  import { getShellConfig } from "../utils/shell";
9
9
  import { getOrCreateSnapshot, getSnapshotSourceCommand } from "../utils/shell-snapshot";
@@ -50,11 +50,12 @@ export async function executeBash(command: string, options?: BashExecutorOptions
50
50
  artifactId: options?.artifactId,
51
51
  });
52
52
 
53
- const child = cspawn([shell, ...args, finalCommand], {
53
+ using child = ptree.spawn([shell, ...args, finalCommand], {
54
54
  cwd: options?.cwd,
55
55
  env: finalEnv,
56
56
  signal: options?.signal,
57
57
  timeout: options?.timeout,
58
+ detached: true,
58
59
  });
59
60
 
60
61
  // Pump streams - errors during abort/timeout are expected
@@ -65,9 +66,8 @@ export async function executeBash(command: string, options?: BashExecutorOptions
65
66
 
66
67
  // Wait for process exit
67
68
  try {
68
- await child.exited;
69
69
  return {
70
- exitCode: child.exitCode ?? 0,
70
+ exitCode: await child.exited,
71
71
  cancelled: false,
72
72
  ...(await sink.dump()),
73
73
  };
package/src/exec/exec.ts CHANGED
@@ -35,22 +35,19 @@ export async function execCommand(
35
35
  cwd: string,
36
36
  options?: ExecOptions,
37
37
  ): Promise<ExecResult> {
38
- const proc = ptree.cspawn([command, ...args], {
38
+ const result = await ptree.exec([command, ...args], {
39
39
  cwd,
40
40
  signal: options?.signal,
41
41
  timeout: options?.timeout,
42
+ allowNonZero: true,
43
+ allowAbort: true,
44
+ stderr: "full",
42
45
  });
43
- // Read streams before awaiting exit to avoid data loss if streams close
44
- const [stdoutText, stderrText] = await Promise.all([proc.stdout.text(), proc.stderr.text()]);
45
- try {
46
- await proc.exited;
47
- } catch {
48
- // ChildProcess rejects on non-zero exit; we handle it below
49
- }
46
+
50
47
  return {
51
- stdout: stdoutText,
52
- stderr: stderrText,
53
- code: proc.exitCode ?? 0,
54
- killed: proc.exitReason instanceof ptree.AbortError,
48
+ stdout: result.stdout,
49
+ stderr: result.stderr,
50
+ code: result.exitCode ?? 0,
51
+ killed: Boolean(result.exitError?.aborted),
55
52
  };
56
53
  }
@@ -47,6 +47,8 @@ export interface ExtensionUIDialogOptions {
47
47
  timeout?: number;
48
48
  /** Initial cursor position for select dialogs (0-indexed) */
49
49
  initialIndex?: number;
50
+ /** Render an outlined list for select dialogs */
51
+ outline?: boolean;
50
52
  }
51
53
 
52
54
  /**
@@ -6,8 +6,6 @@ export async function runDoctorChecks(): Promise<DoctorCheck[]> {
6
6
 
7
7
  // Check external tools
8
8
  const tools = [
9
- { name: "fd", description: "File finder" },
10
- { name: "rg", description: "Ripgrep" },
11
9
  { name: "sd", description: "Find-replace" },
12
10
  { name: "sg", description: "AST-grep" },
13
11
  { name: "git", description: "Version control" },
package/src/ipy/kernel.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { createServer } from "node:net";
2
2
  import * as path from "node:path";
3
- import { logger } from "@oh-my-pi/pi-utils";
4
- import { $, type Subprocess } from "bun";
3
+ import { logger, ptree } from "@oh-my-pi/pi-utils";
4
+ import { $ } from "bun";
5
5
  import { nanoid } from "nanoid";
6
- import { getShellConfig, killProcessTree } from "../utils/shell";
6
+ import { getShellConfig } from "../utils/shell";
7
7
  import { getOrCreateSnapshot } from "../utils/shell-snapshot";
8
8
  import { time } from "../utils/timings";
9
9
  import { htmlToBasicMarkdown } from "../web/scrapers/types";
@@ -452,7 +452,7 @@ export function serializeWebSocketMessage(msg: JupyterMessage): ArrayBuffer {
452
452
  export class PythonKernel {
453
453
  readonly id: string;
454
454
  readonly kernelId: string;
455
- readonly gatewayProcess: Subprocess | null;
455
+ readonly gatewayProcess: ptree.ChildProcess | null;
456
456
  readonly gatewayUrl: string;
457
457
  readonly sessionId: string;
458
458
  readonly username: string;
@@ -469,7 +469,7 @@ export class PythonKernel {
469
469
  private constructor(
470
470
  id: string,
471
471
  kernelId: string,
472
- gatewayProcess: Subprocess | null,
472
+ gatewayProcess: ptree.ChildProcess | null,
473
473
  gatewayUrl: string,
474
474
  sessionId: string,
475
475
  username: string,
@@ -633,14 +633,14 @@ export class PythonKernel {
633
633
  kernelEnv.PYTHONPATH = pythonPathParts;
634
634
  }
635
635
 
636
- let gatewayProcess: Subprocess | null = null;
636
+ let gatewayProcess: ptree.ChildProcess | null = null;
637
637
  let gatewayUrl: string | null = null;
638
638
  let lastError: string | null = null;
639
639
 
640
640
  for (let attempt = 0; attempt < GATEWAY_STARTUP_ATTEMPTS; attempt += 1) {
641
641
  const gatewayPort = await allocatePort();
642
642
  const candidateUrl = `http://127.0.0.1:${gatewayPort}`;
643
- const candidateProcess = Bun.spawn(
643
+ const candidateProcess = ptree.spawn(
644
644
  [
645
645
  runtime.pythonPath,
646
646
  "-m",
@@ -653,10 +653,8 @@ export class PythonKernel {
653
653
  ],
654
654
  {
655
655
  cwd: options.cwd,
656
- stdin: "ignore",
657
- stdout: "pipe",
658
- stderr: "pipe",
659
656
  env: kernelEnv,
657
+ detached: true,
660
658
  },
661
659
  );
662
660
 
@@ -687,7 +685,7 @@ export class PythonKernel {
687
685
 
688
686
  if (gatewayProcess && gatewayUrl) break;
689
687
 
690
- await killProcessTree(candidateProcess.pid);
688
+ candidateProcess.kill();
691
689
  lastError = exited ? "Kernel gateway process exited during startup" : "Kernel gateway failed to start";
692
690
  }
693
691
 
@@ -702,7 +700,7 @@ export class PythonKernel {
702
700
  });
703
701
 
704
702
  if (!createResponse.ok) {
705
- await killProcessTree(gatewayProcess.pid);
703
+ gatewayProcess.kill();
706
704
  throw new Error(`Failed to create kernel: ${await createResponse.text()}`);
707
705
  }
708
706
 
@@ -1118,7 +1116,7 @@ export class PythonKernel {
1118
1116
  await releaseSharedGateway();
1119
1117
  } else if (this.gatewayProcess) {
1120
1118
  try {
1121
- await killProcessTree(this.gatewayProcess.pid);
1119
+ this.gatewayProcess.kill();
1122
1120
  } catch (err: unknown) {
1123
1121
  logger.warn("Failed to terminate gateway process", {
1124
1122
  error: err instanceof Error ? err.message : String(err),
package/src/migrations.ts CHANGED
@@ -5,7 +5,7 @@ import * as fs from "node:fs";
5
5
  import * as path from "node:path";
6
6
  import { isEnoent, logger } from "@oh-my-pi/pi-utils";
7
7
  import chalk from "chalk";
8
- import { getAgentDbPath, getAgentDir, getBinDir } from "./config";
8
+ import { getAgentDbPath, getAgentDir } from "./config";
9
9
  import { AgentStorage } from "./session/agent-storage";
10
10
  import type { AuthCredential } from "./session/auth-storage";
11
11
 
@@ -144,50 +144,6 @@ export async function migrateSessionsFromAgentRoot(): Promise<void> {
144
144
  }
145
145
  }
146
146
 
147
- /**
148
- * Move fd/rg binaries from tools/ to bin/ if they exist.
149
- */
150
- async function migrateToolsToBin(): Promise<void> {
151
- const agentDir = getAgentDir();
152
- const toolsDir = path.join(agentDir, "tools");
153
- const binDir = getBinDir();
154
-
155
- if (!fs.existsSync(toolsDir)) return;
156
-
157
- const binaries = ["fd", "rg", "fd.exe", "rg.exe"];
158
- let movedAny = false;
159
-
160
- for (const bin of binaries) {
161
- const oldPath = path.join(toolsDir, bin);
162
- const newPath = path.join(binDir, bin);
163
- if (!fs.existsSync(oldPath)) continue;
164
-
165
- if (!fs.existsSync(binDir)) {
166
- await fs.promises.mkdir(binDir, { recursive: true });
167
- }
168
-
169
- if (!fs.existsSync(newPath)) {
170
- try {
171
- await fs.promises.rename(oldPath, newPath);
172
- movedAny = true;
173
- } catch (error) {
174
- logger.warn("Failed to migrate binary", { from: oldPath, to: newPath, error: String(error) });
175
- }
176
- } else {
177
- // Target exists, just delete the old one
178
- try {
179
- await fs.promises.rm(oldPath, { force: true });
180
- } catch {
181
- // Ignore
182
- }
183
- }
184
- }
185
-
186
- if (movedAny) {
187
- console.log(chalk.green(`Migrated managed binaries tools/ → bin/`));
188
- }
189
- }
190
-
191
147
  /**
192
148
  * Run all migrations. Called once on startup.
193
149
  *
@@ -201,7 +157,6 @@ export async function runMigrations(_cwd: string): Promise<{
201
157
  // Then: run data migrations
202
158
  const migratedAuthProviders = await migrateAuthToAgentDb();
203
159
  await migrateSessionsFromAgentRoot();
204
- await migrateToolsToBin();
205
160
 
206
161
  return { migratedAuthProviders, deprecationWarnings: [] };
207
162
  }
@@ -2,7 +2,7 @@
2
2
  * Generic selector component for hooks.
3
3
  * Displays a list of string options with keyboard navigation.
4
4
  */
5
- import { Container, matchesKey, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
5
+ import { Container, matchesKey, Spacer, Text, type TUI, visibleWidth } from "@oh-my-pi/pi-tui";
6
6
  import { theme } from "../../modes/theme/theme";
7
7
  import { CountdownTimer } from "./countdown-timer";
8
8
  import { DynamicBorder } from "./dynamic-border";
@@ -11,12 +11,36 @@ export interface HookSelectorOptions {
11
11
  tui?: TUI;
12
12
  timeout?: number;
13
13
  initialIndex?: number;
14
+ outline?: boolean;
15
+ maxVisible?: number;
16
+ }
17
+
18
+ class OutlinedList extends Container {
19
+ private lines: string[] = [];
20
+
21
+ setLines(lines: string[]): void {
22
+ this.lines = lines;
23
+ this.invalidate();
24
+ }
25
+
26
+ render(width: number): string[] {
27
+ const borderColor = (text: string) => theme.fg("border", text);
28
+ const horizontal = borderColor(theme.boxSharp.horizontal.repeat(Math.max(1, width)));
29
+ const innerWidth = Math.max(1, width - 2);
30
+ const content = this.lines.map(line => {
31
+ const pad = Math.max(0, innerWidth - visibleWidth(line));
32
+ return `${borderColor(theme.boxSharp.vertical)}${line}${" ".repeat(pad)}${borderColor(theme.boxSharp.vertical)}`;
33
+ });
34
+ return [horizontal, ...content, horizontal];
35
+ }
14
36
  }
15
37
 
16
38
  export class HookSelectorComponent extends Container {
17
39
  private options: string[];
18
40
  private selectedIndex: number;
19
- private listContainer: Container;
41
+ private maxVisible: number;
42
+ private listContainer: Container | undefined;
43
+ private outlinedList: OutlinedList | undefined;
20
44
  private onSelectCallback: (option: string) => void;
21
45
  private onCancelCallback: () => void;
22
46
  private titleText: Text;
@@ -34,6 +58,7 @@ export class HookSelectorComponent extends Container {
34
58
 
35
59
  this.options = options;
36
60
  this.selectedIndex = Math.min(opts?.initialIndex ?? 0, options.length - 1);
61
+ this.maxVisible = Math.max(3, opts?.maxVisible ?? 12);
37
62
  this.onSelectCallback = onSelect;
38
63
  this.onCancelCallback = onCancel;
39
64
  this.baseTitle = title;
@@ -62,8 +87,13 @@ export class HookSelectorComponent extends Container {
62
87
  );
63
88
  }
64
89
 
65
- this.listContainer = new Container();
66
- this.addChild(this.listContainer);
90
+ if (opts?.outline) {
91
+ this.outlinedList = new OutlinedList();
92
+ this.addChild(this.outlinedList);
93
+ } else {
94
+ this.listContainer = new Container();
95
+ this.addChild(this.listContainer);
96
+ }
67
97
  this.addChild(new Spacer(1));
68
98
  this.addChild(new Text(theme.fg("dim", "up/down navigate enter select esc cancel"), 1, 0));
69
99
  this.addChild(new Spacer(1));
@@ -73,13 +103,31 @@ export class HookSelectorComponent extends Container {
73
103
  }
74
104
 
75
105
  private updateList(): void {
76
- this.listContainer.clear();
77
- for (let i = 0; i < this.options.length; i++) {
106
+ const lines: string[] = [];
107
+ const startIndex = Math.max(
108
+ 0,
109
+ Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.options.length - this.maxVisible),
110
+ );
111
+ const endIndex = Math.min(startIndex + this.maxVisible, this.options.length);
112
+
113
+ for (let i = startIndex; i < endIndex; i++) {
78
114
  const isSelected = i === this.selectedIndex;
79
115
  const text = isSelected
80
116
  ? theme.fg("accent", `${theme.nav.cursor} `) + theme.fg("accent", this.options[i])
81
117
  : ` ${theme.fg("text", this.options[i])}`;
82
- this.listContainer.addChild(new Text(text, 1, 0));
118
+ lines.push(text);
119
+ }
120
+
121
+ if (startIndex > 0 || endIndex < this.options.length) {
122
+ lines.push(theme.fg("dim", ` (${this.selectedIndex + 1}/${this.options.length})`));
123
+ }
124
+ if (this.outlinedList) {
125
+ this.outlinedList.setLines(lines);
126
+ return;
127
+ }
128
+ this.listContainer?.clear();
129
+ for (const line of lines) {
130
+ this.listContainer?.addChild(new Text(line, 1, 0));
83
131
  }
84
132
  }
85
133
 
@@ -189,6 +189,29 @@ export const SETTINGS_DEFS: SettingDef[] = [
189
189
  get: sm => sm.getNotificationOnComplete(),
190
190
  set: (sm, v) => sm.setNotificationOnComplete(v as NotificationMethod),
191
191
  },
192
+ {
193
+ id: "askTimeout",
194
+ tab: "behavior",
195
+ type: "enum",
196
+ label: "Ask tool timeout",
197
+ description: "Auto-select recommended option after timeout (disabled in plan mode)",
198
+ values: ["off", "15", "30", "60", "120"],
199
+ get: sm => {
200
+ const timeout = sm.getAskTimeout();
201
+ return timeout === null ? "off" : String(timeout / 1000);
202
+ },
203
+ set: (sm, v) => sm.setAskTimeout(v === "off" ? null : Number.parseInt(v, 10)),
204
+ },
205
+ {
206
+ id: "askNotification",
207
+ tab: "behavior",
208
+ type: "enum",
209
+ label: "Ask notification",
210
+ description: "Notify when ask tool is waiting for input",
211
+ values: ["auto", "bell", "osc99", "osc9", "off"],
212
+ get: sm => sm.getAskNotification(),
213
+ set: (sm, v) => sm.setAskNotification(v as NotificationMethod),
214
+ },
192
215
  {
193
216
  id: "startupQuiet",
194
217
  tab: "behavior",
@@ -1,4 +1,4 @@
1
- import type { Component, TUI } from "@oh-my-pi/pi-tui";
1
+ import type { Component, OverlayHandle, TUI } from "@oh-my-pi/pi-tui";
2
2
  import { Spacer, Text } from "@oh-my-pi/pi-tui";
3
3
  import { logger } from "@oh-my-pi/pi-utils";
4
4
  import { KeybindingsManager } from "../../config/keybindings";
@@ -18,6 +18,17 @@ import type { InteractiveModeContext } from "../../modes/types";
18
18
  import { setTerminalTitle } from "../../utils/title-generator";
19
19
 
20
20
  export class ExtensionUiController {
21
+ private hookSelectorOverlay: OverlayHandle | undefined;
22
+ private hookInputOverlay: OverlayHandle | undefined;
23
+
24
+ private readonly dialogOverlayOptions = {
25
+ anchor: "bottom-center",
26
+ width: "80%",
27
+ minWidth: 40,
28
+ maxHeight: "70%",
29
+ margin: 1,
30
+ } as const;
31
+
21
32
  constructor(private ctx: InteractiveModeContext) {}
22
33
 
23
34
  /**
@@ -491,6 +502,9 @@ export class ExtensionUiController {
491
502
  dialogOptions?: ExtensionUIDialogOptions,
492
503
  ): Promise<string | undefined> {
493
504
  const { promise, resolve } = Promise.withResolvers<string | undefined>();
505
+ this.hookSelectorOverlay?.hide();
506
+ this.hookSelectorOverlay = undefined;
507
+ const maxVisible = Math.max(4, Math.min(15, this.ctx.ui.terminal.rows - 12));
494
508
  this.ctx.hookSelector = new HookSelectorComponent(
495
509
  title,
496
510
  options,
@@ -502,13 +516,15 @@ export class ExtensionUiController {
502
516
  this.hideHookSelector();
503
517
  resolve(undefined);
504
518
  },
505
- { initialIndex: dialogOptions?.initialIndex, timeout: dialogOptions?.timeout, tui: this.ctx.ui },
519
+ {
520
+ initialIndex: dialogOptions?.initialIndex,
521
+ timeout: dialogOptions?.timeout,
522
+ tui: this.ctx.ui,
523
+ outline: dialogOptions?.outline,
524
+ maxVisible,
525
+ },
506
526
  );
507
-
508
- this.ctx.editorContainer.clear();
509
- this.ctx.editorContainer.addChild(this.ctx.hookSelector);
510
- this.ctx.ui.setFocus(this.ctx.hookSelector);
511
- this.ctx.ui.requestRender();
527
+ this.hookSelectorOverlay = this.ctx.ui.showOverlay(this.ctx.hookSelector, this.dialogOverlayOptions);
512
528
  return promise;
513
529
  }
514
530
 
@@ -516,8 +532,9 @@ export class ExtensionUiController {
516
532
  * Hide the hook selector.
517
533
  */
518
534
  hideHookSelector(): void {
519
- this.ctx.editorContainer.clear();
520
- this.ctx.editorContainer.addChild(this.ctx.editor);
535
+ this.ctx.hookSelector?.dispose();
536
+ this.hookSelectorOverlay?.hide();
537
+ this.hookSelectorOverlay = undefined;
521
538
  this.ctx.hookSelector = undefined;
522
539
  this.ctx.ui.setFocus(this.ctx.editor);
523
540
  this.ctx.ui.requestRender();
@@ -536,6 +553,8 @@ export class ExtensionUiController {
536
553
  */
537
554
  showHookInput(title: string, placeholder?: string): Promise<string | undefined> {
538
555
  const { promise, resolve } = Promise.withResolvers<string | undefined>();
556
+ this.hookInputOverlay?.hide();
557
+ this.hookInputOverlay = undefined;
539
558
  this.ctx.hookInput = new HookInputComponent(
540
559
  title,
541
560
  placeholder,
@@ -548,11 +567,7 @@ export class ExtensionUiController {
548
567
  resolve(undefined);
549
568
  },
550
569
  );
551
-
552
- this.ctx.editorContainer.clear();
553
- this.ctx.editorContainer.addChild(this.ctx.hookInput);
554
- this.ctx.ui.setFocus(this.ctx.hookInput);
555
- this.ctx.ui.requestRender();
570
+ this.hookInputOverlay = this.ctx.ui.showOverlay(this.ctx.hookInput, this.dialogOverlayOptions);
556
571
  return promise;
557
572
  }
558
573
 
@@ -560,8 +575,9 @@ export class ExtensionUiController {
560
575
  * Hide the hook input.
561
576
  */
562
577
  hideHookInput(): void {
563
- this.ctx.editorContainer.clear();
564
- this.ctx.editorContainer.addChild(this.ctx.editor);
578
+ this.ctx.hookInput?.dispose();
579
+ this.hookInputOverlay?.hide();
580
+ this.hookInputOverlay = undefined;
565
581
  this.ctx.hookInput = undefined;
566
582
  this.ctx.ui.setFocus(this.ctx.editor);
567
583
  this.ctx.ui.requestRender();
@@ -112,7 +112,7 @@ export class RpcClient {
112
112
  args.push(...this.options.args);
113
113
  }
114
114
 
115
- this.process = ptree.cspawn(["bun", cliPath, ...args], {
115
+ this.process = ptree.spawn(["bun", cliPath, ...args], {
116
116
  cwd: this.options.cwd,
117
117
  env: { ...process.env, ...this.options.env },
118
118
  stdin: "pipe",
@@ -140,7 +140,7 @@ export class RpcClient {
140
140
  await Bun.sleep(100);
141
141
 
142
142
  try {
143
- const exitCode = await Promise.race([this.process.exited, Bun.sleep(50).then(() => null)]);
143
+ const exitCode = await Promise.race([this.process.exited, Bun.sleep(500).then(() => null)]);
144
144
  if (exitCode !== null) {
145
145
  throw new Error(
146
146
  `Agent process exited immediately with code ${exitCode}. Stderr: ${this.process.peekStderr()}`,
@@ -154,11 +154,11 @@ export class RpcClient {
154
154
  /**
155
155
  * Stop the RPC agent process.
156
156
  */
157
- async stop(): Promise<void> {
157
+ stop() {
158
158
  if (!this.process) return;
159
159
 
160
160
  this.lineReader?.cancel();
161
- await this.process.killAndWait();
161
+ this.process.kill();
162
162
 
163
163
  this.process = null;
164
164
  this.lineReader = null;
@@ -85,4 +85,4 @@ Read `rule://<name>` when working in their domain.
85
85
  {{/if}}
86
86
 
87
87
  Current date and time: {{dateTime}}
88
- Current working directory: {{cwd}}
88
+ Current working directory: {{cwd}}