@oh-my-pi/pi-coding-agent 9.1.1 → 9.2.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "9.1.1",
3
+ "version": "9.2.1",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -74,17 +74,17 @@
74
74
  "scripts": {
75
75
  "check": "tsgo -p tsconfig.json",
76
76
  "format-prompts": "bun scripts/format-prompts.ts",
77
- "build:binary": "bun build --compile ./src/cli.ts --outfile dist/omp",
77
+ "build:binary": "cd ../.. && bun build --compile --define OMP_COMPILED=true --root . ./packages/coding-agent/src/cli.ts ./packages/natives/src/grep/worker.ts ./packages/natives/src/html/worker.ts ./packages/natives/src/image/worker.ts --outfile packages/coding-agent/dist/omp",
78
78
  "generate-template": "bun scripts/generate-template.ts",
79
79
  "test": "bun test"
80
80
  },
81
81
  "dependencies": {
82
- "@oh-my-pi/omp-stats": "9.1.1",
83
- "@oh-my-pi/pi-agent-core": "9.1.1",
84
- "@oh-my-pi/pi-ai": "9.1.1",
85
- "@oh-my-pi/pi-natives": "9.1.1",
86
- "@oh-my-pi/pi-tui": "9.1.1",
87
- "@oh-my-pi/pi-utils": "9.1.1",
82
+ "@oh-my-pi/omp-stats": "9.2.1",
83
+ "@oh-my-pi/pi-agent-core": "9.2.1",
84
+ "@oh-my-pi/pi-ai": "9.2.1",
85
+ "@oh-my-pi/pi-natives": "9.2.1",
86
+ "@oh-my-pi/pi-tui": "9.2.1",
87
+ "@oh-my-pi/pi-utils": "9.2.1",
88
88
  "@openai/agents": "^0.4.4",
89
89
  "@sinclair/typebox": "^0.34.48",
90
90
  "ajv": "^8.17.1",
@@ -97,7 +97,6 @@
97
97
  "nanoid": "^5.1.6",
98
98
  "node-html-parser": "^7.0.2",
99
99
  "smol-toml": "^1.6.0",
100
- "strip-ansi": "^7.1.2",
101
100
  "zod": "^4.3.6"
102
101
  },
103
102
  "devDependencies": {
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Grep CLI command handlers.
3
+ *
4
+ * Handles `omp grep` subcommand for testing grep tool on Windows.
5
+ */
6
+ import * as path from "node:path";
7
+ import { grep } from "@oh-my-pi/pi-natives";
8
+ import chalk from "chalk";
9
+ import { APP_NAME } from "../config";
10
+
11
+ export interface GrepCommandArgs {
12
+ pattern: string;
13
+ path: string;
14
+ glob?: string;
15
+ limit: number;
16
+ context: number;
17
+ mode: "content" | "filesWithMatches" | "count";
18
+ }
19
+
20
+ /**
21
+ * Parse grep subcommand arguments.
22
+ * Returns undefined if not a grep command.
23
+ */
24
+ export function parseGrepArgs(args: string[]): GrepCommandArgs | undefined {
25
+ if (args.length === 0 || args[0] !== "grep") {
26
+ return undefined;
27
+ }
28
+
29
+ const result: GrepCommandArgs = {
30
+ pattern: "",
31
+ path: ".",
32
+ limit: 20,
33
+ context: 2,
34
+ mode: "content",
35
+ };
36
+
37
+ const positional: string[] = [];
38
+
39
+ for (let i = 1; i < args.length; i++) {
40
+ const arg = args[i];
41
+ if (arg === "--glob" || arg === "-g") {
42
+ result.glob = args[++i];
43
+ } else if (arg === "--limit" || arg === "-l") {
44
+ result.limit = parseInt(args[++i], 10);
45
+ } else if (arg === "--context" || arg === "-C") {
46
+ result.context = parseInt(args[++i], 10);
47
+ } else if (arg === "--files" || arg === "-f") {
48
+ result.mode = "filesWithMatches";
49
+ } else if (arg === "--count" || arg === "-c") {
50
+ result.mode = "count";
51
+ } else if (!arg.startsWith("-")) {
52
+ positional.push(arg);
53
+ }
54
+ }
55
+
56
+ if (positional.length >= 1) {
57
+ result.pattern = positional[0];
58
+ }
59
+ if (positional.length >= 2) {
60
+ result.path = positional[1];
61
+ }
62
+
63
+ return result;
64
+ }
65
+
66
+ export async function runGrepCommand(cmd: GrepCommandArgs): Promise<void> {
67
+ if (!cmd.pattern) {
68
+ console.error(chalk.red("Error: Pattern is required"));
69
+ process.exit(1);
70
+ }
71
+
72
+ const searchPath = path.resolve(cmd.path);
73
+ console.log(chalk.dim(`Searching in: ${searchPath}`));
74
+ console.log(chalk.dim(`Pattern: ${cmd.pattern}`));
75
+ console.log(chalk.dim(`Mode: ${cmd.mode}, Limit: ${cmd.limit}, Context: ${cmd.context}`));
76
+
77
+ console.log("");
78
+
79
+ try {
80
+ const result = await grep({
81
+ pattern: cmd.pattern,
82
+ path: searchPath,
83
+ glob: cmd.glob,
84
+ mode: cmd.mode,
85
+ maxCount: cmd.limit,
86
+ context: cmd.mode === "content" ? cmd.context : undefined,
87
+ hidden: true,
88
+ });
89
+
90
+ console.log(chalk.green(`Total matches: ${result.totalMatches}`));
91
+ console.log(chalk.green(`Files with matches: ${result.filesWithMatches}`));
92
+ console.log(chalk.green(`Files searched: ${result.filesSearched}`));
93
+ if (result.limitReached) {
94
+ console.log(chalk.yellow(`Limit reached: true`));
95
+ }
96
+ console.log("");
97
+
98
+ for (const match of result.matches) {
99
+ const displayPath = match.path.replace(/\\/g, "/");
100
+
101
+ if (cmd.mode === "content") {
102
+ if (match.contextBefore) {
103
+ for (const ctx of match.contextBefore) {
104
+ console.log(chalk.dim(`${displayPath}-${ctx.lineNumber}- ${ctx.line}`));
105
+ }
106
+ }
107
+ console.log(`${chalk.cyan(displayPath)}:${chalk.yellow(String(match.lineNumber))}: ${match.line}`);
108
+ if (match.contextAfter) {
109
+ for (const ctx of match.contextAfter) {
110
+ console.log(chalk.dim(`${displayPath}-${ctx.lineNumber}- ${ctx.line}`));
111
+ }
112
+ }
113
+ console.log("");
114
+ } else if (cmd.mode === "count") {
115
+ console.log(`${chalk.cyan(displayPath)}: ${match.matchCount ?? 0} matches`);
116
+ } else {
117
+ console.log(chalk.cyan(displayPath));
118
+ }
119
+ }
120
+ } catch (err) {
121
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
122
+ process.exit(1);
123
+ }
124
+ }
125
+
126
+ export function printGrepHelp(): void {
127
+ console.log(`${chalk.bold(`${APP_NAME} grep`)} - Test grep tool
128
+
129
+ ${chalk.bold("Usage:")}
130
+ ${APP_NAME} grep <pattern> [path] [options]
131
+
132
+ ${chalk.bold("Arguments:")}
133
+ pattern Regex pattern to search for
134
+ path Directory or file to search (default: .)
135
+
136
+ ${chalk.bold("Options:")}
137
+ -g, --glob <pattern> Filter files by glob pattern
138
+ -l, --limit <n> Max matches (default: 20)
139
+ -C, --context <n> Context lines (default: 2)
140
+ -f, --files Output file names only
141
+ -c, --count Output match counts per file
142
+ -h, --help Show this help
143
+
144
+ ${chalk.bold("Environment:")}
145
+ OMP_GREP_WORKERS=0 Disable worker pool (use single-threaded mode)
146
+
147
+ ${chalk.bold("Examples:")}
148
+ ${APP_NAME} grep "import" src/
149
+ ${APP_NAME} grep "TODO" . --glob "*.ts"
150
+ ${APP_NAME} grep "function" --files
151
+ `);
152
+ }
@@ -114,6 +114,7 @@ export async function runStatsCommand(cmd: StatsCommandArgs): Promise<void> {
114
114
  Bun.spawn(openCommand === "cmd" ? ["cmd", "/c", "start", url] : [openCommand, url], {
115
115
  stdout: "ignore",
116
116
  stderr: "ignore",
117
+ windowsHide: true,
117
118
  }).unref();
118
119
 
119
120
  console.log("Press Ctrl+C to stop\n");
@@ -37,6 +37,7 @@ export async function commit(cwd: string, message: string): Promise<GitResult> {
37
37
  stdin: Buffer.from(message),
38
38
  stdout: "pipe",
39
39
  stderr: "pipe",
40
+ windowsHide: true,
40
41
  });
41
42
 
42
43
  const [stdout, stderr, exitCode] = await Promise.all([
@@ -90,10 +90,12 @@ export interface ExaSettings {
90
90
 
91
91
  export type WebSearchProviderOption = "auto" | "exa" | "perplexity" | "anthropic";
92
92
  export type ImageProviderOption = "auto" | "gemini" | "openrouter";
93
+ export type KimiApiFormatOption = "openai" | "anthropic";
93
94
 
94
95
  export interface ProviderSettings {
95
96
  webSearch?: WebSearchProviderOption; // default: "auto" (exa > perplexity > anthropic)
96
97
  image?: ImageProviderOption; // default: "auto" (openrouter > gemini)
98
+ kimiApiFormat?: KimiApiFormatOption; // default: "anthropic" (use Anthropic-compatible API for Kimi, more stable)
97
99
  }
98
100
 
99
101
  export interface BashInterceptorRule {
@@ -1349,6 +1351,19 @@ export class SettingsManager {
1349
1351
  await this.save();
1350
1352
  }
1351
1353
 
1354
+ getKimiApiFormat(): KimiApiFormatOption {
1355
+ return this.settings.providers?.kimiApiFormat ?? "anthropic";
1356
+ }
1357
+
1358
+ async setKimiApiFormat(format: KimiApiFormatOption): Promise<void> {
1359
+ if (!this.globalSettings.providers) {
1360
+ this.globalSettings.providers = {};
1361
+ }
1362
+ this.globalSettings.providers.kimiApiFormat = format;
1363
+ this.markModified("providers", "kimiApiFormat");
1364
+ await this.save();
1365
+ }
1366
+
1352
1367
  getBashInterceptorEnabled(): boolean {
1353
1368
  return this.settings.bashInterceptor?.enabled ?? DEFAULT_BASH_INTERCEPTOR_SETTINGS.enabled;
1354
1369
  }
@@ -310,7 +310,7 @@ export class DebugSelectorComponent extends Container {
310
310
  const [cmd, ...args] = openArgs;
311
311
 
312
312
  try {
313
- Bun.spawn([cmd, ...args], { stdout: "ignore", stderr: "ignore" }).unref();
313
+ Bun.spawn([cmd, ...args], { stdout: "ignore", stderr: "ignore", windowsHide: true }).unref();
314
314
  this.ctx.showStatus(`Opened: ${artifactsDir}`);
315
315
  } catch (err) {
316
316
  this.ctx.showError(`Failed to open artifact folder: ${err instanceof Error ? err.message : String(err)}`);
@@ -50,6 +50,7 @@ export async function installPlugin(packageName: string): Promise<InstalledPlugi
50
50
  stdin: "ignore",
51
51
  stdout: "pipe",
52
52
  stderr: "pipe",
53
+ windowsHide: true,
53
54
  });
54
55
 
55
56
  const exitCode = await proc.exited;
@@ -91,6 +92,7 @@ export async function uninstallPlugin(name: string): Promise<void> {
91
92
  stdin: "ignore",
92
93
  stdout: "pipe",
93
94
  stderr: "pipe",
95
+ windowsHide: true,
94
96
  });
95
97
 
96
98
  const exitCode = await proc.exited;
@@ -159,6 +159,7 @@ export class PluginManager {
159
159
  stdin: "ignore",
160
160
  stdout: "pipe",
161
161
  stderr: "pipe",
162
+ windowsHide: true,
162
163
  });
163
164
 
164
165
  const exitCode = await proc.exited;
@@ -239,6 +240,7 @@ export class PluginManager {
239
240
  stdin: "ignore",
240
241
  stdout: "pipe",
241
242
  stderr: "pipe",
243
+ windowsHide: true,
242
244
  });
243
245
 
244
246
  const exitCode = await proc.exited;
@@ -621,6 +623,7 @@ export class PluginManager {
621
623
  stdin: "ignore",
622
624
  stdout: "pipe",
623
625
  stderr: "pipe",
626
+ windowsHide: true,
624
627
  });
625
628
  return (await proc.exited) === 0;
626
629
  } catch {
@@ -420,6 +420,7 @@ async function startGatewayProcess(
420
420
  stdout: "pipe",
421
421
  stderr: "pipe",
422
422
  detached: true,
423
+ windowsHide: true,
423
424
  env: kernelEnv,
424
425
  },
425
426
  );
@@ -86,6 +86,7 @@ async function runBiome(
86
86
  cwd,
87
87
  stdout: "pipe",
88
88
  stderr: "pipe",
89
+ windowsHide: true,
89
90
  });
90
91
 
91
92
  const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
package/src/lsp/index.ts CHANGED
@@ -437,6 +437,7 @@ async function runWorkspaceDiagnostics(
437
437
  cwd,
438
438
  stdout: "pipe",
439
439
  stderr: "pipe",
440
+ windowsHide: true,
440
441
  });
441
442
 
442
443
  const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
package/src/lsp/lspmux.ts CHANGED
@@ -104,6 +104,7 @@ async function checkServerRunning(binaryPath: string): Promise<boolean> {
104
104
  const proc = Bun.spawn([binaryPath, "status"], {
105
105
  stdout: "pipe",
106
106
  stderr: "pipe",
107
+ windowsHide: true,
107
108
  });
108
109
 
109
110
  const exited = await Promise.race([
package/src/lsp/render.ts CHANGED
@@ -306,6 +306,8 @@ function highlightCode(codeText: string, language: string, theme: Theme): string
306
306
  type: theme.getFgAnsi("syntaxType"),
307
307
  operator: theme.getFgAnsi("syntaxOperator"),
308
308
  punctuation: theme.getFgAnsi("syntaxPunctuation"),
309
+ inserted: theme.getFgAnsi("toolDiffAdded"),
310
+ deleted: theme.getFgAnsi("toolDiffRemoved"),
309
311
  };
310
312
  return nativeHighlightCode(codeText, validLang, colors).split("\n");
311
313
  } catch {
package/src/main.ts CHANGED
@@ -14,6 +14,7 @@ import chalk from "chalk";
14
14
  import { type Args, parseArgs, printHelp } from "./cli/args";
15
15
  import { parseConfigArgs, printConfigHelp, runConfigCommand } from "./cli/config-cli";
16
16
  import { processFileArguments } from "./cli/file-processor";
17
+ import { parseGrepArgs, printGrepHelp, runGrepCommand } from "./cli/grep-cli";
17
18
  import { parseJupyterArgs, printJupyterHelp, runJupyterCommand } from "./cli/jupyter-cli";
18
19
  import { listModels } from "./cli/list-models";
19
20
  import { parsePluginArgs, printPluginHelp, runPluginCommand } from "./cli/plugin-cli";
@@ -553,6 +554,17 @@ export async function main(args: string[]) {
553
554
  return;
554
555
  }
555
556
 
557
+ // Handle grep subcommand (for testing grep tool)
558
+ const grepCmd = parseGrepArgs(args);
559
+ if (grepCmd) {
560
+ if (args.includes("--help") || args.includes("-h")) {
561
+ printGrepHelp();
562
+ return;
563
+ }
564
+ await runGrepCommand(grepCmd);
565
+ return;
566
+ }
567
+
556
568
  // Handle commit subcommand
557
569
  const commitCmd = parseCommitArgs(args);
558
570
  if (commitCmd) {
@@ -2,7 +2,6 @@
2
2
  * Component for displaying bash command execution with streaming output.
3
3
  */
4
4
  import { Container, Loader, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
5
- import stripAnsi from "strip-ansi";
6
5
  import { getSymbolTheme, theme } from "../../modes/theme/theme";
7
6
  import type { TruncationMeta } from "../../tools/output-meta";
8
7
  import { formatSize } from "../../tools/truncate";
@@ -188,7 +187,7 @@ export class BashExecutionComponent extends Container {
188
187
  private normalizeOutput(text: string): string {
189
188
  // Strip ANSI codes and normalize line endings
190
189
  // Note: binary data is already sanitized in tui-renderer.ts executeBashCommand
191
- return stripAnsi(text).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
190
+ return Bun.stripANSI(text).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
192
191
  }
193
192
 
194
193
  private setOutput(output: string): void {
@@ -3,7 +3,6 @@
3
3
  * Shares the same kernel session as the agent's Python tool.
4
4
  */
5
5
  import { Container, Loader, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
6
- import stripAnsi from "strip-ansi";
7
6
  import { getSymbolTheme, highlightCode, theme } from "../../modes/theme/theme";
8
7
  import type { TruncationMeta } from "../../tools/output-meta";
9
8
  import { formatSize } from "../../tools/truncate";
@@ -172,7 +171,7 @@ export class PythonExecutionComponent extends Container {
172
171
  }
173
172
 
174
173
  private normalizeOutput(text: string): string {
175
- return stripAnsi(text).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
174
+ return Bun.stripANSI(text).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
176
175
  }
177
176
 
178
177
  private setOutput(output: string): void {
@@ -11,6 +11,7 @@ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
11
11
  import { TERMINAL_INFO } from "@oh-my-pi/pi-tui";
12
12
  import type {
13
13
  ImageProviderOption,
14
+ KimiApiFormatOption,
14
15
  NotificationMethod,
15
16
  PythonKernelMode,
16
17
  PythonToolMode,
@@ -416,6 +417,19 @@ export const SETTINGS_DEFS: SettingDef[] = [
416
417
  { value: "openrouter", label: "OpenRouter", description: "Use OpenRouter (requires OPENROUTER_API_KEY)" },
417
418
  ],
418
419
  },
420
+ {
421
+ id: "kimiApiFormat",
422
+ tab: "tools",
423
+ type: "submenu",
424
+ label: "Kimi API format",
425
+ description: "API format for Kimi Code provider",
426
+ get: sm => sm.getKimiApiFormat(),
427
+ set: (sm, v) => sm.setKimiApiFormat(v as KimiApiFormatOption),
428
+ getOptions: () => [
429
+ { value: "openai", label: "OpenAI", description: "Use OpenAI-compatible API (api.kimi.com)" },
430
+ { value: "anthropic", label: "Anthropic", description: "Use Anthropic-compatible API (api.moonshot.ai)" },
431
+ ],
432
+ },
419
433
 
420
434
  // ═══════════════════════════════════════════════════════════════════════════
421
435
  // Display tab - Visual/UI settings
@@ -2049,6 +2049,8 @@ function getHighlightColors(t: Theme): NativeHighlightColors {
2049
2049
  type: t.getFgAnsi("syntaxType"),
2050
2050
  operator: t.getFgAnsi("syntaxOperator"),
2051
2051
  punctuation: t.getFgAnsi("syntaxPunctuation"),
2052
+ inserted: t.getFgAnsi("toolDiffAdded"),
2053
+ deleted: t.getFgAnsi("toolDiffRemoved"),
2052
2054
  };
2053
2055
  }
2054
2056
  return cachedHighlightColors;
@@ -3,7 +3,8 @@
3
3
  Fast file pattern matching that works with any codebase size.
4
4
 
5
5
  <instruction>
6
- - Supports glob patterns like `**/*.js` or `src/**/*.ts`
6
+ - Pattern includes the search path: `src/**/*.ts`, `lib/*.json`, `**/*.md`
7
+ - Simple patterns like `*.ts` automatically search recursively from cwd
7
8
  - Includes hidden files by default (use `hidden: false` to exclude)
8
9
  - Speculatively perform multiple searches in parallel when potentially useful
9
10
  </instruction>
package/src/sdk.ts CHANGED
@@ -1115,6 +1115,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1115
1115
  followUpMode: settingsManager.getFollowUpMode(),
1116
1116
  interruptMode: settingsManager.getInterruptMode(),
1117
1117
  thinkingBudgets: settingsManager.getThinkingBudgets(),
1118
+ kimiApiFormat: settingsManager.getKimiApiFormat(),
1118
1119
  getToolContext: tc => toolContextStore.getContext(tc),
1119
1120
  getApiKey: async () => {
1120
1121
  const currentModel = agent.state.model;
package/src/tools/find.ts CHANGED
@@ -22,8 +22,7 @@ import { toolResult } from "./tool-result";
22
22
  import { type TruncationResult, truncateHead } from "./truncate";
23
23
 
24
24
  const findSchema = Type.Object({
25
- pattern: Type.String({ description: "Glob pattern, e.g. '*.ts', '**/*.json'" }),
26
- path: Type.Optional(Type.String({ description: "Directory to search (default: cwd)" })),
25
+ pattern: Type.String({ description: "Glob pattern, e.g. '*.ts', 'src/**/*.json', 'lib/*.tsx'" }),
27
26
  hidden: Type.Optional(Type.Boolean({ description: "Include hidden files and directories (default: true)" })),
28
27
  limit: Type.Optional(Type.Number({ description: "Max results (default: 1000)" })),
29
28
  });
@@ -31,6 +30,51 @@ const findSchema = Type.Object({
31
30
  const DEFAULT_LIMIT = 1000;
32
31
  const GLOB_TIMEOUT_MS = 5000;
33
32
 
33
+ /**
34
+ * Parse a pattern to extract the base directory path and glob pattern.
35
+ * Examples:
36
+ * "src/app/**\/*.tsx" → { basePath: "src/app", globPattern: "**\/*.tsx" }
37
+ * "src/app/*.tsx" → { basePath: "src/app", globPattern: "*.tsx" }
38
+ * "*.ts" → { basePath: ".", globPattern: "**\/*.ts" }
39
+ * "**\/*.json" → { basePath: ".", globPattern: "**\/*.json" }
40
+ * "/abs/path/**\/*.ts" → { basePath: "/abs/path", globPattern: "**\/*.ts" }
41
+ */
42
+ function parsePatternPath(pattern: string): { basePath: string; globPattern: string } {
43
+ // Find the first segment containing glob characters
44
+ const segments = pattern.split("/");
45
+ const globChars = ["*", "?", "[", "{"];
46
+
47
+ let firstGlobIndex = -1;
48
+ for (let i = 0; i < segments.length; i++) {
49
+ if (globChars.some(c => segments[i].includes(c))) {
50
+ firstGlobIndex = i;
51
+ break;
52
+ }
53
+ }
54
+
55
+ // No glob characters found - treat as literal path with implicit **/*
56
+ if (firstGlobIndex === -1) {
57
+ // Pattern is a directory path like "src/app" - search recursively in it
58
+ return { basePath: pattern, globPattern: "**/*" };
59
+ }
60
+
61
+ // Glob starts at first segment - no base path
62
+ if (firstGlobIndex === 0) {
63
+ // Simple pattern like "*.ts" needs **/ prefix for recursive search
64
+ const needsRecursive = !pattern.startsWith("**/");
65
+ return {
66
+ basePath: ".",
67
+ globPattern: needsRecursive ? `**/${pattern}` : pattern,
68
+ };
69
+ }
70
+
71
+ // Split at the glob boundary
72
+ const basePath = segments.slice(0, firstGlobIndex).join("/");
73
+ const globPattern = segments.slice(firstGlobIndex).join("/");
74
+
75
+ return { basePath, globPattern };
76
+ }
77
+
34
78
  export interface FindToolDetails {
35
79
  truncation?: TruncationResult;
36
80
  resultLimitReached?: number;
@@ -81,10 +125,19 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
81
125
  _onUpdate?: AgentToolUpdateCallback<FindToolDetails>,
82
126
  _context?: AgentToolContext,
83
127
  ): Promise<AgentToolResult<FindToolDetails>> {
84
- const { pattern, path: searchDir, limit, hidden } = params;
128
+ const { pattern, limit, hidden } = params;
85
129
 
86
130
  return untilAborted(signal, async () => {
87
- const searchPath = resolveToCwd(searchDir || ".", this.session.cwd);
131
+ // Parse pattern to extract base directory and glob pattern
132
+ // e.g., "src/app/**/*.tsx" → basePath: "src/app", globPattern: "**/*.tsx"
133
+ // e.g., "*.ts" → basePath: ".", globPattern: "**/*.ts"
134
+ const normalizedPattern = pattern.trim().replace(/\\/g, "/");
135
+ if (!normalizedPattern) {
136
+ throw new ToolError("Pattern must not be empty");
137
+ }
138
+
139
+ const { basePath, globPattern } = parsePatternPath(normalizedPattern);
140
+ const searchPath = resolveToCwd(basePath, this.session.cwd);
88
141
 
89
142
  if (searchPath === "/") {
90
143
  throw new ToolError("Searching from root directory '/' is not allowed");
@@ -94,10 +147,6 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
94
147
  const relative = path.relative(this.session.cwd, searchPath).replace(/\\/g, "/");
95
148
  return relative.length === 0 ? "." : relative;
96
149
  })();
97
- const normalizedPattern = pattern.trim();
98
- if (!normalizedPattern) {
99
- throw new ToolError("Pattern must not be empty");
100
- }
101
150
 
102
151
  const rawLimit = limit ?? DEFAULT_LIMIT;
103
152
  const effectiveLimit = Number.isFinite(rawLimit) ? Math.floor(rawLimit) : Number.NaN;
@@ -105,7 +154,6 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
105
154
  throw new ToolError("Limit must be a positive number");
106
155
  }
107
156
  const includeHidden = hidden ?? true;
108
- const globPattern = normalizedPattern.replace(/\\/g, "/");
109
157
 
110
158
  // If custom operations provided with glob, use that instead of fd
111
159
  if (this.customOps?.glob) {
@@ -113,7 +161,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
113
161
  throw new ToolError(`Path not found: ${searchPath}`);
114
162
  }
115
163
 
116
- const results = await this.customOps.glob(normalizedPattern, searchPath, {
164
+ const results = await this.customOps.glob(globPattern, searchPath, {
117
165
  ignore: ["**/node_modules/**", "**/.git/**"],
118
166
  limit: effectiveLimit,
119
167
  });
@@ -279,8 +327,6 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
279
327
 
280
328
  interface FindRenderArgs {
281
329
  pattern: string;
282
- path?: string;
283
- sortByMtime?: boolean;
284
330
  limit?: number;
285
331
  }
286
332
 
@@ -290,8 +336,6 @@ export const findToolRenderer = {
290
336
  inline: true,
291
337
  renderCall(args: FindRenderArgs, uiTheme: Theme): Component {
292
338
  const meta: string[] = [];
293
- if (args.path) meta.push(`in ${args.path}`);
294
- if (args.sortByMtime) meta.push("sort:mtime");
295
339
  if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
296
340
 
297
341
  const text = renderStatusLine(
@@ -37,25 +37,33 @@ export async function copyToClipboard(text: string): Promise<void> {
37
37
 
38
38
  try {
39
39
  if (p === "darwin") {
40
- await Bun.spawn(["pbcopy"], { stdin: Buffer.from(text), timeout }).exited;
40
+ await Bun.spawn(["pbcopy"], { stdin: Buffer.from(text), timeout, windowsHide: true }).exited;
41
41
  } else if (p === "win32") {
42
- await Bun.spawn(["clip"], { stdin: Buffer.from(text), timeout }).exited;
42
+ await Bun.spawn(["clip"], { stdin: Buffer.from(text), timeout, windowsHide: true }).exited;
43
43
  } else {
44
44
  const wayland = isWaylandSession();
45
45
  if (wayland) {
46
46
  const wlCopyPath = Bun.which("wl-copy");
47
47
  if (wlCopyPath) {
48
48
  // Fire-and-forget: wl-copy may not exit promptly, so we unref to avoid blocking
49
- void Bun.spawn([wlCopyPath], { stdin: Buffer.from(text), timeout }).unref();
49
+ void Bun.spawn([wlCopyPath], { stdin: Buffer.from(text), timeout, windowsHide: true }).unref();
50
50
  return;
51
51
  }
52
52
  }
53
53
 
54
54
  // Linux - try xclip first, fall back to xsel
55
55
  try {
56
- await Bun.spawn(["xclip", "-selection", "clipboard"], { stdin: Buffer.from(text), timeout }).exited;
56
+ await Bun.spawn(["xclip", "-selection", "clipboard"], {
57
+ stdin: Buffer.from(text),
58
+ timeout,
59
+ windowsHide: true,
60
+ }).exited;
57
61
  } catch {
58
- await Bun.spawn(["xsel", "--clipboard", "--input"], { stdin: Buffer.from(text), timeout }).exited;
62
+ await Bun.spawn(["xsel", "--clipboard", "--input"], {
63
+ stdin: Buffer.from(text),
64
+ timeout,
65
+ windowsHide: true,
66
+ }).exited;
59
67
  }
60
68
  }
61
69
  } catch (error) {