@oh-my-pi/pi-coding-agent 16.4.6 → 16.5.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 (141) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/dist/cli.js +3312 -3254
  3. package/dist/types/cli/args.d.ts +5 -0
  4. package/dist/types/cli/gallery-fixtures/shell.d.ts +1 -1
  5. package/dist/types/commands/launch.d.ts +15 -0
  6. package/dist/types/config/model-resolver.d.ts +4 -3
  7. package/dist/types/config/model-roles.d.ts +8 -0
  8. package/dist/types/config/settings-schema.d.ts +46 -14
  9. package/dist/types/extensibility/extensions/types.d.ts +1 -1
  10. package/dist/types/launch/broker.d.ts +2 -0
  11. package/dist/types/launch/client.d.ts +22 -0
  12. package/dist/types/launch/paths.d.ts +4 -0
  13. package/dist/types/launch/presence.d.ts +8 -0
  14. package/dist/types/launch/protocol.d.ts +170 -0
  15. package/dist/types/launch/terminal-output.d.ts +7 -0
  16. package/dist/types/modes/components/agent-hub.d.ts +1 -1
  17. package/dist/types/modes/components/index.d.ts +1 -0
  18. package/dist/types/modes/components/model-browser.d.ts +28 -3
  19. package/dist/types/modes/components/model-hub.d.ts +0 -10
  20. package/dist/types/modes/components/model-picker.d.ts +44 -0
  21. package/dist/types/modes/components/plan-review-overlay.d.ts +2 -0
  22. package/dist/types/modes/components/status-line/types.d.ts +3 -0
  23. package/dist/types/modes/components/welcome.d.ts +4 -0
  24. package/dist/types/modes/print-mode.d.ts +14 -8
  25. package/dist/types/modes/print-mode.test.d.ts +1 -0
  26. package/dist/types/modes/theme/theme.d.ts +2 -1
  27. package/dist/types/sdk.d.ts +5 -1
  28. package/dist/types/session/agent-session.d.ts +42 -0
  29. package/dist/types/session/session-context.d.ts +9 -0
  30. package/dist/types/session/session-entries.d.ts +6 -0
  31. package/dist/types/thinking.d.ts +2 -2
  32. package/dist/types/tiny/models.d.ts +1 -1
  33. package/dist/types/tools/browser/launch.d.ts +1 -0
  34. package/dist/types/tools/browser/run-cancellation.d.ts +28 -2
  35. package/dist/types/tools/browser/tab-protocol.d.ts +6 -0
  36. package/dist/types/tools/browser/tab-worker.d.ts +6 -0
  37. package/dist/types/tools/builtin-names.d.ts +1 -1
  38. package/dist/types/tools/index.d.ts +1 -0
  39. package/dist/types/tools/launch.d.ts +121 -0
  40. package/dist/types/tools/render-utils.d.ts +2 -0
  41. package/dist/types/tools/terminal-output.d.ts +5 -0
  42. package/dist/types/vibe/runtime.d.ts +2 -2
  43. package/dist/types/web/search/types.d.ts +0 -8
  44. package/package.json +20 -20
  45. package/src/cli/args.ts +11 -0
  46. package/src/cli/flag-tables.ts +9 -0
  47. package/src/cli/gallery-fixtures/shell.ts +82 -1
  48. package/src/cli.ts +10 -0
  49. package/src/commands/launch.ts +17 -0
  50. package/src/config/model-resolver.ts +110 -31
  51. package/src/config/model-roles.ts +14 -0
  52. package/src/config/settings-schema.ts +71 -7
  53. package/src/edit/renderer.ts +13 -10
  54. package/src/eval/__tests__/agent-bridge.test.ts +2 -2
  55. package/src/eval/__tests__/completion-bridge.test.ts +1 -1
  56. package/src/eval/completion-bridge.ts +4 -4
  57. package/src/eval/js/shared/rewrite-imports.ts +31 -13
  58. package/src/export/ttsr.ts +0 -3
  59. package/src/extensibility/extensions/model-api.ts +1 -1
  60. package/src/extensibility/extensions/types.ts +1 -1
  61. package/src/internal-urls/docs-index.ts +4 -4
  62. package/src/launch/broker.ts +1017 -0
  63. package/src/launch/client.ts +344 -0
  64. package/src/launch/paths.ts +17 -0
  65. package/src/launch/presence.ts +82 -0
  66. package/src/launch/protocol.ts +386 -0
  67. package/src/launch/terminal-output.ts +46 -0
  68. package/src/main.ts +50 -1
  69. package/src/modes/acp/acp-agent.ts +8 -1
  70. package/src/modes/components/agent-hub.ts +101 -31
  71. package/src/modes/components/compaction-summary-message.ts +8 -2
  72. package/src/modes/components/index.ts +1 -0
  73. package/src/modes/components/model-browser.ts +152 -49
  74. package/src/modes/components/model-hub.ts +55 -142
  75. package/src/modes/components/model-picker.ts +233 -0
  76. package/src/modes/components/plan-review-overlay.ts +7 -0
  77. package/src/modes/components/snapcompact-shape-preview-doc.md +7 -11
  78. package/src/modes/components/status-line/component.test.ts +41 -2
  79. package/src/modes/components/status-line/component.ts +4 -0
  80. package/src/modes/components/status-line/segments.ts +6 -0
  81. package/src/modes/components/status-line/types.ts +3 -0
  82. package/src/modes/components/tips.txt +2 -1
  83. package/src/modes/components/welcome.ts +13 -14
  84. package/src/modes/controllers/command-controller.ts +9 -1
  85. package/src/modes/controllers/event-controller.ts +31 -32
  86. package/src/modes/controllers/selector-controller.ts +96 -22
  87. package/src/modes/controllers/tan-command-controller.ts +40 -1
  88. package/src/modes/interactive-mode.ts +20 -3
  89. package/src/modes/print-mode.test.ts +71 -0
  90. package/src/modes/print-mode.ts +51 -2
  91. package/src/modes/theme/theme.ts +9 -0
  92. package/src/modes/utils/ui-helpers.ts +25 -4
  93. package/src/prompts/agents/designer.md +1 -1
  94. package/src/prompts/agents/librarian.md +1 -1
  95. package/src/prompts/agents/reviewer.md +1 -1
  96. package/src/prompts/agents/scout.md +1 -1
  97. package/src/prompts/system/plan-yolo-handoff.md +5 -0
  98. package/src/prompts/system/prewalk-checklist.md +7 -0
  99. package/src/prompts/system/prewalk-continue.md +1 -0
  100. package/src/prompts/system/prewalk-plan.md +13 -0
  101. package/src/prompts/system/system-prompt.md +9 -7
  102. package/src/prompts/system/tan-context-switch.md +17 -0
  103. package/src/prompts/tools/bash.md +6 -4
  104. package/src/prompts/tools/browser.md +4 -4
  105. package/src/prompts/tools/launch.md +25 -0
  106. package/src/sdk.ts +8 -2
  107. package/src/session/agent-session.ts +550 -87
  108. package/src/session/session-context.test.ts +10 -5
  109. package/src/session/session-context.ts +25 -8
  110. package/src/session/session-entries.ts +6 -0
  111. package/src/slash-commands/builtin-registry.ts +25 -0
  112. package/src/task/agents.ts +2 -2
  113. package/src/thinking.ts +10 -3
  114. package/src/tiny/models.ts +7 -7
  115. package/src/tools/bash-interactive.ts +5 -8
  116. package/src/tools/bash.ts +38 -24
  117. package/src/tools/browser/cmux/cmux-tab.ts +17 -2
  118. package/src/tools/browser/launch.ts +8 -4
  119. package/src/tools/browser/run-cancellation.ts +66 -6
  120. package/src/tools/browser/tab-protocol.ts +6 -0
  121. package/src/tools/browser/tab-supervisor.ts +17 -1
  122. package/src/tools/browser/tab-worker.ts +140 -21
  123. package/src/tools/builtin-names.ts +1 -0
  124. package/src/tools/eval-render.ts +16 -14
  125. package/src/tools/index.ts +5 -0
  126. package/src/tools/inspect-image.ts +2 -2
  127. package/src/tools/launch.ts +643 -0
  128. package/src/tools/render-utils.ts +3 -0
  129. package/src/tools/renderers.ts +2 -0
  130. package/src/tools/terminal-output.ts +141 -0
  131. package/src/tts/speech-enhancer.ts +2 -2
  132. package/src/utils/image-vision-fallback.ts +3 -3
  133. package/src/vibe/runtime.ts +2 -2
  134. package/src/web/search/provider.ts +0 -10
  135. package/src/web/search/providers/perplexity.ts +18 -2
  136. package/src/web/search/providers/public.ts +2 -4
  137. package/src/web/search/types.ts +0 -10
  138. package/dist/types/web/search/providers/bing.d.ts +0 -14
  139. package/dist/types/web/search/providers/yahoo.d.ts +0 -14
  140. package/src/web/search/providers/bing.ts +0 -197
  141. package/src/web/search/providers/yahoo.ts +0 -179
@@ -131,6 +131,10 @@ const tabs = new Map<string, TabSession>();
131
131
  // awaits) cannot interleave and leak a worker + browser refCount.
132
132
  const acquireChains = new Map<string, Promise<void>>();
133
133
  const GRACE_MS = 750;
134
+ // Names of tabs the supervisor force-killed (timeout past grace, failed recycle),
135
+ // mapped to the kill reason. Lets the next `run` on that name explain WHY the tab
136
+ // vanished instead of a bare "not alive". Cleared when the name is opened again.
137
+ const killedTabs = new Map<string, string>();
134
138
 
135
139
  export function getTab(name: string): TabSession | undefined {
136
140
  return tabs.get(name);
@@ -161,6 +165,7 @@ async function acquireTabImpl(
161
165
  if (opts.signal?.aborted) {
162
166
  throw new ToolAbortError("Browser tab open aborted");
163
167
  }
168
+ killedTabs.delete(name);
164
169
  // Temporary refCount hold so releasing an existing tab on the SAME browser
165
170
  // below cannot drop it to refCount 0 and dispose the instance we are about
166
171
  // to reuse (e.g. reopening the sole tab with a different dialogs policy).
@@ -386,7 +391,14 @@ async function runInTabWithSnapshot(
386
391
  snapshot: SessionSnapshot,
387
392
  ): Promise<RunResultOk> {
388
393
  const tab = tabs.get(name);
389
- if (!tab || tab.state === "dead") throw new ToolError(`Tab ${JSON.stringify(name)} is not alive. Reopen it.`);
394
+ if (!tab || tab.state === "dead") {
395
+ const killed = killedTabs.get(name);
396
+ throw new ToolError(
397
+ killed
398
+ ? `Tab ${JSON.stringify(name)} was killed: ${killed}. Reopen it.`
399
+ : `Tab ${JSON.stringify(name)} is not alive. Open it first with action:"open".`,
400
+ );
401
+ }
390
402
  if (tab.pending.size > 0) throw new ToolError(`Tab ${JSON.stringify(name)} is busy`);
391
403
  const id = Snowflake.next();
392
404
  const { promise, resolve, reject } = Promise.withResolvers<RunResultOk>();
@@ -712,6 +724,9 @@ async function recycleTimedOutWorkerTab(tab: WorkerTabSession, timeoutMs: number
712
724
  safeDir: getPuppeteerDir(),
713
725
  targetId: tab.targetId,
714
726
  dialogs: tab.dialogPolicy,
727
+ // Unblock a wedged page (open JS dialog, hung navigation) before adopting it —
728
+ // otherwise init stalls, times out, and the tab gets force-killed.
729
+ recover: true,
715
730
  };
716
731
  let worker = await spawnTabWorker();
717
732
  try {
@@ -743,6 +758,7 @@ async function recycleTimedOutWorkerTab(tab: WorkerTabSession, timeoutMs: number
743
758
  async function forceKillTab(name: string, reason: string): Promise<void> {
744
759
  const tab = tabs.get(name);
745
760
  if (!tab) return;
761
+ killedTabs.set(name, reason);
746
762
  tab.state = "dead";
747
763
  const error = postmortem.markExpectedCleanupError(new ToolError(reason));
748
764
  for (const pending of tab.pending.values()) pending.reject(error);
@@ -6,6 +6,7 @@ import { postmortem, Snowflake, untilAborted } from "@oh-my-pi/pi-utils";
6
6
  import type { HTMLElement } from "linkedom";
7
7
  import type {
8
8
  Browser,
9
+ CDPSession,
9
10
  Dialog,
10
11
  ElementHandle,
11
12
  ElementScreenshotOptions,
@@ -35,7 +36,13 @@ import {
35
36
  loadPuppeteerInWorker,
36
37
  } from "./launch";
37
38
  import { extractReadableFromHtml, type ReadableFormat } from "./readable";
38
- import { markHandled, waitForBrowserRun } from "./run-cancellation";
39
+ import {
40
+ CELL_BUDGET_SLACK_MS,
41
+ markHandled,
42
+ resolvePredicateTimeout,
43
+ type WaitPredicateOptions,
44
+ waitForBrowserRun,
45
+ } from "./run-cancellation";
39
46
  import { cloneSafe, RunOutput } from "./run-output";
40
47
  import type {
41
48
  Observation,
@@ -50,6 +57,13 @@ import type {
50
57
  WorkerInitPayload,
51
58
  } from "./tab-protocol";
52
59
 
60
+ declare module "puppeteer-core" {
61
+ interface Frame {
62
+ /** Puppeteer's main JavaScript realm, retained by our pinned runtime patch. */
63
+ mainRealm(): Realm;
64
+ }
65
+ }
66
+
53
67
  declare global {
54
68
  interface Element extends HTMLElement {}
55
69
  function getComputedStyle(element: Element): Record<string, unknown>;
@@ -105,6 +119,11 @@ const PLAYWRIGHT_ONLY_SELECTOR_RE =
105
119
  type DialogPolicy = "accept" | "dismiss";
106
120
  type DragTarget = string | { readonly x: number; readonly y: number };
107
121
  type ActionabilityResult = { ok: true; x: number; y: number } | { ok: false; reason: string };
122
+ /** Last JS dialog seen on the page; kept for timeout attribution until handled or navigation. */
123
+ interface OpenDialogInfo {
124
+ type: string;
125
+ message: string;
126
+ }
108
127
 
109
128
  /**
110
129
  * Per-op fail-fast ceilings for `tab.*` helpers. All are kept strictly under the cell
@@ -126,7 +145,7 @@ type ActionabilityResult = { ok: true; x: number; y: number } | { ok: false; rea
126
145
  const QUICK_OP_TIMEOUT_MS = 20_000;
127
146
  const ACTION_OP_TIMEOUT_MS = 8_000;
128
147
  /** Headroom subtracted from the cell budget so a per-op deadline fires before it. */
129
- const OP_DEADLINE_SLACK_MS = 1_000;
148
+ const OP_DEADLINE_SLACK_MS = CELL_BUDGET_SLACK_MS;
130
149
  /**
131
150
  * A selector op whose selector has matched nothing for this long fails fast with the
132
151
  * zero-match hint instead of burning the rest of its deadline: a wrong selector or a
@@ -595,6 +614,7 @@ export class WorkerCore {
595
614
  #mode?: WorkerInitPayload["mode"];
596
615
  #dialogPolicy?: DialogPolicy;
597
616
  #dialogHandler?: (dialog: Dialog) => void;
617
+ #openDialog?: OpenDialogInfo;
598
618
 
599
619
  constructor(transport: Transport) {
600
620
  this.#transport = transport;
@@ -648,6 +668,7 @@ export class WorkerCore {
648
668
  });
649
669
  if (payload.mode === "headless") {
650
670
  this.#page = await this.#browser.newPage();
671
+ this.#observeDialogs();
651
672
  await applyStealthPatches(this.#browser, this.#page, { browserSession: null, override: null });
652
673
  await applyViewport(this.#page, payload.viewport);
653
674
  if (payload.dialogs) this.#applyDialogPolicy(payload.dialogs);
@@ -659,7 +680,15 @@ export class WorkerCore {
659
680
  });
660
681
  }
661
682
  } else {
662
- this.#page = await this.#findAttachedPage(payload.targetId);
683
+ const target = await this.#findAttachedTarget(payload.targetId);
684
+ // Post-timeout recycle: unblock the target BEFORE adopting the page — an open
685
+ // modal dialog or hung navigation can stall `target.page()` / ready info, and a
686
+ // stalled init used to time out and force-kill the tab.
687
+ if (payload.recover) await this.#recoverAttachedTarget(target);
688
+ const page = await target.page();
689
+ if (!page) throw new ToolError(`Target ${payload.targetId} is no longer available on the attached browser`);
690
+ this.#page = page;
691
+ this.#observeDialogs();
663
692
  if (payload.dialogs) this.#applyDialogPolicy(payload.dialogs);
664
693
  }
665
694
  this.#targetId = await targetIdForPage(this.#page);
@@ -669,17 +698,53 @@ export class WorkerCore {
669
698
  }
670
699
  }
671
700
 
672
- async #findAttachedPage(targetId: string): Promise<Page> {
701
+ async #findAttachedTarget(targetId: string): Promise<Target> {
673
702
  if (!this.#browser) throw new ToolError("Browser is not connected");
674
703
  for (const target of this.#browser.targets()) {
675
704
  if ((await targetIdForTarget(target).catch(() => "")) !== targetId) continue;
676
- const page = await target.page();
677
- if (!page) break;
678
- return page;
705
+ return target;
679
706
  }
680
707
  throw new ToolError(`Target ${targetId} is no longer available on the attached browser`);
681
708
  }
682
709
 
710
+ /**
711
+ * Best-effort unblocking of a wedged target during post-timeout recovery: dismiss any
712
+ * open JS dialog and stop a pending navigation over a raw CDP session (created on the
713
+ * target, not the page, so it works while the page itself is unresponsive). Every step
714
+ * tolerates "nothing to do".
715
+ */
716
+ async #recoverAttachedTarget(target: Target): Promise<void> {
717
+ let session: CDPSession | undefined;
718
+ try {
719
+ session = await target.createCDPSession();
720
+ await session.send("Page.enable").catch(() => undefined);
721
+ await session.send("Page.handleJavaScriptDialog", { accept: false }).catch(() => undefined);
722
+ await session.send("Page.stopLoading").catch(() => undefined);
723
+ } catch (error) {
724
+ this.#log("debug", "Recovery CDP session failed; proceeding with attach", {
725
+ error: error instanceof Error ? error.message : String(error),
726
+ });
727
+ } finally {
728
+ await session?.detach().catch(() => undefined);
729
+ }
730
+ }
731
+
732
+ /**
733
+ * Record JS dialogs for timeout attribution without handling them (semantics of an
734
+ * unset `dialogs` policy are unchanged — the page stays blocked until user code or
735
+ * the policy handler acts). Cleared when the policy handler settles the dialog or a
736
+ * main-frame navigation proves the modal is gone.
737
+ */
738
+ #observeDialogs(): void {
739
+ const page = this.#requirePage();
740
+ page.on("dialog", dialog => {
741
+ this.#openDialog = { type: dialog.type(), message: dialog.message() };
742
+ });
743
+ page.on("framenavigated", frame => {
744
+ if (frame === page.mainFrame()) this.#openDialog = undefined;
745
+ });
746
+ }
747
+
683
748
  async #currentReadyInfo(): Promise<ReadyInfo> {
684
749
  const page = this.#requirePage();
685
750
  const targetId = this.#targetId ?? (await targetIdForPage(page));
@@ -698,11 +763,15 @@ export class WorkerCore {
698
763
  if (this.#dialogHandler) page.off("dialog", this.#dialogHandler);
699
764
  const handler = (dialog: Dialog): void => {
700
765
  const action = policy === "accept" ? dialog.accept() : dialog.dismiss();
701
- void action.catch(err =>
702
- this.#log("debug", "Dialog auto-handler failed", {
703
- policy,
704
- error: err instanceof Error ? err.message : String(err),
705
- }),
766
+ void action.then(
767
+ () => {
768
+ this.#openDialog = undefined;
769
+ },
770
+ err =>
771
+ this.#log("debug", "Dialog auto-handler failed", {
772
+ policy,
773
+ error: err instanceof Error ? err.message : String(err),
774
+ }),
706
775
  );
707
776
  };
708
777
  page.on("dialog", handler);
@@ -761,7 +830,20 @@ export class WorkerCore {
761
830
  assert: (cond: unknown, text?: string): void => {
762
831
  if (!cond) throw new ToolError(text ?? "Assertion failed");
763
832
  },
764
- wait: (ms: number): Promise<void> => waitForBrowserRun(ms, signal),
833
+ // Both wait forms register in the in-flight map so a cell that dies while
834
+ // sleeping/polling names the culprit instead of a bare whole-cell timeout.
835
+ wait: (msOrPredicate: number | (() => unknown), opts?: WaitPredicateOptions): Promise<unknown> => {
836
+ const label = typeof msOrPredicate === "number" ? `wait(${msOrPredicate}ms)` : "wait(predicate)";
837
+ const resolved =
838
+ typeof msOrPredicate === "number"
839
+ ? undefined
840
+ : { timeout: resolvePredicateTimeout(msg.timeoutMs, opts?.timeout), interval: opts?.interval };
841
+ return markHandled(
842
+ this.#runOp(active, label, signal, Number.POSITIVE_INFINITY, sig =>
843
+ waitForBrowserRun(msOrPredicate, sig, resolved),
844
+ ),
845
+ );
846
+ },
765
847
  });
766
848
  const { promise: cancelRejection, reject: rejectCancel } = Promise.withResolvers<never>();
767
849
  const onCancel = (): void => {
@@ -771,9 +853,13 @@ export class WorkerCore {
771
853
  : new ToolAbortError(undefined, { cause: signal.reason });
772
854
  if (timeoutSignal.aborted) {
773
855
  const stalled = describeInflight(active.inflight);
856
+ const dialog = this.#openDialog;
857
+ const dialogNote = dialog
858
+ ? `; a ${dialog.type}(${JSON.stringify(dialog.message.slice(0, 80))}) dialog opened during this run and may still block the page — reopen the tab with dialogs:"accept"|"dismiss" or handle page.on('dialog')`
859
+ : "";
774
860
  rejectCancel(
775
861
  new ToolError(
776
- `Browser code execution timed out after ${msg.timeoutMs}ms${stalled ? ` (stalled on ${stalled})` : ""}`,
862
+ `Browser code execution timed out after ${msg.timeoutMs}ms${stalled ? ` (stalled on ${stalled})` : ""}${dialogNote}`,
777
863
  ),
778
864
  );
779
865
  } else {
@@ -985,7 +1071,7 @@ export class WorkerCore {
985
1071
  active: ActiveRun,
986
1072
  ): TabApi {
987
1073
  const page = this.#requirePage();
988
- const { quickOpMs, actionOpMs } = resolveOpTimeouts(timeoutMs);
1074
+ const { budgetBound, quickOpMs, actionOpMs } = resolveOpTimeouts(timeoutMs);
989
1075
  const waitMs = (explicit?: number): number => resolveWaitTimeout(timeoutMs, explicit);
990
1076
  const INF = Number.POSITIVE_INFINITY;
991
1077
  const op = <T>(
@@ -1003,10 +1089,24 @@ export class WorkerCore {
1003
1089
  goto: (url, opts) =>
1004
1090
  op(`tab.goto(${JSON.stringify(url)})`, INF, async sig => {
1005
1091
  this.#clearElementCache();
1006
- // Default to "load" because dev servers with HMR/WS never reach networkidle.
1007
- await untilAborted(sig, () =>
1008
- page.goto(url, { waitUntil: opts?.waitUntil ?? "load", timeout: timeoutMs }),
1009
- );
1092
+ try {
1093
+ // Default to "load" because dev servers with HMR/WS never reach networkidle.
1094
+ // budgetBound (not the full cell) so a hung navigation fails named and
1095
+ // catchable inside the run instead of dying with the whole cell.
1096
+ await untilAborted(sig, () =>
1097
+ page.goto(url, { waitUntil: opts?.waitUntil ?? "load", timeout: budgetBound }),
1098
+ );
1099
+ } catch (err) {
1100
+ if (err instanceof Error && err.name === "TimeoutError") {
1101
+ // Abandon the hung navigation NOW — a still-pending load stalls every
1102
+ // later op on this page and cascades into more opaque timeouts.
1103
+ await this.#stopLoading();
1104
+ throw new ToolError(
1105
+ `tab.goto(${JSON.stringify(url)}) timed out after ${budgetBound}ms; pending navigation stopped — retry with a longer tool timeout or waitUntil:"domcontentloaded"`,
1106
+ );
1107
+ }
1108
+ throw err;
1109
+ }
1010
1110
  }),
1011
1111
  observe: opts => op("tab.observe()", quickOpMs, sig => this.#collectObservation({ ...opts, signal: sig })),
1012
1112
  ariaSnapshot: (selector, opts) =>
@@ -1164,8 +1264,11 @@ export class WorkerCore {
1164
1264
  op("tab.evaluate()", INF, sig =>
1165
1265
  untilAborted(sig, () =>
1166
1266
  typeof fn === "string"
1167
- ? page.evaluate(fn)
1168
- : page.evaluate(fn as (...a: unknown[]) => unknown, ...args),
1267
+ ? page.mainFrame().mainRealm().evaluate(fn)
1268
+ : page
1269
+ .mainFrame()
1270
+ .mainRealm()
1271
+ .evaluate(fn as (...a: unknown[]) => unknown, ...args),
1169
1272
  ),
1170
1273
  ) as never,
1171
1274
  scrollIntoView: selector =>
@@ -1553,6 +1656,22 @@ export class WorkerCore {
1553
1656
  for (const handle of handles) void handle.dispose().catch(() => undefined);
1554
1657
  }
1555
1658
 
1659
+ /** Best-effort `Page.stopLoading` so an abandoned navigation cannot stall later ops. */
1660
+ async #stopLoading(): Promise<void> {
1661
+ try {
1662
+ const session = await this.#requirePage().createCDPSession();
1663
+ try {
1664
+ await session.send("Page.stopLoading");
1665
+ } finally {
1666
+ await session.detach().catch(() => undefined);
1667
+ }
1668
+ } catch (error) {
1669
+ this.#log("debug", "Page.stopLoading failed", {
1670
+ error: error instanceof Error ? error.message : String(error),
1671
+ });
1672
+ }
1673
+ }
1674
+
1556
1675
  async #close(): Promise<void> {
1557
1676
  this.#unsub();
1558
1677
  this.#clearElementCache();
@@ -1,6 +1,7 @@
1
1
  export const BUILTIN_TOOL_NAMES = [
2
2
  "read",
3
3
  "bash",
4
+ "launch",
4
5
  "edit",
5
6
  "ast_grep",
6
7
  "ast_edit",
@@ -414,37 +414,39 @@ function formatStatusEventExpanded(event: EvalStatusEvent, theme: Theme): string
414
414
  return lines;
415
415
  }
416
416
 
417
- /** Render status events as tree lines. */
417
+ /**
418
+ * Render status events as tree lines. Shows a tail window (newest events are
419
+ * the live edge for `log()` progress loops) behind an "… N earlier" marker,
420
+ * matching the code/output tail-window convention. Collapsed keeps a small
421
+ * fixed window; expanded widens to the viewport-sized preview window.
422
+ */
418
423
  function renderStatusEvents(events: EvalStatusEvent[], theme: Theme, expanded: boolean): string[] {
419
424
  if (events.length === 0) return [];
420
425
 
421
- const maxCollapsed = 3;
422
- const maxExpanded = 10;
423
- const displayCount = expanded ? Math.min(events.length, maxExpanded) : Math.min(events.length, maxCollapsed);
426
+ const max = expanded ? Math.max(10, previewWindowRows()) : 3;
427
+ const hidden = Math.max(0, events.length - max);
428
+ const visible = hidden > 0 ? events.slice(hidden) : events;
424
429
 
425
430
  const lines: string[] = [];
426
- for (let i = 0; i < displayCount; i++) {
427
- const isLast = i === displayCount - 1 && (expanded || events.length <= maxCollapsed);
431
+ if (hidden > 0) {
432
+ lines.push(`${theme.fg("dim", theme.tree.branch)} ${theme.fg("dim", `… ${hidden} earlier`)}`);
433
+ }
434
+ for (let i = 0; i < visible.length; i++) {
435
+ const isLast = i === visible.length - 1;
428
436
  const branch = isLast ? theme.tree.last : theme.tree.branch;
429
437
 
430
438
  if (expanded) {
431
- const eventLines = formatStatusEventExpanded(events[i], theme);
439
+ const eventLines = formatStatusEventExpanded(visible[i], theme);
432
440
  lines.push(`${theme.fg("dim", branch)} ${eventLines[0]}`);
433
441
  const continueBranch = isLast ? " " : `${theme.tree.vertical} `;
434
442
  for (let j = 1; j < eventLines.length; j++) {
435
443
  lines.push(`${theme.fg("dim", continueBranch)}${eventLines[j]}`);
436
444
  }
437
445
  } else {
438
- lines.push(`${theme.fg("dim", branch)} ${formatStatusEvent(events[i], theme)}`);
446
+ lines.push(`${theme.fg("dim", branch)} ${formatStatusEvent(visible[i], theme)}`);
439
447
  }
440
448
  }
441
449
 
442
- if (!expanded && events.length > maxCollapsed) {
443
- lines.push(`${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", `… ${events.length - maxCollapsed} more`)}`);
444
- } else if (expanded && events.length > maxExpanded) {
445
- lines.push(`${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", `… ${events.length - maxExpanded} more`)}`);
446
- }
447
-
448
450
  return lines;
449
451
  }
450
452
 
@@ -50,6 +50,7 @@ import { GrepTool } from "./grep";
50
50
  import { InspectImageTool } from "./inspect-image";
51
51
  import { IrcTool, isIrcEnabled } from "./irc";
52
52
  import { JobTool } from "./job";
53
+ import { LaunchTool } from "./launch";
53
54
  import { LearnTool } from "./learn";
54
55
  import { ManageSkillTool } from "./manage-skill";
55
56
  import { MemoryEditTool } from "./memory-edit";
@@ -89,6 +90,7 @@ export * from "./image-gen";
89
90
  export * from "./inspect-image";
90
91
  export * from "./irc";
91
92
  export * from "./job";
93
+ export * from "./launch";
92
94
  export * from "./learn";
93
95
  export * from "./manage-skill";
94
96
  export * from "./memory-edit";
@@ -389,6 +391,7 @@ export type BuiltinToolLoadMode = "essential" | "discoverable";
389
391
  export const DEFAULT_ESSENTIAL_TOOL_NAMES: readonly string[] = [
390
392
  "read",
391
393
  "bash",
394
+ "launch",
392
395
  "edit",
393
396
  "write",
394
397
  "glob",
@@ -448,6 +451,7 @@ export function filterInitialToolsForDiscoveryAll(
448
451
  export const BUILTIN_TOOLS: Record<BuiltinToolName, ToolFactory> = {
449
452
  read: s => new ReadTool(s),
450
453
  bash: s => new BashTool(s),
454
+ launch: s => new LaunchTool(s),
451
455
  edit: s => new EditTool(s),
452
456
  ast_grep: s => new AstGrepTool(s),
453
457
  ast_edit: s => new AstEditTool(s),
@@ -605,6 +609,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
605
609
  if (name === "goal") return goalEnabled && goalModeActive;
606
610
  if (name === "lsp") return enableLsp && session.settings.get("lsp.enabled");
607
611
  if (name === "bash") return session.settings.get("bash.enabled");
612
+ if (name === "launch") return session.settings.get("launch.enabled");
608
613
  if (name === "eval") return allowEval;
609
614
  if (name === "debug") return session.settings.get("debug.enabled");
610
615
  if (name === "todo") return !includeYield && session.settings.get("todo.enabled");
@@ -157,8 +157,8 @@ export class InspectImageTool implements AgentTool<typeof inspectImageSchema, In
157
157
 
158
158
  const activeModelPattern = this.session.getActiveModelString?.() ?? this.session.getModelString?.();
159
159
  const model =
160
- resolvePattern("pi/vision") ??
161
- resolvePattern("pi/default") ??
160
+ resolvePattern("@vision") ??
161
+ resolvePattern("@default") ??
162
162
  resolvePattern(activeModelPattern) ??
163
163
  availableModels[0];
164
164
  if (!model) {