@oh-my-pi/pi-coding-agent 16.3.14 → 16.4.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 (76) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/cli.js +3053 -3158
  3. package/dist/types/advisor/config.d.ts +7 -0
  4. package/dist/types/cli/args.d.ts +1 -0
  5. package/dist/types/config/model-resolver.d.ts +1 -1
  6. package/dist/types/config/models-config-schema.d.ts +28 -15
  7. package/dist/types/config/models-config.d.ts +6 -0
  8. package/dist/types/config/settings-schema.d.ts +7 -2
  9. package/dist/types/dap/config.d.ts +13 -1
  10. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  11. package/dist/types/lsp/config.d.ts +7 -1
  12. package/dist/types/main.d.ts +2 -0
  13. package/dist/types/mcp/transports/stdio.d.ts +8 -3
  14. package/dist/types/modes/components/hook-selector.d.ts +2 -0
  15. package/dist/types/modes/theme/defaults/index.d.ts +2 -0
  16. package/dist/types/modes/theme/theme.d.ts +3 -2
  17. package/dist/types/sdk.d.ts +2 -0
  18. package/dist/types/session/agent-session.d.ts +5 -3
  19. package/dist/types/session/session-context.test.d.ts +1 -0
  20. package/dist/types/session/session-entries.d.ts +4 -0
  21. package/dist/types/thinking.d.ts +6 -3
  22. package/dist/types/utils/file-display-mode.d.ts +1 -1
  23. package/package.json +12 -12
  24. package/src/advisor/__tests__/config.test.ts +84 -0
  25. package/src/advisor/config.ts +28 -0
  26. package/src/cli/args.ts +1 -0
  27. package/src/cli/flag-tables.ts +3 -0
  28. package/src/config/model-registry.ts +1 -1
  29. package/src/config/model-resolver.ts +14 -12
  30. package/src/config/models-config-schema.ts +3 -2
  31. package/src/config/settings-schema.ts +5 -2
  32. package/src/dap/config.ts +136 -39
  33. package/src/dap/defaults.json +1 -1
  34. package/src/eval/js/shared/runtime.ts +20 -4
  35. package/src/eval/js/worker-core.ts +9 -2
  36. package/src/exec/bash-executor.ts +8 -1
  37. package/src/extensibility/extensions/types.ts +6 -0
  38. package/src/internal-urls/docs-index.generated.txt +1 -1
  39. package/src/lsp/config.ts +27 -13
  40. package/src/lsp/index.ts +114 -29
  41. package/src/main.ts +21 -1
  42. package/src/mcp/transports/stdio.test.ts +12 -0
  43. package/src/mcp/transports/stdio.ts +14 -8
  44. package/src/modes/components/hook-selector.ts +9 -2
  45. package/src/modes/controllers/extension-ui-controller.ts +3 -0
  46. package/src/modes/controllers/input-controller.ts +4 -4
  47. package/src/modes/controllers/selector-controller.ts +1 -1
  48. package/src/modes/theme/defaults/dark-poimandres.json +6 -5
  49. package/src/modes/theme/defaults/light-poimandres.json +6 -5
  50. package/src/modes/theme/theme-schema.json +6 -2
  51. package/src/modes/theme/theme.ts +24 -18
  52. package/src/prompts/agents/init.md +1 -1
  53. package/src/prompts/agents/plan.md +2 -2
  54. package/src/prompts/agents/reviewer.md +1 -1
  55. package/src/prompts/agents/{explore.md → scout.md} +3 -2
  56. package/src/prompts/system/plan-mode-active.md +2 -2
  57. package/src/prompts/system/system-prompt.md +5 -3
  58. package/src/prompts/tools/ast-grep.md +1 -1
  59. package/src/prompts/tools/debug.md +4 -4
  60. package/src/prompts/tools/grep.md +1 -1
  61. package/src/prompts/tools/task.md +2 -2
  62. package/src/sdk.ts +68 -45
  63. package/src/session/agent-session.ts +215 -64
  64. package/src/session/session-context.test.ts +83 -0
  65. package/src/session/session-context.ts +1 -0
  66. package/src/session/session-entries.ts +4 -0
  67. package/src/session/session-manager.ts +9 -1
  68. package/src/system-prompt.test.ts +20 -11
  69. package/src/task/agents.ts +2 -5
  70. package/src/task/executor.ts +111 -74
  71. package/src/thinking.ts +16 -7
  72. package/src/tools/ask.ts +51 -13
  73. package/src/tools/browser/cmux/cmux-tab.ts +21 -12
  74. package/src/tools/debug.ts +41 -11
  75. package/src/utils/file-display-mode.ts +1 -1
  76. package/src/prompts/agents/tester.md +0 -111
package/src/lsp/config.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import { $which, isRecord, logger, pathIsWithin } from "@oh-my-pi/pi-utils";
4
+ import { $which, isRecord, logger, pathIsWithin, type WhichOptions } from "@oh-my-pi/pi-utils";
5
5
  import { YAML } from "bun";
6
6
  import { getConfigDirPaths } from "../config";
7
7
  import { type ClaudePluginRoot, getPreloadedPluginRoots } from "../discovery/helpers";
@@ -249,7 +249,7 @@ const LOCAL_BIN_PATHS: Array<{ markers: string[]; binDir: string }> = [
249
249
  { markers: ["Gemfile", "Gemfile.lock"], binDir: "vendor/bundle/bin" },
250
250
  { markers: ["Gemfile", "Gemfile.lock"], binDir: "bin" },
251
251
  // Go - check project-local bin
252
- { markers: ["go.mod", "go.sum"], binDir: "bin" },
252
+ { markers: ["go.mod", "go.sum", "go.work"], binDir: "bin" },
253
253
  ];
254
254
 
255
255
  const WINDOWS_LOCAL_EXECUTABLE_EXTENSIONS = [".exe", ".cmd", ".bat"] as const;
@@ -267,6 +267,21 @@ function resolveLocalCommand(basePath: string): string | null {
267
267
  return null;
268
268
  }
269
269
 
270
+ function resolveCommandFromLocalRoot(command: string, cwd: string): string | null {
271
+ for (const { markers, binDir } of LOCAL_BIN_PATHS) {
272
+ if (!hasRootMarkers(cwd, markers)) continue;
273
+ const resolved = resolveLocalCommand(path.join(cwd, binDir, command));
274
+ if (resolved) return resolved;
275
+ }
276
+ return null;
277
+ }
278
+
279
+ /** Controls project-local and PATH executable lookup. */
280
+ export interface ResolveCommandOptions extends Pick<WhichOptions, "cache" | "PATH"> {
281
+ /** Ordered project roots checked before PATH; defaults to the command cwd. */
282
+ localRoots?: readonly string[];
283
+ }
284
+
270
285
  /**
271
286
  * Resolve a command to an executable path.
272
287
  * Checks project-local bin directories first, then falls back to $PATH.
@@ -275,20 +290,19 @@ function resolveLocalCommand(basePath: string): string | null {
275
290
  * @param cwd - Working directory to search from
276
291
  * @returns Absolute path to the executable, or null if not found
277
292
  */
278
- export function resolveCommand(command: string, cwd: string): string | null {
279
- // Check local bin directories based on project markers
280
- for (const { markers, binDir } of LOCAL_BIN_PATHS) {
281
- if (hasRootMarkers(cwd, markers)) {
282
- const localPath = path.join(cwd, binDir, command);
283
- const resolvedLocalPath = resolveLocalCommand(localPath);
284
- if (resolvedLocalPath) {
285
- return resolvedLocalPath;
286
- }
293
+ export function resolveCommand(command: string, cwd: string, options?: ResolveCommandOptions): string | null {
294
+ if (options?.localRoots) {
295
+ for (const root of options.localRoots) {
296
+ const resolved = resolveCommandFromLocalRoot(command, root);
297
+ if (resolved) return resolved;
287
298
  }
299
+ } else {
300
+ const resolved = resolveCommandFromLocalRoot(command, cwd);
301
+ if (resolved) return resolved;
288
302
  }
289
303
 
290
- // Fall back to $PATH
291
- return $which(command);
304
+ if (!options) return $which(command);
305
+ return $which(command, { cache: options.cache, PATH: options.PATH });
292
306
  }
293
307
 
294
308
  interface ConfigSource {
package/src/lsp/index.ts CHANGED
@@ -573,8 +573,79 @@ interface ProjectType {
573
573
  description: string;
574
574
  }
575
575
 
576
+ /** Convert a `go.work` use directory into the package pattern `go build` needs. */
577
+ function goWorkspaceBuildPattern(diskPath: string): string | null {
578
+ const trimmed = diskPath.trim();
579
+ if (!trimmed) return null;
580
+
581
+ const isAbsolute = path.isAbsolute(trimmed) || path.win32.isAbsolute(trimmed);
582
+ const normalized = trimmed.replaceAll("\\", "/").replace(/\/+$/, "");
583
+ const dir = normalized || ".";
584
+ if (dir === ".") return "./...";
585
+ if (dir.endsWith("/...")) return dir;
586
+ if (isAbsolute || dir.startsWith("./") || dir.startsWith("../")) return `${dir}/...`;
587
+ return `./${dir}/...`;
588
+ }
589
+
590
+ /** Parse `go work edit -json` output into per-module package patterns. */
591
+ function parseGoWorkspaceBuildPatterns(output: string): string[] {
592
+ let parsed: unknown;
593
+ try {
594
+ parsed = JSON.parse(output);
595
+ } catch {
596
+ return [];
597
+ }
598
+
599
+ if (!parsed || typeof parsed !== "object" || !("Use" in parsed) || !Array.isArray(parsed.Use)) return [];
600
+
601
+ const patterns = new Set<string>();
602
+ for (const entry of parsed.Use) {
603
+ if (!entry || typeof entry !== "object" || !("DiskPath" in entry) || typeof entry.DiskPath !== "string") {
604
+ continue;
605
+ }
606
+ const pattern = goWorkspaceBuildPattern(entry.DiskPath);
607
+ if (pattern) patterns.add(pattern);
608
+ }
609
+ return [...patterns];
610
+ }
611
+
612
+ /** Resolve the `go build` command for a `go.work` workspace. */
613
+ async function resolveGoWorkspaceDiagnosticsCommand(cwd: string, signal?: AbortSignal): Promise<string[]> {
614
+ const fallback = ["go", "build", "./..."];
615
+ try {
616
+ const proc = Bun.spawn(["go", "work", "edit", "-json"], {
617
+ cwd,
618
+ stdout: "pipe",
619
+ stderr: "pipe",
620
+ windowsHide: true,
621
+ });
622
+ const abortHandler = () => {
623
+ proc.kill();
624
+ };
625
+ if (signal) {
626
+ signal.addEventListener("abort", abortHandler, { once: true });
627
+ }
628
+
629
+ try {
630
+ const [stdout] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
631
+ const exitCode = await proc.exited;
632
+ throwIfAborted(signal);
633
+ if (exitCode !== 0) return fallback;
634
+ const patterns = parseGoWorkspaceBuildPatterns(stdout);
635
+ return patterns.length > 0 ? ["go", "build", ...patterns] : fallback;
636
+ } finally {
637
+ signal?.removeEventListener("abort", abortHandler);
638
+ }
639
+ } catch {
640
+ if (signal?.aborted) {
641
+ throw new ToolAbortError();
642
+ }
643
+ return fallback;
644
+ }
645
+ }
646
+
576
647
  /** Detect project type from root markers */
577
- function detectProjectType(cwd: string): ProjectType {
648
+ async function detectProjectType(cwd: string, signal?: AbortSignal): Promise<ProjectType> {
578
649
  // Check for Rust (Cargo.toml)
579
650
  if (fs.existsSync(path.join(cwd, "Cargo.toml"))) {
580
651
  return { type: "rust", command: ["cargo", "check", "--message-format=short"], description: "Rust (cargo check)" };
@@ -585,6 +656,15 @@ function detectProjectType(cwd: string): ProjectType {
585
656
  return { type: "typescript", command: ["npx", "tsc", "--noEmit"], description: "TypeScript (tsc --noEmit)" };
586
657
  }
587
658
 
659
+ // Check for Go workspaces before single-module Go projects.
660
+ if (fs.existsSync(path.join(cwd, "go.work"))) {
661
+ return {
662
+ type: "go",
663
+ command: await resolveGoWorkspaceDiagnosticsCommand(cwd, signal),
664
+ description: "Go workspace (go build)",
665
+ };
666
+ }
667
+
588
668
  // Check for Go (go.mod)
589
669
  if (fs.existsSync(path.join(cwd, "go.mod"))) {
590
670
  return { type: "go", command: ["go", "build", "./..."], description: "Go (go build)" };
@@ -604,47 +684,52 @@ async function runWorkspaceDiagnostics(
604
684
  signal?: AbortSignal,
605
685
  ): Promise<{ output: string; projectType: ProjectType }> {
606
686
  throwIfAborted(signal);
607
- const projectType = detectProjectType(cwd);
687
+ const projectType = await detectProjectType(cwd, signal);
608
688
  if (!projectType.command) {
609
689
  return {
610
- output: `Cannot detect project type. Supported: Rust (Cargo.toml), TypeScript (tsconfig.json), Go (go.mod), Python (pyproject.toml)`,
690
+ output: `Cannot detect project type. Supported: Rust (Cargo.toml), TypeScript (tsconfig.json), Go (go.work/go.mod), Python (pyproject.toml)`,
611
691
  projectType,
612
692
  };
613
693
  }
614
- const proc = Bun.spawn(projectType.command, {
615
- cwd,
616
- stdout: "pipe",
617
- stderr: "pipe",
618
- windowsHide: true,
619
- });
620
- const abortHandler = () => {
621
- proc.kill();
622
- };
623
- if (signal) {
624
- signal.addEventListener("abort", abortHandler, { once: true });
625
- }
626
-
627
694
  try {
628
- const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
629
- await proc.exited;
630
- throwIfAborted(signal);
631
- const combined = (stdout + stderr).trim();
632
- if (!combined) {
633
- return { output: "No issues found", projectType };
695
+ const proc = Bun.spawn(projectType.command, {
696
+ cwd,
697
+ stdout: "pipe",
698
+ stderr: "pipe",
699
+ windowsHide: true,
700
+ });
701
+ const abortHandler = () => {
702
+ proc.kill();
703
+ };
704
+ if (signal) {
705
+ signal.addEventListener("abort", abortHandler, { once: true });
634
706
  }
635
- // Limit output length
636
- const lines = combined.split("\n");
637
- if (lines.length > 50) {
638
- return { output: `${lines.slice(0, 50).join("\n")}\n[…${lines.length - 50}ln elided…]`, projectType };
707
+
708
+ try {
709
+ const [stdout, stderr] = await Promise.all([
710
+ new Response(proc.stdout).text(),
711
+ new Response(proc.stderr).text(),
712
+ ]);
713
+ await proc.exited;
714
+ throwIfAborted(signal);
715
+ const combined = (stdout + stderr).trim();
716
+ if (!combined) {
717
+ return { output: "No issues found", projectType };
718
+ }
719
+ // Limit output length
720
+ const lines = combined.split("\n");
721
+ if (lines.length > 50) {
722
+ return { output: `${lines.slice(0, 50).join("\n")}\n[…${lines.length - 50}ln elided…]`, projectType };
723
+ }
724
+ return { output: combined, projectType };
725
+ } finally {
726
+ signal?.removeEventListener("abort", abortHandler);
639
727
  }
640
- return { output: combined, projectType };
641
728
  } catch (e) {
642
729
  if (signal?.aborted) {
643
730
  throw new ToolAbortError();
644
731
  }
645
732
  return { output: `Failed to run ${projectType.command.join(" ")}: ${e}`, projectType };
646
- } finally {
647
- signal?.removeEventListener("abort", abortHandler);
648
733
  }
649
734
  }
650
735
 
package/src/main.ts CHANGED
@@ -789,7 +789,8 @@ export function applyResolvedSystemPromptInputs(
789
789
  }
790
790
  }
791
791
 
792
- async function buildSessionOptions(
792
+ /** Builds startup session options from parsed CLI flags, scoped models, and resolved session lineage. */
793
+ export async function buildSessionOptions(
793
794
  parsed: Args,
794
795
  scopedModels: ScopedModel[],
795
796
  sessionManager: SessionManager | undefined,
@@ -820,6 +821,25 @@ async function buildSessionOptions(
820
821
  if (parsed.providerSessionId) {
821
822
  options.providerSessionId = parsed.providerSessionId;
822
823
  }
824
+ if (parsed.providerPromptCacheKey) {
825
+ options.providerPromptCacheKey = parsed.providerPromptCacheKey;
826
+ options.providerPromptCacheKeySource = "explicit";
827
+ } else {
828
+ const header = sessionManager?.getHeader();
829
+ const scopedModelOverride = scopedModels.length > 0 && !parsed.continue && !parsed.resume;
830
+ const forkCacheShapeChanged =
831
+ scopedModelOverride ||
832
+ parsed.model !== undefined ||
833
+ parsed.thinking !== undefined ||
834
+ parsed.systemPrompt !== undefined ||
835
+ parsed.appendSystemPrompt !== undefined ||
836
+ parsed.tools !== undefined ||
837
+ parsed.noTools === true;
838
+ if (!forkCacheShapeChanged && header?.providerPromptCacheKey) {
839
+ options.providerPromptCacheKey = header.providerPromptCacheKey;
840
+ options.providerPromptCacheKeySource = "fork";
841
+ }
842
+ }
823
843
 
824
844
  // Model from CLI
825
845
  // - supports --provider <name> --model <pattern>
@@ -31,6 +31,18 @@ describe("resolveStdioSpawnCommand", () => {
31
31
  });
32
32
  });
33
33
 
34
+ it("keeps Darwin stdio MCP servers attached so TCC Apple Events prompts can resolve", async () => {
35
+ await expect(
36
+ resolveStdioSpawnCommand(
37
+ { command: "xcrun", args: ["mcpbridge"] },
38
+ { cwd: process.cwd(), env: {}, platform: "darwin" },
39
+ ),
40
+ ).resolves.toEqual({
41
+ cmd: ["xcrun", "mcpbridge"],
42
+ detached: false,
43
+ });
44
+ });
45
+
34
46
  it("detaches off-Windows MCP servers so terminal job-control signals cannot stop them", async () => {
35
47
  await expect(
36
48
  resolveStdioSpawnCommand(
@@ -37,13 +37,18 @@ export interface StdioSpawnCommand {
37
37
  */
38
38
  windowsHide?: boolean;
39
39
  /**
40
- * Run the subprocess in its own session.
40
+ * Run the subprocess in its own session when the platform can safely do so.
41
41
  *
42
- * POSIX: `true`. Detach → `setsid`, so the MCP process tree has no
43
- * controlling terminal and terminal job-control signals (Ctrl+Z SIGTSTP,
42
+ * Linux/other POSIX: `true`. Detach → `setsid`, so the MCP process tree has
43
+ * no controlling terminal and terminal job-control signals (Ctrl+Z SIGTSTP,
44
44
  * background-read SIGTTIN) cannot stop stdio servers such as
45
45
  * `chrome-devtools-mcp` and leave our read loop blocked on silent pipes.
46
46
  *
47
+ * macOS: `false`. LaunchServices/TCC attributes Apple Events automation to
48
+ * the responsible terminal process only while the child stays in the
49
+ * inherited session; detaching via `setsid` prevents the permission prompt
50
+ * for servers such as `xcrun mcpbridge` (#4987).
51
+ *
47
52
  * Windows: `false`. There is no SIGTSTP/SIGTTIN to escape, and Windows
48
53
  * wrapper chains must stay in the OMP console session so nested console
49
54
  * grandchildren keep stdout routed through our pipe (#3544).
@@ -247,7 +252,7 @@ export async function resolveStdioSpawnCommand(
247
252
  options: ResolveStdioSpawnOptions,
248
253
  ): Promise<StdioSpawnCommand> {
249
254
  const args = config.args ?? [];
250
- if (options.platform !== "win32") return { cmd: [config.command, ...args], detached: true };
255
+ if (options.platform !== "win32") return { cmd: [config.command, ...args], detached: options.platform !== "darwin" };
251
256
 
252
257
  const windowsHide = options.hostHasInheritableConsole === undefined ? true : !options.hostHasInheritableConsole;
253
258
  const resolved = await resolveWindowsCommandPath(config.command, options.cwd, options.env);
@@ -366,10 +371,11 @@ export class StdioTransport implements MCPTransport {
366
371
  });
367
372
 
368
373
  // Platform-derived session and console-window handling come from
369
- // `resolveStdioSpawnCommand`: POSIX detaches into its own session to
370
- // escape terminal job-control signals (SIGTSTP, SIGTTIN); Windows stays
371
- // attached, and only hides the child when the host has no console to
372
- // share. See `StdioSpawnCommand`.
374
+ // `resolveStdioSpawnCommand`: Linux/other POSIX detach into their own
375
+ // session to escape terminal job-control signals (SIGTSTP, SIGTTIN);
376
+ // macOS stays attached so TCC can prompt for Apple Events automation;
377
+ // Windows stays attached, and only hides the child when the host has no
378
+ // console to share. See `StdioSpawnCommand`.
373
379
  this.#process = spawn({
374
380
  cmd: spawnCommand.cmd,
375
381
  cwd,
@@ -61,6 +61,8 @@ export interface HookSelectorOptions {
61
61
  tui?: TUI;
62
62
  timeout?: number;
63
63
  onTimeout?: () => void;
64
+ onTimeoutStart?: () => void;
65
+ onTimeoutReset?: () => void;
64
66
  initialIndex?: number;
65
67
  outline?: boolean;
66
68
  maxVisible?: number;
@@ -178,6 +180,7 @@ export class HookSelectorComponent extends Container {
178
180
  #onLeftCallback: (() => void) | undefined;
179
181
  #onRightCallback: (() => void) | undefined;
180
182
  #onExternalEditorCallback: (() => void) | undefined;
183
+ #onTimeoutResetCallback: (() => void) | undefined;
181
184
  #slider: HookSelectorSlider | undefined;
182
185
  #sliderIndex: number = 0;
183
186
  #sliderComponent: Text | undefined;
@@ -213,6 +216,7 @@ export class HookSelectorComponent extends Container {
213
216
  this.#onLeftCallback = opts?.onLeft;
214
217
  this.#onRightCallback = opts?.onRight;
215
218
  this.#onExternalEditorCallback = opts?.onExternalEditor;
219
+ this.#onTimeoutResetCallback = opts?.onTimeoutReset;
216
220
  if (opts?.slider && opts.slider.segments.length > 0) {
217
221
  this.#slider = opts.slider;
218
222
  this.#sliderIndex = Math.max(0, Math.min(opts.slider.index, opts.slider.segments.length - 1));
@@ -232,6 +236,7 @@ export class HookSelectorComponent extends Container {
232
236
  }
233
237
 
234
238
  if (opts?.timeout && opts.timeout > 0 && opts.tui) {
239
+ opts.onTimeoutStart?.();
235
240
  this.#countdown = new CountdownTimer(
236
241
  opts.timeout,
237
242
  opts.tui,
@@ -633,8 +638,10 @@ export class HookSelectorComponent extends Container {
633
638
  }
634
639
 
635
640
  handleInput(keyData: string): void {
636
- // Reset countdown on any interaction
637
- this.#countdown?.reset();
641
+ if (this.#countdown) {
642
+ this.#countdown.reset();
643
+ this.#onTimeoutResetCallback?.();
644
+ }
638
645
 
639
646
  if (matchesSelectCancel(keyData)) {
640
647
  this.#onCancelCallback();
@@ -61,6 +61,7 @@ export class ExtensionUiController {
61
61
  async initHooksAndCustomTools(): Promise<void> {
62
62
  // Create and set hook & tool UI context
63
63
  const uiContext: ExtensionUIContext = {
64
+ timeoutStartsOnPresentation: true,
64
65
  select: (title, options, dialogOptions) => this.showCollabAwareSelector(title, options, dialogOptions),
65
66
  confirm: (title, message, _dialogOptions) => this.showHookConfirm(title, message),
66
67
  input: (title, placeholder, dialogOptions) => this.showHookInput(title, placeholder, dialogOptions),
@@ -624,6 +625,8 @@ export class ExtensionUiController {
624
625
  initialIndex: dialogOptions?.initialIndex,
625
626
  timeout: dialogOptions?.timeout,
626
627
  onTimeout: dialogOptions?.onTimeout,
628
+ onTimeoutStart: dialogOptions?.onTimeoutStart,
629
+ onTimeoutReset: dialogOptions?.onTimeoutReset,
627
630
  tui: this.ctx.ui,
628
631
  outline: dialogOptions?.outline,
629
632
  disabledIndices: dialogOptions?.disabledIndices,
@@ -1024,10 +1024,10 @@ export class InputController {
1024
1024
  // leaves wrappers and pipeline peers running and the terminal
1025
1025
  // hung — exactly the failure shape we're fixing. Stopping the whole
1026
1026
  // group keeps the shell's job-control view consistent. Long-lived
1027
- // children that must survive the suspend (MCP stdio servers via
1028
- // the `detached: true` spawn in `mcp/transports/stdio.ts`, every
1029
- // brush external command via brush's per-child `setsid` in
1030
- // `crates/vendor/brush-core/src/commands.rs`) are already in
1027
+ // children that must survive the suspend (Linux/other POSIX MCP stdio
1028
+ // servers via the platform-specific `detached: true` spawn in
1029
+ // `mcp/transports/stdio.ts`, every brush external command via brush's
1030
+ // per-child `setsid` in `crates/vendor/brush-core/src/commands.rs`) are
1031
1031
  // their own sessions, so pgid=0 does not reach them.
1032
1032
  process.kill(0, "SIGSTOP");
1033
1033
  } catch (err) {
@@ -1179,7 +1179,7 @@ export class SelectorController {
1179
1179
  },
1180
1180
  onManualCodeInput: useManualInput ? () => manualInput.waitForInput(providerId) : undefined,
1181
1181
  });
1182
- await this.ctx.session.modelRegistry.refresh();
1182
+ this.ctx.session.modelRegistry.refreshInBackground();
1183
1183
  const block = new TranscriptBlock();
1184
1184
  block.addChild(
1185
1185
  new Text(theme.fg("success", `${theme.status.success} Successfully logged in to ${providerId}`), 1, 0),
@@ -122,11 +122,12 @@
122
122
  "status.pending": "◌",
123
123
  "nav.cursor": "▸",
124
124
  "nav.selected": "▴",
125
- "thinking.minimal": "",
126
- "thinking.low": "",
127
- "thinking.medium": "",
128
- "thinking.high": "",
129
- "thinking.xhigh": "",
125
+ "thinking.minimal": "",
126
+ "thinking.low": "",
127
+ "thinking.medium": "",
128
+ "thinking.high": "",
129
+ "thinking.xhigh": "",
130
+ "thinking.max": "●",
130
131
  "icon.model": "◇",
131
132
  "icon.plan": "◈",
132
133
  "icon.goal": "⊙",
@@ -122,11 +122,12 @@
122
122
  "status.pending": "◌",
123
123
  "nav.cursor": "▸",
124
124
  "nav.selected": "▴",
125
- "thinking.minimal": "",
126
- "thinking.low": "",
127
- "thinking.medium": "",
128
- "thinking.high": "",
129
- "thinking.xhigh": "",
125
+ "thinking.minimal": "",
126
+ "thinking.low": "",
127
+ "thinking.medium": "",
128
+ "thinking.high": "",
129
+ "thinking.xhigh": "",
130
+ "thinking.max": "●",
130
131
  "icon.model": "◇",
131
132
  "icon.plan": "◈",
132
133
  "icon.goal": "⊙",
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "colors": {
35
35
  "type": "object",
36
- "description": "Theme color definitions (all required)",
36
+ "description": "Theme color definitions (all required except thinkingMax)",
37
37
  "required": [
38
38
  "accent",
39
39
  "border",
@@ -301,7 +301,11 @@
301
301
  },
302
302
  "thinkingXhigh": {
303
303
  "$ref": "#/$defs/colorValue",
304
- "description": "Thinking level border: xhigh (OpenAI codex-max only)"
304
+ "description": "Thinking level border: xhigh"
305
+ },
306
+ "thinkingMax": {
307
+ "$ref": "#/$defs/colorValue",
308
+ "description": "Thinking level border: max (optional; falls back to thinkingXhigh when omitted)"
305
309
  },
306
310
  "bashMode": {
307
311
  "$ref": "#/$defs/colorValue",
@@ -142,6 +142,7 @@ export type SymbolKey =
142
142
  | "thinking.medium"
143
143
  | "thinking.high"
144
144
  | "thinking.xhigh"
145
+ | "thinking.max"
145
146
  | "thinking.autoPending"
146
147
  // Checkboxes
147
148
  | "checkbox.checked"
@@ -344,11 +345,12 @@ const UNICODE_SYMBOLS: SymbolMap = {
344
345
  // Compaction divider
345
346
  "icon.camera": "📷",
346
347
  // Thinking levels
347
- "thinking.minimal": " min",
348
- "thinking.low": " low",
349
- "thinking.medium": " med",
350
- "thinking.high": " high",
351
- "thinking.xhigh": " xhigh",
348
+ "thinking.minimal": " min",
349
+ "thinking.low": " low",
350
+ "thinking.medium": " med",
351
+ "thinking.high": " high",
352
+ "thinking.xhigh": " xhigh",
353
+ "thinking.max": "◉ max",
352
354
  "thinking.autoPending": "⟳",
353
355
  // Checkboxes
354
356
  "checkbox.checked": "☑",
@@ -639,19 +641,15 @@ const NERD_SYMBOLS: SymbolMap = {
639
641
  "icon.mic": "\uf130",
640
642
  // Compaction divider - fa-camera-retro
641
643
  "icon.camera": "\uf083",
642
- // Thinking Levels - emoji labels
643
- // pick: 🤨 min | alt:  min  min
644
- "thinking.minimal": "\u{F0E7} min",
645
- // pick: 🤔 low | alt:  low  low
646
- "thinking.low": "\u{F10C} low",
647
- // pick: 🤓 med | alt:  med  med
648
- "thinking.medium": "\u{F192} med",
649
- // pick: 🤯 high | alt: high high
650
- "thinking.high": "\u{F111} high",
651
- // pick: 🧠 xhi | alt:  xhi  xhi
652
- "thinking.xhigh": "\u{F06D} xhi",
653
- // pick: (fa-circle-o-notch) | alt: 󰂼 (nf-md-cached) ⟳
654
- "thinking.autoPending": "\uf1ce",
644
+ // Thinking levels increasing circle slices, with fire reserved for max.
645
+ "thinking.minimal": "\u{F0A9E} min",
646
+ "thinking.low": "\u{F0A9F} low",
647
+ "thinking.medium": "\u{F0AA1} med",
648
+ "thinking.high": "\u{F0AA3} high",
649
+ "thinking.xhigh": "\u{F0AA5} xhi",
650
+ "thinking.max": "\u{F06D} max",
651
+ // Auto mode uses shuffle until the model resolves its thinking level.
652
+ "thinking.autoPending": "\u{F074}",
655
653
  // Checkboxes
656
654
  // pick:  | alt:  
657
655
  "checkbox.checked": "\uf14a",
@@ -868,6 +866,7 @@ const ASCII_SYMBOLS: SymbolMap = {
868
866
  "thinking.medium": "[med]",
869
867
  "thinking.high": "[high]",
870
868
  "thinking.xhigh": "[xhi]",
869
+ "thinking.max": "[max]",
871
870
  "thinking.autoPending": "[~]",
872
871
  // Checkboxes
873
872
  "checkbox.checked": "[x]",
@@ -1059,6 +1058,7 @@ const themeColorsSchema = type({
1059
1058
  thinkingMedium: "string | number",
1060
1059
  thinkingHigh: "string | number",
1061
1060
  thinkingXhigh: "string | number",
1061
+ "thinkingMax?": "string | number",
1062
1062
  bashMode: "string | number",
1063
1063
  pythonMode: "string | number",
1064
1064
  statusLineBg: "string | number",
@@ -1163,6 +1163,7 @@ export type ThemeColor =
1163
1163
  | "thinkingMedium"
1164
1164
  | "thinkingHigh"
1165
1165
  | "thinkingXhigh"
1166
+ | "thinkingMax"
1166
1167
  | "bashMode"
1167
1168
  | "pythonMode"
1168
1169
  | "statusLineSep"
@@ -1225,6 +1226,7 @@ const THEME_COLOR_RECORD = {
1225
1226
  thinkingMedium: true,
1226
1227
  thinkingHigh: true,
1227
1228
  thinkingXhigh: true,
1229
+ thinkingMax: true,
1228
1230
  bashMode: true,
1229
1231
  pythonMode: true,
1230
1232
  statusLineSep: true,
@@ -1675,6 +1677,9 @@ export class Theme {
1675
1677
  return (str: string) => this.fg("thinkingHigh", str);
1676
1678
  case "xhigh":
1677
1679
  return (str: string) => this.fg("thinkingXhigh", str);
1680
+ case "max":
1681
+ // thinkingMax is optional; themes without it resolve to the xhigh color.
1682
+ return (str: string) => this.fg(this.#fgColors.thinkingMax ? "thinkingMax" : "thinkingXhigh", str);
1678
1683
  default:
1679
1684
  return (str: string) => this.fg("thinkingOff", str);
1680
1685
  }
@@ -1861,6 +1866,7 @@ export class Theme {
1861
1866
  medium: this.#symbols["thinking.medium"],
1862
1867
  high: this.#symbols["thinking.high"],
1863
1868
  xhigh: this.#symbols["thinking.xhigh"],
1869
+ max: this.#symbols["thinking.max"],
1864
1870
  autoPending: this.#symbols["thinking.autoPending"],
1865
1871
  };
1866
1872
  }
@@ -4,7 +4,7 @@ description: Generate AGENTS.md for current codebase
4
4
  thinking-level: medium
5
5
  ---
6
6
 
7
- Generate AGENTS.md by launching multiple `explore` agents in parallel (via `task` tool) scanning different areas (core src, tests, configs/build, scripts/docs), then synthesize findings into a single file.
7
+ Generate AGENTS.md by launching multiple `scout` agents in parallel (via `task` tool) scanning different areas (core src, tests, configs/build, scripts/docs), then synthesize findings into a single file.
8
8
 
9
9
  <structure>
10
10
  - **Project Overview**: Brief description of project purpose
@@ -2,7 +2,7 @@
2
2
  name: plan
3
3
  description: Software architect for complex multi-file architectural decisions. NOT for simple tasks, single-file changes, or tasks completable in <5 tool calls.
4
4
  tools: read, grep, glob, bash, lsp, web_search, ast_grep
5
- spawns: explore
5
+ spawns: scout
6
6
  model: pi/plan, pi/slow
7
7
  ---
8
8
 
@@ -19,7 +19,7 @@ Analyze the codebase and the user's request. Produce a detailed implementation p
19
19
  4. Identify types, interfaces, contracts
20
20
  5. Note dependencies between components
21
21
 
22
- You MUST spawn `explore` agents for independent areas and synthesize findings.
22
+ You MUST spawn `scout` agents for independent areas and synthesize findings.
23
23
 
24
24
  ## Phase 3: Design
25
25
  1. List concrete changes (files, functions, types)
@@ -2,7 +2,7 @@
2
2
  name: reviewer
3
3
  description: "Code review specialist for quality/security analysis"
4
4
  tools: read, grep, glob, bash, lsp, web_search, ast_grep
5
- spawns: explore
5
+ spawns: scout
6
6
  model: pi/slow
7
7
  output:
8
8
  properties:
@@ -1,10 +1,11 @@
1
1
  ---
2
- name: explore
3
- description: Fast read-only codebase scout returning compressed context for handoff
2
+ name: scout
3
+ description: MUST be used for exploratory codebase research, rapid code analysis, and broad pattern searches. Fast read-only scout returning compressed context for handoff.
4
4
  tools: read, grep, glob, web_search
5
5
  model: pi/smol
6
6
  thinking-level: medium
7
7
  read-summarize: false
8
+ blocking: true
8
9
  output:
9
10
  properties:
10
11
  summary: