@oh-my-pi/pi-coding-agent 16.3.15 → 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 (74) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/cli.js +3361 -3357
  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/tools/ast-grep.md +1 -1
  58. package/src/prompts/tools/debug.md +4 -4
  59. package/src/prompts/tools/grep.md +1 -1
  60. package/src/prompts/tools/task.md +2 -2
  61. package/src/sdk.ts +68 -45
  62. package/src/session/agent-session.ts +215 -64
  63. package/src/session/session-context.test.ts +83 -0
  64. package/src/session/session-context.ts +1 -0
  65. package/src/session/session-entries.ts +4 -0
  66. package/src/session/session-manager.ts +9 -1
  67. package/src/system-prompt.test.ts +20 -11
  68. package/src/task/agents.ts +2 -3
  69. package/src/task/executor.ts +111 -74
  70. package/src/thinking.ts +16 -7
  71. package/src/tools/ask.ts +51 -13
  72. package/src/tools/browser/cmux/cmux-tab.ts +21 -12
  73. package/src/tools/debug.ts +41 -11
  74. package/src/utils/file-display-mode.ts +1 -1
package/src/tools/ask.ts CHANGED
@@ -380,6 +380,7 @@ interface AskSingleQuestionOptions {
380
380
  }
381
381
 
382
382
  interface UIContext {
383
+ timeoutStartsOnPresentation?: boolean;
383
384
  select(
384
385
  prompt: string,
385
386
  options: ExtensionUISelectItem[],
@@ -389,6 +390,8 @@ interface UIContext {
389
390
  signal?: AbortSignal;
390
391
  outline?: boolean;
391
392
  onTimeout?: () => void;
393
+ onTimeoutStart?: () => void;
394
+ onTimeoutReset?: () => void;
392
395
  onLeft?: () => void;
393
396
  onRight?: () => void;
394
397
  helpText?: string;
@@ -432,12 +435,30 @@ async function askSingleQuestion(
432
435
  const helpText = navigation
433
436
  ? "up/down navigate enter select ←/→ question esc cancel"
434
437
  : "up/down navigate enter select esc cancel";
438
+ const timeoutMs = typeof timeout === "number" && timeout > 0 ? timeout : undefined;
439
+ const timeoutController = timeoutMs === undefined ? undefined : new AbortController();
440
+ const dialogSignal =
441
+ signal && timeoutController
442
+ ? AbortSignal.any([signal, timeoutController.signal])
443
+ : (timeoutController?.signal ?? signal);
444
+ let timeoutId: NodeJS.Timeout | undefined;
445
+ let timeoutStartedMs = Date.now();
446
+ const armFallbackTimeout = (durationMs: number) => {
447
+ clearTimeout(timeoutId);
448
+ timeoutStartedMs = Date.now();
449
+ timeoutId = setTimeout(() => {
450
+ timeoutTriggered = true;
451
+ timeoutController?.abort();
452
+ }, durationMs);
453
+ };
435
454
  const dialogOptions = {
436
455
  initialIndex,
437
456
  timeout,
438
- signal,
457
+ signal: dialogSignal,
439
458
  outline: true,
440
459
  onTimeout,
460
+ onTimeoutStart: timeoutMs === undefined ? undefined : () => armFallbackTimeout(timeoutMs),
461
+ onTimeoutReset: timeoutMs === undefined ? undefined : () => armFallbackTimeout(timeoutMs),
441
462
  helpText,
442
463
  selectionMarker: marker?.selectionMarker,
443
464
  checkedIndices: marker?.checkedIndices,
@@ -453,19 +474,32 @@ async function askSingleQuestion(
453
474
  }
454
475
  : undefined,
455
476
  };
456
- const startMs = Date.now();
457
- const choice = signal
458
- ? await untilAborted(signal, () => ui.select(prompt, optionsToShow, dialogOptions))
459
- : await ui.select(prompt, optionsToShow, dialogOptions);
460
- if (!timeoutTriggered && choice === undefined && typeof timeout === "number") {
461
- // Fallback for UI surfaces that enforce `timeout` without invoking
462
- // `onTimeout`: their auto-cancel resolves right at the deadline. A
463
- // cancel arriving well past the deadline is a deliberate user Esc on
464
- // a surface that kept the dialog open keep treating it as a cancel.
465
- const elapsed = Date.now() - startMs;
466
- timeoutTriggered = elapsed >= timeout && elapsed <= timeout + TIMEOUT_DETECTION_TOLERANCE_MS;
477
+ try {
478
+ const runSelect = () => {
479
+ const selection = ui.select(prompt, optionsToShow, dialogOptions);
480
+ if (timeoutMs !== undefined && !ui.timeoutStartsOnPresentation) {
481
+ armFallbackTimeout(timeoutMs);
482
+ }
483
+ return selection;
484
+ };
485
+ const choice = dialogSignal ? await untilAborted(dialogSignal, runSelect) : await runSelect();
486
+ if (!timeoutTriggered && choice === undefined && typeof timeout === "number") {
487
+ // Fallback for UI surfaces that enforce `timeout` without invoking
488
+ // `onTimeout`: their auto-cancel resolves right at the deadline. A
489
+ // cancel arriving well past the deadline is a deliberate user Esc on
490
+ // a surface that kept the dialog open — keep treating it as a cancel.
491
+ const elapsed = Date.now() - timeoutStartedMs;
492
+ timeoutTriggered = elapsed >= timeout && elapsed <= timeout + TIMEOUT_DETECTION_TOLERANCE_MS;
493
+ }
494
+ return { choice, timedOut: timeoutTriggered, navigation: navigationAction };
495
+ } catch (error) {
496
+ if (timeoutTriggered && error instanceof Error && error.name === "AbortError") {
497
+ return { choice: undefined, timedOut: true, navigation: navigationAction };
498
+ }
499
+ throw error;
500
+ } finally {
501
+ clearTimeout(timeoutId);
467
502
  }
468
- return { choice, timedOut: timeoutTriggered, navigation: navigationAction };
469
503
  };
470
504
 
471
505
  const promptForCustomInput = async (
@@ -614,6 +648,9 @@ async function askSingleQuestion(
614
648
  customInput = undefined;
615
649
  break;
616
650
  }
651
+ if (timedOut && selectedOptions.length === 0 && customInput === undefined) {
652
+ selectedOptions = getAutoSelectionOnTimeout(questionOptions, recommended);
653
+ }
617
654
  if (navigation?.allowForward) {
618
655
  return { selectedOptions, customInput, timedOut, navigation: "forward" };
619
656
  }
@@ -743,6 +780,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
743
780
 
744
781
  const extensionUi = context.ui;
745
782
  const ui: UIContext = {
783
+ timeoutStartsOnPresentation: extensionUi.timeoutStartsOnPresentation,
746
784
  select: (prompt, options, dialogOptions) => extensionUi.select(prompt, options, dialogOptions),
747
785
  editor: (title, prefill, dialogOptions, editorOptions) =>
748
786
  extensionUi.editor(title, prefill, dialogOptions, editorOptions),
@@ -1288,20 +1288,13 @@ export async function runCmuxCode(tab: CmuxTab, opts: RunCmuxCodeOptions): Promi
1288
1288
  const screenshots: ScreenshotResult[] = [];
1289
1289
  const runId = crypto.randomUUID();
1290
1290
  tab.setRunContext({ session: opts.snapshot, displays, screenshots, signal, timeoutMs: opts.timeoutMs });
1291
- const runtime = tab.ensureRuntime(opts.snapshot);
1292
- runtime.setCwd(opts.snapshot.cwd);
1293
- const runTab = bindBrowserRunFacade(tab, signal);
1294
- runtime.setRunScope({
1295
- page: bindBrowserRunFacade(tab.page, signal),
1296
- browser: bindBrowserRunFacade(tab.browser, signal),
1297
- tab: runTab,
1298
- assert: (cond: unknown, text?: string): void => {
1299
- if (!cond) throw new ToolError(text ?? "Assertion failed");
1300
- },
1301
- wait: (ms: number): Promise<void> => waitForBrowserRun(ms, signal),
1302
- });
1303
1291
 
1304
1292
  const { promise: cancelRejection, reject } = Promise.withResolvers<never>();
1293
+ // If the synchronous setup below throws (same-realm ownership conflict)
1294
+ // while `signal` is already aborted, `Promise.race` never attaches a
1295
+ // handler to this promise; keep its armed rejection from surfacing as an
1296
+ // unhandled rejection — the postmortem-fatal path this run guards against.
1297
+ cancelRejection.catch(() => {});
1305
1298
  const onAbort = (): void => {
1306
1299
  if (timeoutSignal.aborted) {
1307
1300
  reject(new ToolError(`Browser code execution timed out after ${opts.timeoutMs}ms`));
@@ -1317,6 +1310,22 @@ export async function runCmuxCode(tab: CmuxTab, opts: RunCmuxCodeOptions): Promi
1317
1310
  else signal.addEventListener("abort", onAbort, { once: true });
1318
1311
 
1319
1312
  try {
1313
+ const runtime = tab.ensureRuntime(opts.snapshot);
1314
+ // setCwd is non-exclusive; setRunScope/run still assert same-realm ownership.
1315
+ // Keep both inside try so a concurrent in-process eval/browser run surfaces as
1316
+ // a rejected promise the supervisor can report, never an unhandled rejection.
1317
+ runtime.setCwd(opts.snapshot.cwd);
1318
+ const runTab = bindBrowserRunFacade(tab, signal);
1319
+ runtime.setRunScope({
1320
+ page: bindBrowserRunFacade(tab.page, signal),
1321
+ browser: bindBrowserRunFacade(tab.browser, signal),
1322
+ tab: runTab,
1323
+ assert: (cond: unknown, text?: string): void => {
1324
+ if (!cond) throw new ToolError(text ?? "Assertion failed");
1325
+ },
1326
+ wait: (ms: number): Promise<void> => waitForBrowserRun(ms, signal),
1327
+ });
1328
+
1320
1329
  const hooks: RuntimeHooks = {
1321
1330
  onText: chunk => {
1322
1331
  throwIfAborted(signal);
@@ -31,6 +31,7 @@ import {
31
31
  type DapThread,
32
32
  type DapVariable,
33
33
  dapSessionManager,
34
+ getAdapterConfigs,
34
35
  getAvailableAdapters,
35
36
  type LaunchProgramKind,
36
37
  resolveLaunchOverrides,
@@ -50,6 +51,7 @@ import {
50
51
  formatStatusIcon,
51
52
  PREVIEW_LIMITS,
52
53
  replaceTabs,
54
+ shortenPath,
53
55
  TRUNCATE_LENGTHS,
54
56
  truncateToWidth,
55
57
  } from "./render-utils";
@@ -106,9 +108,9 @@ const debugActionSchema = type.enumerated(
106
108
  );
107
109
  const debugSchema = type({
108
110
  action: debugActionSchema,
109
- "program?": type("string").describe("program path"),
111
+ "program?": type("string").describe("debug target path; Delve accepts Go package directories"),
110
112
  "args?": type("string[]").describe("program arguments"),
111
- "adapter?": type("string").describe("debugger adapter (gdb, lldb-dap, debugpy, dlv)"),
113
+ "adapter?": type("string").describe("configured adapter id (gdb, lldb-dap, debugpy, dlv, rdbg, or dap.json entry)"),
112
114
  cwd: "string?",
113
115
  "file?": type("string").describe("source file"),
114
116
  "line?": type("number").describe("source line"),
@@ -494,7 +496,33 @@ function buildOutcomeText(outcome: DapContinueOutcome, timeoutSec: number, verb:
494
496
 
495
497
  function getConfiguredAdapters(cwd: string): string {
496
498
  const adapters = getAvailableAdapters(cwd).map(adapter => adapter.name);
497
- return adapters.length > 0 ? adapters.join(", ") : "none";
499
+ const names = adapters.length > 0 ? adapters.join(", ") : "none";
500
+ return truncateToWidth(replaceTabs(names), TRUNCATE_LENGTHS.LONG);
501
+ }
502
+
503
+ const ADAPTER_UNAVAILABLE_MESSAGES: Readonly<Record<string, string>> = {
504
+ debugpy: "adapter 'debugpy' is not available: python not found in PATH",
505
+ dlv: "adapter 'dlv' is not available: install with 'go install github.com/go-delve/delve/cmd/dlv@latest'",
506
+ rdbg: "adapter 'rdbg' is not available: install with 'gem install debug'",
507
+ };
508
+
509
+ const ADAPTER_CANONICAL_COMMANDS: Readonly<Record<string, string>> = {
510
+ debugpy: "python",
511
+ dlv: "dlv",
512
+ rdbg: "rdbg",
513
+ };
514
+
515
+ function formatAdapterUnavailable(adapterName: string, command: string, cwd: string): string {
516
+ const displayName = truncateToWidth(replaceTabs(adapterName), TRUNCATE_LENGTHS.SHORT);
517
+ const canonicalCommand = ADAPTER_CANONICAL_COMMANDS[adapterName] ?? adapterName;
518
+ if (command !== canonicalCommand) {
519
+ const displayCommand = truncateToWidth(replaceTabs(shortenPath(command)), TRUNCATE_LENGTHS.CONTENT);
520
+ return `adapter '${displayName}' is not available: configured command '${displayCommand}' did not resolve. Check the DAP adapter config for this workspace.`;
521
+ }
522
+ return (
523
+ ADAPTER_UNAVAILABLE_MESSAGES[adapterName] ??
524
+ `adapter '${displayName}' is not available. Installed adapters: ${getConfiguredAdapters(cwd)}`
525
+ );
498
526
  }
499
527
 
500
528
  async function classifyLaunchProgram(program: string): Promise<LaunchProgramKind> {
@@ -515,7 +543,7 @@ function validateLaunchProgram(
515
543
  if (programKind !== "directory" || adapter.acceptsDirectoryProgram) return;
516
544
  const displayPath = formatPathRelativeToCwd(program, cwd, { trailingSlash: true });
517
545
  throw new ToolError(
518
- `launch program resolves to a directory: ${displayPath}. Pass an executable file path, or for Python use adapter "debugpy" with program set to the .py file.`,
546
+ `launch program resolves to a directory: ${displayPath}. Pass an executable file path or choose an adapter that supports package directories.`,
519
547
  );
520
548
  }
521
549
 
@@ -711,15 +739,16 @@ export class DebugTool implements AgentTool<typeof debugSchema, DebugToolDetails
711
739
  const commandCwd = params.cwd ? resolveToCwd(params.cwd, this.session.cwd) : this.session.cwd;
712
740
  const program = resolveToCwd(params.program, commandCwd);
713
741
  const programKind = await classifyLaunchProgram(program);
714
- const adapter = selectLaunchAdapter(program, commandCwd, params.adapter, programKind);
715
- if (!adapter) {
716
- if (params.adapter === "debugpy") {
717
- throw new ToolError("adapter 'debugpy' is not available: python not found in PATH");
718
- }
742
+ const selection = selectLaunchAdapter(program, commandCwd, params.adapter, programKind);
743
+ if (selection.kind === "unavailable") {
744
+ throw new ToolError(formatAdapterUnavailable(selection.adapterName, selection.command, commandCwd));
745
+ }
746
+ if (selection.kind === "none") {
719
747
  throw new ToolError(
720
748
  `No debugger adapter available. Installed adapters: ${getConfiguredAdapters(commandCwd)}`,
721
749
  );
722
750
  }
751
+ const { adapter } = selection;
723
752
  validateLaunchProgram(program, commandCwd, programKind, adapter);
724
753
  const extraLaunchArguments = resolveLaunchOverrides(adapter, program, programKind);
725
754
  const snapshot = await dapSessionManager.launch(
@@ -738,8 +767,9 @@ export class DebugTool implements AgentTool<typeof debugSchema, DebugToolDetails
738
767
  const commandCwd = params.cwd ? resolveToCwd(params.cwd, this.session.cwd) : this.session.cwd;
739
768
  const adapter = selectAttachAdapter(commandCwd, params.adapter, params.port);
740
769
  if (!adapter) {
741
- if (params.adapter === "debugpy") {
742
- throw new ToolError("adapter 'debugpy' is not available: python not found in PATH");
770
+ if (params.adapter) {
771
+ const command = getAdapterConfigs(commandCwd)[params.adapter]?.command ?? params.adapter;
772
+ throw new ToolError(formatAdapterUnavailable(params.adapter, command, commandCwd));
743
773
  }
744
774
  throw new ToolError(
745
775
  `No debugger adapter available. Installed adapters: ${getConfiguredAdapters(commandCwd)}`,
@@ -21,7 +21,7 @@ export interface FileDisplayModeSession {
21
21
  /**
22
22
  * Computes effective line display mode from session settings/env.
23
23
  * Hashline mode takes precedence and implies line-addressed output everywhere.
24
- * Hashlines are suppressed when the edit tool is not available (e.g. explore agents),
24
+ * Hashlines are suppressed when the edit tool is not available (e.g. scout agents),
25
25
  * when the caller signals a `raw` read, and when the source is `immutable`
26
26
  * (e.g. internal URLs like artifact://, agent://, memory:// — there is no edit
27
27
  * path that could consume the anchors). Raw output is returned as-is.