@oh-my-pi/pi-coding-agent 17.2.8 → 17.2.10

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 (246) hide show
  1. package/CHANGELOG.md +94 -1
  2. package/dist/{CHANGELOG-0b0w17se.md → CHANGELOG-rhwzpexa.md} +94 -1
  3. package/dist/cli.js +13216 -15614
  4. package/dist/types/async/job-manager.d.ts +12 -0
  5. package/dist/types/cli/args.d.ts +1 -0
  6. package/dist/types/config/model-resolver.d.ts +13 -0
  7. package/dist/types/discovery/agents.d.ts +11 -0
  8. package/dist/types/extensibility/custom-commands/types.d.ts +5 -5
  9. package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
  10. package/dist/types/extensibility/extensions/loader.d.ts +2 -2
  11. package/dist/types/extensibility/extensions/types.d.ts +34 -8
  12. package/dist/types/extensibility/hooks/types.d.ts +5 -5
  13. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +10 -0
  14. package/dist/types/hindsight/state.d.ts +0 -1
  15. package/dist/types/index.d.ts +2 -2
  16. package/dist/types/lsp/utils.d.ts +8 -0
  17. package/dist/types/main.d.ts +1 -1
  18. package/dist/types/modes/acp/acp-agent.d.ts +1 -1
  19. package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
  20. package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -1
  21. package/dist/types/modes/acp/acp-mode.d.ts +1 -1
  22. package/dist/types/modes/components/agent-hub-projection.d.ts +38 -0
  23. package/dist/types/modes/components/agent-hub-renderer.d.ts +41 -0
  24. package/dist/types/modes/components/agent-hub.d.ts +12 -5
  25. package/dist/types/modes/components/model-browser.d.ts +9 -1
  26. package/dist/types/modes/interactive-mode.d.ts +8 -1
  27. package/dist/types/modes/session-observer-registry.d.ts +2 -0
  28. package/dist/types/registry/agent-lifecycle.d.ts +1 -1
  29. package/dist/types/registry/agent-registry.d.ts +47 -0
  30. package/dist/types/registry/persisted-agents.d.ts +3 -1
  31. package/dist/types/session/agent-session-types.d.ts +7 -0
  32. package/dist/types/session/agent-session.d.ts +8 -4
  33. package/dist/types/session/session-entries.d.ts +10 -0
  34. package/dist/types/session/session-loader.d.ts +11 -1
  35. package/dist/types/session/session-manager.d.ts +9 -1
  36. package/dist/types/session/session-tools.d.ts +10 -1
  37. package/dist/types/session/turn-recovery.d.ts +6 -2
  38. package/dist/types/slash-commands/acp-builtins.d.ts +1 -1
  39. package/dist/types/slash-commands/available-commands.d.ts +1 -1
  40. package/dist/types/task/executor.d.ts +10 -0
  41. package/dist/types/task/index.d.ts +2 -3
  42. package/dist/types/task/read-only-policy.d.ts +3 -0
  43. package/dist/types/task/structured-subagent.d.ts +2 -0
  44. package/dist/types/task/types.d.ts +4 -0
  45. package/dist/types/tools/bash.d.ts +3 -3
  46. package/dist/types/tools/browser/launch.d.ts +3 -0
  47. package/dist/types/tools/browser/tab-worker.d.ts +2 -2
  48. package/dist/types/tools/path-utils.d.ts +8 -0
  49. package/dist/types/tools/read.d.ts +3 -5
  50. package/dist/types/tools/run-scope.d.ts +27 -1
  51. package/dist/types/tools/shell-tokenize.d.ts +21 -3
  52. package/dist/types/tools/terminal-output.d.ts +1 -1
  53. package/dist/types/utils/changelog.d.ts +3 -2
  54. package/dist/types/utils/late-cleanup.d.ts +2 -0
  55. package/dist/types/utils/turndown.d.ts +1 -1
  56. package/dist/types/vibe/runtime.d.ts +4 -3
  57. package/dist/types/web/scrapers/readthedocs.d.ts +0 -3
  58. package/dist/types/web/search/providers/browser-headers.d.ts +2 -3
  59. package/examples/custom-tools/README.md +1 -1
  60. package/examples/extensions/README.md +1 -1
  61. package/examples/extensions/api-demo.ts +5 -6
  62. package/examples/extensions/chalk-logger.ts +1 -1
  63. package/examples/extensions/hello.ts +2 -2
  64. package/examples/extensions/reload-runtime.ts +1 -1
  65. package/examples/extensions/tools.ts +2 -2
  66. package/examples/extensions/with-deps/index.ts +1 -1
  67. package/examples/sdk/06-extensions.ts +1 -1
  68. package/package.json +15 -31
  69. package/scripts/bundle-dist.ts +1 -11
  70. package/scripts/omp +11 -2
  71. package/src/advisor/runtime.ts +54 -0
  72. package/src/async/job-manager.ts +26 -0
  73. package/src/cli/agents-cli.ts +1 -1
  74. package/src/cli/args.ts +24 -2
  75. package/src/cli/auth-broker-cli.ts +1 -1
  76. package/src/cli/auth-gateway-cli.ts +1 -1
  77. package/src/cli/bench-cli.ts +1 -1
  78. package/src/cli/claude-trace-cli.ts +1 -1
  79. package/src/cli/config-cli.ts +1 -1
  80. package/src/cli/dry-balance-cli.ts +1 -1
  81. package/src/cli/file-processor.ts +1 -1
  82. package/src/cli/flag-tables.ts +4 -0
  83. package/src/cli/grep-cli.ts +1 -1
  84. package/src/cli/grievances-cli.ts +1 -1
  85. package/src/cli/help-extra.ts +1 -1
  86. package/src/cli/models-cli.ts +1 -1
  87. package/src/cli/plugin-cli.ts +1 -1
  88. package/src/cli/read-cli.ts +1 -1
  89. package/src/cli/setup-cli.ts +1 -1
  90. package/src/cli/shell-cli.ts +1 -1
  91. package/src/cli/ssh-cli.ts +1 -1
  92. package/src/cli/stats-cli.ts +4 -4
  93. package/src/cli/tiny-models-cli.ts +1 -1
  94. package/src/cli/ttsr-cli.ts +1 -1
  95. package/src/cli/update-cli.ts +3 -57
  96. package/src/cli/usage-cli.ts +1 -1
  97. package/src/cli/web-search-cli.ts +1 -1
  98. package/src/cli/worktree-cli.ts +1 -1
  99. package/src/commands/say.ts +1 -1
  100. package/src/commands/token.ts +1 -1
  101. package/src/commit/agentic/agent.ts +1 -1
  102. package/src/commit/cli.ts +1 -1
  103. package/src/config/config-file.ts +2 -2
  104. package/src/config/keybindings.ts +3 -3
  105. package/src/config/model-discovery.ts +36 -2
  106. package/src/config/model-registry.ts +4 -1
  107. package/src/config/model-resolver.ts +54 -7
  108. package/src/discovery/agents.ts +73 -3
  109. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +5 -4
  110. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -1
  111. package/src/discovery/claude.ts +8 -5
  112. package/src/discovery/cursor.ts +8 -5
  113. package/src/discovery/gemini.ts +11 -8
  114. package/src/discovery/vscode.ts +1 -0
  115. package/src/discovery/windsurf.ts +4 -2
  116. package/src/edit/hashline/filesystem.ts +7 -7
  117. package/src/eval/agent-bridge.ts +1 -3
  118. package/src/eval/jl/prelude.jl +4 -4
  119. package/src/eval/js/shared/prelude.txt +2 -2
  120. package/src/eval/py/prelude.py +0 -3
  121. package/src/eval/py/runner.py +38 -1
  122. package/src/eval/rb/prelude.rb +1 -2
  123. package/src/extensibility/custom-commands/loader.ts +4 -4
  124. package/src/extensibility/custom-commands/types.ts +5 -5
  125. package/src/extensibility/custom-tools/loader.ts +4 -4
  126. package/src/extensibility/custom-tools/types.ts +1 -1
  127. package/src/extensibility/extensions/loader.ts +7 -6
  128. package/src/extensibility/extensions/types.ts +39 -8
  129. package/src/extensibility/hooks/loader.ts +4 -5
  130. package/src/extensibility/hooks/types.ts +5 -5
  131. package/src/extensibility/legacy-pi-coding-agent-shim.ts +23 -0
  132. package/src/extensibility/plugins/legacy-pi-compat.ts +53 -0
  133. package/src/hindsight/state.ts +2 -22
  134. package/src/index.ts +2 -2
  135. package/src/internal-urls/memory-protocol.ts +33 -0
  136. package/src/launch/terminal-output.ts +1 -1
  137. package/src/lsp/client.ts +2 -2
  138. package/src/lsp/utils.ts +29 -0
  139. package/src/main.ts +85 -21
  140. package/src/markit/converters/docx.ts +1 -1
  141. package/src/markit/converters/epub.ts +1 -1
  142. package/src/markit/converters/pptx.ts +1 -1
  143. package/src/markit/converters/xlsx.ts +1 -1
  144. package/src/modes/acp/acp-agent.ts +7 -6
  145. package/src/modes/acp/acp-client-bridge.ts +1 -1
  146. package/src/modes/acp/acp-event-mapper.ts +1 -1
  147. package/src/modes/acp/acp-mode.ts +1 -1
  148. package/src/modes/components/agent-hub-projection.ts +248 -0
  149. package/src/modes/components/agent-hub-renderer.ts +194 -0
  150. package/src/modes/components/agent-hub.ts +670 -192
  151. package/src/modes/components/agent-transcript-viewer.ts +1 -3
  152. package/src/modes/components/assistant-message.ts +1 -1
  153. package/src/modes/components/bordered-loader.ts +1 -1
  154. package/src/modes/components/custom-editor.ts +2 -2
  155. package/src/modes/components/extensions/inspector-panel.ts +11 -6
  156. package/src/modes/components/footer.ts +1 -3
  157. package/src/modes/components/hook-input.ts +1 -1
  158. package/src/modes/components/hook-selector.ts +2 -2
  159. package/src/modes/components/model-browser.ts +11 -3
  160. package/src/modes/components/model-hub.ts +4 -1
  161. package/src/modes/components/session-selector.ts +2 -2
  162. package/src/modes/components/status-line/segments.ts +4 -3
  163. package/src/modes/controllers/event-controller.ts +188 -7
  164. package/src/modes/controllers/extension-ui-controller.ts +4 -3
  165. package/src/modes/controllers/mcp-command-controller.ts +81 -20
  166. package/src/modes/controllers/selector-controller.ts +40 -49
  167. package/src/modes/controllers/streaming-reveal.ts +1 -1
  168. package/src/modes/interactive-mode.ts +13 -2
  169. package/src/modes/print-mode.ts +29 -12
  170. package/src/modes/rpc/rpc-mode.ts +2 -2
  171. package/src/modes/runtime-init.ts +1 -1
  172. package/src/modes/session-observer-registry.ts +5 -0
  173. package/src/modes/theme/theme.ts +2 -2
  174. package/src/prompts/tools/computer.md +1 -1
  175. package/src/registry/agent-lifecycle.ts +35 -10
  176. package/src/registry/agent-registry.ts +65 -2
  177. package/src/registry/persisted-agents.ts +341 -16
  178. package/src/sdk.ts +11 -5
  179. package/src/session/agent-session-types.ts +8 -0
  180. package/src/session/agent-session.ts +177 -188
  181. package/src/session/session-entries.ts +10 -0
  182. package/src/session/session-listing.ts +1 -1
  183. package/src/session/session-loader.ts +74 -8
  184. package/src/session/session-manager.ts +22 -2
  185. package/src/session/session-paths.ts +105 -110
  186. package/src/session/session-tools.ts +28 -1
  187. package/src/session/settings-stream-fn.ts +7 -3
  188. package/src/session/turn-recovery.ts +236 -18
  189. package/src/session/unexpected-stop-classifier.ts +12 -3
  190. package/src/slash-commands/acp-builtins.ts +1 -1
  191. package/src/slash-commands/available-commands.ts +1 -1
  192. package/src/slash-commands/helpers/stats-dashboard.ts +2 -1
  193. package/src/task/agents.ts +1 -1
  194. package/src/task/executor.ts +173 -47
  195. package/src/task/index.ts +9 -28
  196. package/src/task/isolation-runner.ts +43 -12
  197. package/src/task/persisted-revive.ts +12 -5
  198. package/src/task/read-only-policy.ts +27 -0
  199. package/src/task/structured-subagent.ts +31 -6
  200. package/src/task/types.ts +4 -0
  201. package/src/tiny/worker.ts +1 -1
  202. package/src/tools/auto-generated-guard.ts +1 -1
  203. package/src/tools/bash-interactive.ts +4 -4
  204. package/src/tools/bash-interceptor.ts +8 -4
  205. package/src/tools/bash.ts +6 -6
  206. package/src/tools/browser/cmux/cmux-tab.ts +66 -18
  207. package/src/tools/browser/launch.ts +76 -19
  208. package/src/tools/browser/readable.ts +9 -9
  209. package/src/tools/browser/tab-supervisor.ts +12 -3
  210. package/src/tools/browser/tab-worker-entry.ts +1 -1
  211. package/src/tools/browser/tab-worker.ts +101 -12
  212. package/src/tools/fetch.ts +1 -1
  213. package/src/tools/glob.ts +4 -2
  214. package/src/tools/path-utils.ts +22 -5
  215. package/src/tools/read.ts +22 -5
  216. package/src/tools/run-scope.ts +290 -4
  217. package/src/tools/shell-tokenize.ts +38 -9
  218. package/src/tools/terminal-output.ts +1 -1
  219. package/src/tools/todo.ts +1 -1
  220. package/src/utils/changelog.ts +5 -4
  221. package/src/utils/clipboard.ts +10 -2
  222. package/src/utils/external-editor.ts +4 -2
  223. package/src/utils/jj.ts +1 -1
  224. package/src/utils/late-cleanup.ts +17 -0
  225. package/src/utils/turndown.ts +1 -2
  226. package/src/vibe/runtime.ts +202 -58
  227. package/src/web/scrapers/arxiv.ts +1 -1
  228. package/src/web/scrapers/go-pkg.ts +1 -1
  229. package/src/web/scrapers/hackage.ts +1 -12
  230. package/src/web/scrapers/iacr.ts +1 -1
  231. package/src/web/scrapers/readthedocs.ts +2 -1
  232. package/src/web/scrapers/twitter.ts +2 -2
  233. package/src/web/scrapers/types.ts +1 -1
  234. package/src/web/scrapers/wikipedia.ts +1 -1
  235. package/src/web/search/providers/browser-headers.ts +12 -39
  236. package/src/web/search/providers/codex.ts +3 -26
  237. package/src/web/search/providers/duckduckgo.ts +1 -1
  238. package/src/web/search/providers/ecosia.ts +2 -2
  239. package/src/web/search/providers/exa.ts +1 -1
  240. package/src/web/search/providers/firecrawl.ts +1 -1
  241. package/src/web/search/providers/google.ts +1 -1
  242. package/src/web/search/providers/mojeek.ts +2 -2
  243. package/src/web/search/providers/perplexity.ts +1 -1
  244. package/src/web/search/providers/public.ts +1 -1
  245. package/src/web/search/providers/startpage.ts +2 -2
  246. package/src/markit/converters/mammoth.d.ts +0 -24
package/src/tools/read.ts CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  readImageMetadata,
27
27
  untilAborted,
28
28
  } from "@oh-my-pi/pi-utils";
29
- import { LRUCache } from "lru-cache/raw";
29
+ import { LRUCache } from "@oh-my-pi/pi-utils/lru";
30
30
  import {
31
31
  canonicalSnapshotKey,
32
32
  getFileSnapshotStore,
@@ -721,6 +721,10 @@ const readSchema = type({
721
721
  ),
722
722
  });
723
723
 
724
+ const readSchemaWithoutMemory = type({
725
+ path: type("string").describe("Local path, internal URI (e.g. skill://), or URL. Inline selectors are supported."),
726
+ });
727
+
724
728
  export type ReadToolInput = typeof readSchema.infer;
725
729
 
726
730
  export interface ReadToolDetails {
@@ -737,6 +741,8 @@ export interface ReadToolDetails {
737
741
  meta?: OutputMeta;
738
742
  /** Full on-disk byte size recorded before applying a file range. */
739
743
  fileSize?: number;
744
+ /** Full source line count when the read reached EOF and the count is exact. */
745
+ totalLines?: number;
740
746
  /** Raw text + start line for user-visible TUI rendering, set when content is text-like.
741
747
  * Mirrors the same lines the model receives but without hashline/line-number prefixes,
742
748
  * so the TUI can render the file content with its own gutter without re-parsing the formatted text. */
@@ -864,7 +870,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
864
870
  readonly label = "Read";
865
871
  readonly loadMode = "essential";
866
872
  description: string;
867
- readonly parameters = readSchema;
873
+ get parameters(): typeof readSchema {
874
+ return this.session.settings.get("memory.backend") === "off" ? readSchemaWithoutMemory : readSchema;
875
+ }
868
876
  readonly strict = true;
869
877
 
870
878
  readonly #autoResizeImages: boolean;
@@ -960,8 +968,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
960
968
  async #tryReadDelimitedPaths(
961
969
  readPath: string,
962
970
  signal?: AbortSignal,
971
+ routedUrlPredicate?: (entry: string) => boolean,
963
972
  ): Promise<AgentToolResult<ReadToolDetails> | null> {
964
- const parts = await splitDelimitedPathEntry(readPath, this.session.cwd);
973
+ const parts = await splitDelimitedPathEntry(readPath, this.session.cwd, { routedUrlPredicate });
965
974
  if (!parts) return null;
966
975
 
967
976
  const notice = `Note: interpreted as ${parts.length} paths: ${parts.join(", ")}`;
@@ -1397,6 +1406,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1397
1406
  const details = options.details ?? {};
1398
1407
  const allLines = text.split("\n");
1399
1408
  const totalLines = allLines.length;
1409
+ details.totalLines = totalLines;
1400
1410
  // User-requested 0-indexed range start. Lines BEFORE this are leading
1401
1411
  // context (added below if offset is explicit).
1402
1412
  const requestedStart = offset ? Math.max(0, offset - 1) : 0;
@@ -1592,6 +1602,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1592
1602
  const details = options.details ?? {};
1593
1603
  const allLines = text.split("\n");
1594
1604
  const totalLines = allLines.length;
1605
+ details.totalLines = totalLines;
1595
1606
  const shouldAddHashLines = displayMode.hashLines;
1596
1607
  const shouldAddLineNumbers = shouldAddHashLines ? false : displayMode.lineNumbers;
1597
1608
  const hashContext =
@@ -2302,9 +2313,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2302
2313
  }
2303
2314
 
2304
2315
  // Handle native OMP URLs and custom-scheme resources advertised by MCP servers.
2305
- // Use the internal-URL-aware splitter so malformed selectors are peeled
2306
- // off the URL and surfaced via parseSel rather than confusing handlers.
2307
2316
  const internalRouter = InternalUrlRouter.instance();
2317
+ const delimitedInternalResult = internalRouter.canResolve(readPath)
2318
+ ? await this.#tryReadDelimitedPaths(readPath, signal, entry => internalRouter.canResolve(entry))
2319
+ : null;
2320
+ if (delimitedInternalResult) return delimitedInternalResult;
2321
+
2322
+ // Peel malformed selectors through the internal-URL-aware parser before routing.
2308
2323
  let promotedSelector: string | undefined;
2309
2324
  if (internalRouter.canResolve(readPath)) {
2310
2325
  const internalTarget = splitInternalUrlSel(readPath);
@@ -2908,6 +2923,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2908
2923
  details = {};
2909
2924
  sourcePath = absolutePath;
2910
2925
  }
2926
+ if (reachedEof) details.totalLines = totalFileLines;
2911
2927
 
2912
2928
  if (hashContext?.tag) {
2913
2929
  recordSeenLinesFromBody(this.session, absolutePath, hashContext.tag, outputText);
@@ -3239,6 +3255,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
3239
3255
  if (!rawSelector && artifact.size > MAX_ARTIFACT_RAW_INLINE_BYTES) {
3240
3256
  outputText += `\n\n[${this.#formatArtifactWorkflowNotice(artifact, artifactUrl)}]`;
3241
3257
  }
3258
+ if (reachedEof) details.totalLines = totalFileLines;
3242
3259
  if (displayContent) details.displayContent = displayContent;
3243
3260
  if (truncationInfo) details.truncation = truncationInfo.result;
3244
3261
  const resultBuilder = toolResult<ReadToolDetails>(details)
@@ -1,6 +1,285 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
1
2
  import { untilAborted } from "@oh-my-pi/pi-utils/abortable";
3
+ import * as postmortem from "@oh-my-pi/pi-utils/postmortem";
2
4
  import { ToolError, throwIfAborted } from "./tool-errors";
3
5
 
6
+ const browserRunRejections = new WeakMap<object, object>();
7
+
8
+ /** Associates a browser operation failure with its owning evaluated run. */
9
+ export function markBrowserRunRejection<T>(reason: T, owner: object): T {
10
+ if (reason !== null && (typeof reason === "object" || typeof reason === "function")) {
11
+ browserRunRejections.set(reason, owner);
12
+ }
13
+ return reason;
14
+ }
15
+
16
+ /** Returns whether a rejection was marked for the specified evaluated run. */
17
+ export function isBrowserRunRejection(reason: unknown, owner: object): boolean {
18
+ return (
19
+ reason !== null &&
20
+ (typeof reason === "object" || typeof reason === "function") &&
21
+ browserRunRejections.get(reason) === owner
22
+ );
23
+ }
24
+
25
+ /** Returns whether a rejection belongs to the marked browser run or its evaluated source file. */
26
+ export function isBrowserRunOwnedRejection(reason: unknown, owner: object, filename: string): boolean {
27
+ if (isBrowserRunRejection(reason, owner)) return true;
28
+ return reason instanceof Error && typeof reason.stack === "string" && reason.stack.includes(filename);
29
+ }
30
+
31
+ type FloatingRejectionHandler = (reason: unknown) => void;
32
+
33
+ interface ObservedPromiseState {
34
+ handled: boolean;
35
+ userContinuationFailed: boolean;
36
+ }
37
+
38
+ const observedBrowserPromises = new WeakMap<Promise<unknown>, ObservedPromiseState>();
39
+ const observedPromiseConstructor = { [Symbol.species]: Promise };
40
+
41
+ type PromiseCombinatorName = "all" | "race";
42
+ type PromiseCombinator = (this: PromiseConstructor, values: Iterable<unknown>) => Promise<unknown>;
43
+
44
+ interface PromiseCombinatorTrackingContext {
45
+ owner: object;
46
+ onFloatingRejection: FloatingRejectionHandler;
47
+ }
48
+
49
+ const PROMISE_COMBINATORS: readonly PromiseCombinatorName[] = ["all", "race"];
50
+ const NativePromise = Promise;
51
+ const nativePromiseCombinators: Record<PromiseCombinatorName, PromiseCombinator> = {
52
+ all: Promise.all,
53
+ race: Promise.race,
54
+ };
55
+ const promiseCombinatorTracking = new AsyncLocalStorage<PromiseCombinatorTrackingContext>();
56
+ let previousPromiseDescriptor: PropertyDescriptor | undefined;
57
+ let promiseCombinatorTrackingScopes = 0;
58
+
59
+ /**
60
+ * Observes native promise-combinator results derived from browser promises for
61
+ * the duration of one evaluated run. Native `await` remains unchanged; dropped
62
+ * user continuations from `Promise.all` and `Promise.race` are routed to the
63
+ * owning run.
64
+ */
65
+ export async function withBrowserPromiseCombinatorTracking<T>(
66
+ owner: object,
67
+ onFloatingRejection: FloatingRejectionHandler,
68
+ run: () => Promise<T>,
69
+ ): Promise<T> {
70
+ installPromiseCombinatorTracking();
71
+ try {
72
+ return await promiseCombinatorTracking.run({ owner, onFloatingRejection }, run);
73
+ } finally {
74
+ restorePromiseCombinatorTracking();
75
+ }
76
+ }
77
+
78
+ function installPromiseCombinatorTracking(): void {
79
+ if (promiseCombinatorTrackingScopes > 0) {
80
+ promiseCombinatorTrackingScopes++;
81
+ return;
82
+ }
83
+ const descriptor = Object.getOwnPropertyDescriptor(globalThis, "Promise");
84
+ if (!descriptor) throw new Error("Global Promise descriptor is unavailable");
85
+ const trackedPromise = createTrackedPromiseConstructor();
86
+ Object.defineProperty(globalThis, "Promise", { ...descriptor, value: trackedPromise });
87
+ previousPromiseDescriptor = descriptor;
88
+ promiseCombinatorTrackingScopes = 1;
89
+ }
90
+
91
+ function restorePromiseCombinatorTracking(): void {
92
+ if (promiseCombinatorTrackingScopes > 1) {
93
+ promiseCombinatorTrackingScopes--;
94
+ return;
95
+ }
96
+ const descriptor = previousPromiseDescriptor;
97
+ try {
98
+ if (!descriptor) throw new Error("Global Promise tracking scope is not installed");
99
+ Object.defineProperty(globalThis, "Promise", descriptor);
100
+ } finally {
101
+ previousPromiseDescriptor = undefined;
102
+ promiseCombinatorTrackingScopes = 0;
103
+ }
104
+ }
105
+
106
+ function createTrackedPromiseConstructor(): PromiseConstructor {
107
+ class TrackedPromise<T> extends NativePromise<T> {}
108
+ for (const name of PROMISE_COMBINATORS) {
109
+ const original = nativePromiseCombinators[name];
110
+ Object.defineProperty(TrackedPromise, name, {
111
+ configurable: true,
112
+ writable: true,
113
+ value(this: PromiseConstructor, values: Iterable<unknown>): Promise<unknown> {
114
+ let hasObservedInput = false;
115
+ const result = Reflect.apply(original, this, [
116
+ tapObservedBrowserPromises(values, () => {
117
+ hasObservedInput = true;
118
+ }),
119
+ ]) as Promise<unknown>;
120
+ const context = promiseCombinatorTracking.getStore();
121
+ return hasObservedInput && context
122
+ ? observeBrowserRunPromise(result, context.owner, context.onFloatingRejection)
123
+ : result;
124
+ },
125
+ });
126
+ }
127
+ return TrackedPromise;
128
+ }
129
+
130
+ function* tapObservedBrowserPromises(
131
+ values: Iterable<unknown>,
132
+ onObserved: () => void,
133
+ ): Generator<unknown, void, undefined> {
134
+ for (const value of values) {
135
+ if (observedBrowserPromises.has(value as Promise<unknown>)) onObserved();
136
+ yield value;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Observes every explicit continuation of a browser promise without replacing
142
+ * the native promise. Browser failures remain contained; an unhandled error
143
+ * created by user continuation code is reported to the owning run.
144
+ */
145
+ export function observeBrowserRunPromise<T>(
146
+ promise: Promise<T>,
147
+ owner: object,
148
+ onFloatingRejection: FloatingRejectionHandler,
149
+ ): Promise<T> {
150
+ return observeBrowserRunPromiseWithState(promise, owner, onFloatingRejection, {
151
+ handled: false,
152
+ userContinuationFailed: false,
153
+ });
154
+ }
155
+
156
+ function observeBrowserRunPromiseWithState<T>(
157
+ promise: Promise<T>,
158
+ owner: object,
159
+ onFloatingRejection: FloatingRejectionHandler,
160
+ state: ObservedPromiseState,
161
+ ): Promise<T> {
162
+ if (observedBrowserPromises.has(promise)) return promise;
163
+ observedBrowserPromises.set(promise, state);
164
+ const originalThen = promise.then.bind(promise);
165
+ const originalFinally = promise.finally.bind(promise);
166
+ void originalThen(undefined, reason => {
167
+ setTimeout(() => {
168
+ if (!state.handled && (state.userContinuationFailed || !isBrowserRunRejection(reason, owner))) {
169
+ onFloatingRejection(reason);
170
+ }
171
+ }, 0);
172
+ });
173
+ Object.defineProperties(promise, {
174
+ constructor: { configurable: true, value: observedPromiseConstructor },
175
+ // biome-ignore lint/suspicious/noThenProperty: native Promise continuations must remain thenable.
176
+ then: {
177
+ configurable: true,
178
+ value: <TResult1 = T, TResult2 = never>(
179
+ onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
180
+ onRejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
181
+ ): Promise<TResult1 | TResult2> => {
182
+ state.handled = true;
183
+ const childState = createContinuationState();
184
+ return observeBrowserRunPromiseWithState(
185
+ originalThen(
186
+ recordContinuationFailure(onFulfilled, childState),
187
+ recordContinuationFailure(onRejected, childState),
188
+ ),
189
+ owner,
190
+ onFloatingRejection,
191
+ childState,
192
+ );
193
+ },
194
+ },
195
+ catch: {
196
+ configurable: true,
197
+ value: <TResult = never>(
198
+ onRejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null,
199
+ ): Promise<T | TResult> => {
200
+ state.handled = true;
201
+ const childState = createContinuationState();
202
+ return observeBrowserRunPromiseWithState(
203
+ originalThen(undefined, recordContinuationFailure(onRejected, childState)),
204
+ owner,
205
+ onFloatingRejection,
206
+ childState,
207
+ );
208
+ },
209
+ },
210
+ finally: {
211
+ configurable: true,
212
+ value: (onFinally?: (() => void) | null): Promise<T> => {
213
+ state.handled = true;
214
+ const childState = createContinuationState();
215
+ return observeBrowserRunPromiseWithState(
216
+ originalFinally(recordContinuationFailure(onFinally, childState)),
217
+ owner,
218
+ onFloatingRejection,
219
+ childState,
220
+ );
221
+ },
222
+ },
223
+ });
224
+ return promise;
225
+ }
226
+
227
+ function createContinuationState(): ObservedPromiseState {
228
+ return { handled: false, userContinuationFailed: false };
229
+ }
230
+
231
+ function recordContinuationFailure<TArgs extends unknown[], TResult>(
232
+ continuation: ((...args: TArgs) => TResult | PromiseLike<TResult>) | null | undefined,
233
+ state: ObservedPromiseState,
234
+ ): ((...args: TArgs) => TResult | PromiseLike<TResult>) | null | undefined {
235
+ if (!continuation) return continuation;
236
+ return (...args) => {
237
+ try {
238
+ const result = continuation(...args);
239
+ if (!isThenable(result)) return result;
240
+ return Promise.resolve(result).catch(reason => {
241
+ state.userContinuationFailed = true;
242
+ throw reason;
243
+ }) as PromiseLike<TResult>;
244
+ } catch (reason) {
245
+ state.userContinuationFailed = true;
246
+ throw reason;
247
+ }
248
+ };
249
+ }
250
+
251
+ function isThenable(value: unknown): value is PromiseLike<unknown> {
252
+ if (value === null || (typeof value !== "object" && typeof value !== "function")) return false;
253
+ return typeof Reflect.get(value, "then") === "function";
254
+ }
255
+
256
+ function trackBrowserRunPromise<T>(
257
+ promise: Promise<T>,
258
+ owner?: object,
259
+ onFloatingRejection?: FloatingRejectionHandler,
260
+ ): Promise<T> {
261
+ if (!owner) return markHandled(promise);
262
+ const tracked = promise.catch(error => {
263
+ throw markBrowserRunRejection(error, owner);
264
+ });
265
+ return onFloatingRejection ? observeBrowserRunPromise(tracked, owner, onFloatingRejection) : tracked;
266
+ }
267
+
268
+ /**
269
+ * Installs worker-realm rejection routing. Consumed browser-run failures stay in
270
+ * the worker; unrelated failures retain the default fatal worker behavior.
271
+ */
272
+ export function installBrowserWorkerRejectionGuard(consume: (reason: unknown) => boolean): () => void {
273
+ const onRejection = (reason: unknown): void => {
274
+ if (postmortem.isExpectedCleanupError(reason) || consume(reason)) return;
275
+ setTimeout(() => {
276
+ throw reason;
277
+ }, 0);
278
+ };
279
+ process.on("unhandledRejection", onRejection);
280
+ return () => process.off("unhandledRejection", onRejection);
281
+ }
282
+
4
283
  /**
5
284
  * Marks a run-scoped promise as observed without changing its behavior for awaited callers.
6
285
  *
@@ -83,11 +362,16 @@ export function waitForRun(
83
362
  await untilAborted(signal, async () => await Bun.sleep(interval));
84
363
  }
85
364
  })();
86
- return markHandled(promise);
365
+ return trackBrowserRunPromise(promise);
87
366
  }
88
367
 
89
368
  /** Binds a long-lived scope facade (page/tab/desktop objects) to one evaluated run's abort signal. */
90
- export function bindRunFacade<T extends object>(target: T, signal: AbortSignal): T {
369
+ export function bindRunFacade<T extends object>(
370
+ target: T,
371
+ signal: AbortSignal,
372
+ rejectionOwner?: object,
373
+ onFloatingRejection?: FloatingRejectionHandler,
374
+ ): T {
91
375
  const cache = new Map<PropertyKey, unknown>();
92
376
  return new Proxy(target, {
93
377
  get(current, prop) {
@@ -102,11 +386,13 @@ export function bindRunFacade<T extends object>(target: T, signal: AbortSignal):
102
386
  if (result && typeof result === "object") {
103
387
  const then = Reflect.get(result, "then");
104
388
  if (typeof then === "function") {
105
- return markHandled(
389
+ return trackBrowserRunPromise(
106
390
  Promise.resolve(result).then(resolved => {
107
391
  throwIfAborted(signal);
108
392
  return resolved;
109
393
  }),
394
+ rejectionOwner,
395
+ onFloatingRejection,
110
396
  );
111
397
  }
112
398
  }
@@ -121,7 +407,7 @@ export function bindRunFacade<T extends object>(target: T, signal: AbortSignal):
121
407
  // brand-check internal slots that a Proxy cannot forward, and reading a
122
408
  // signal needs no abort gating anyway.
123
409
  if (value instanceof AbortSignal) return value;
124
- const wrapped = bindRunFacade(value, signal);
410
+ const wrapped = bindRunFacade(value, signal, rejectionOwner, onFloatingRejection);
125
411
  cache.set(prop, wrapped);
126
412
  return wrapped;
127
413
  }
@@ -83,25 +83,47 @@ export function tokenizeShellSegments(command: string): string[][] {
83
83
  }
84
84
 
85
85
  /**
86
- * Returns the original text of flat shell command segments. Unlike
86
+ * A flat shell command segment with the context needed to decide interception.
87
+ *
88
+ * @see extractFlatShellCommandSegments
89
+ */
90
+ export interface FlatShellCommandSegment {
91
+ /** Original segment text with quoting and escaping preserved. */
92
+ text: string;
93
+ /**
94
+ * True when this segment consumes the previous stage's stdout via an
95
+ * unquoted `|` or `|&`. Blank and comment-only continuation lines preserve
96
+ * the pending pipe state. Such a stage reads piped stdin, so path-based
97
+ * dedicated tools (read/grep/glob) cannot replace it. `||`, `;`, `&`, and
98
+ * `&&` start an independent command and leave this false.
99
+ */
100
+ pipedStdin: boolean;
101
+ }
102
+
103
+ /**
104
+ * Returns the flat shell command segments with the original text of each. Unlike
87
105
  * `tokenizeShellSegments`, this preserves quoting and escaping so the results
88
- * are safe to match against user-configured regular expressions.
106
+ * are safe to match against user-configured regular expressions, and flags
107
+ * segments that receive piped stdin.
89
108
  *
90
109
  * The extractor deliberately declines to split syntax whose execution context
91
110
  * cannot be determined with this small scanner (heredocs, command substitution,
92
111
  * backticks, grouping, and malformed quoting). Callers must still check the
93
112
  * complete input in that case.
94
113
  */
95
- export function extractFlatShellCommandSegments(command: string): string[] {
96
- const segments: string[] = [];
114
+ export function extractFlatShellCommandSegments(command: string): FlatShellCommandSegment[] {
115
+ const segments: FlatShellCommandSegment[] = [];
97
116
  let segmentStart = 0;
98
117
  let inSingle = false;
99
118
  let inDouble = false;
100
119
  let atWordStart = true;
120
+ let currentPiped = false;
101
121
 
102
- const pushSegment = (end: number) => {
122
+ const pushSegment = (end: number): boolean => {
103
123
  const segment = command.slice(segmentStart, end).trim();
104
- if (segment.length > 0) segments.push(segment);
124
+ if (segment.length === 0) return false;
125
+ segments.push({ text: segment, pipedStdin: currentPiped });
126
+ return true;
105
127
  };
106
128
 
107
129
  for (let i = 0; i < command.length; i++) {
@@ -154,12 +176,14 @@ export function extractFlatShellCommandSegments(command: string): string[] {
154
176
  return [];
155
177
  }
156
178
  if (ch === "#" && atWordStart) {
157
- pushSegment(i);
179
+ const pushed = pushSegment(i);
158
180
  const newline = command.indexOf("\n", i + 1);
159
181
  if (newline === -1) return segments;
160
182
  i = newline;
161
183
  segmentStart = newline + 1;
162
184
  atWordStart = true;
185
+ // Preserve a pending pipe through a comment-only continuation.
186
+ if (pushed) currentPiped = false;
163
187
  continue;
164
188
  }
165
189
  const isRedirectionOperatorCharacter =
@@ -169,8 +193,13 @@ export function extractFlatShellCommandSegments(command: string): string[] {
169
193
  ? command[i - 1] === ">" || command[i - 1] === "<" || command[i + 1] === ">"
170
194
  : false;
171
195
  if ((ch === "\n" || ch === ";" || ch === "|" || ch === "&") && !isRedirectionOperatorCharacter) {
172
- pushSegment(i);
173
- if ((ch === "|" || ch === "&") && command[i + 1] === ch) i++;
196
+ const pushed = pushSegment(i);
197
+ const doubled = (ch === "|" || ch === "&") && command[i + 1] === ch;
198
+ const pipeStderr = ch === "|" && command[i + 1] === "&";
199
+ if (doubled || pipeStderr) i++;
200
+ // `|` and `|&` pipe into the next segment. Blank continuation
201
+ // lines preserve that pending state; all other operators reset it.
202
+ if (pushed || ch !== "\n") currentPiped = ch === "|" && !doubled;
174
203
  segmentStart = i + 1;
175
204
  atWordStart = true;
176
205
  continue;
@@ -1,5 +1,5 @@
1
1
  import { sanitizeText } from "@oh-my-pi/pi-utils";
2
- import type { Terminal as XtermTerminal } from "@xterm/headless";
2
+ import type { Terminal as XtermTerminal } from "@oh-my-pi/pi-utils/vterm";
3
3
 
4
4
  const RESET = "\x1b[0m";
5
5
  const SGR = /\x1b\[([0-9;]*)m/g;
package/src/tools/todo.ts CHANGED
@@ -4,7 +4,7 @@ import type { ToolExample } from "@oh-my-pi/pi-ai";
4
4
  import type { Component } from "@oh-my-pi/pi-tui";
5
5
  import { Text } from "@oh-my-pi/pi-tui";
6
6
  import { isRecord, prompt, sanitizeText } from "@oh-my-pi/pi-utils";
7
- import chalk from "chalk";
7
+ import chalk from "@oh-my-pi/pi-utils/chalk";
8
8
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
9
9
  import type { Theme } from "../modes/theme/theme";
10
10
  import todoDescription from "../prompts/tools/todo.md" with { type: "text" };
@@ -198,9 +198,10 @@ function parseChangelogContent(content: string): ChangelogEntry[] {
198
198
  }
199
199
 
200
200
  /**
201
- * Compare versions. Returns: -1 if v1 < v2, 0 if v1 === v2, 1 if v1 > v2
201
+ * Compare changelog entries by their parsed version parts.
202
+ * Returns: -1 if v1 < v2, 0 if v1 === v2, 1 if v1 > v2
202
203
  */
203
- export function compareVersions(v1: ChangelogEntry, v2: ChangelogEntry): number {
204
+ export function compareChangelogEntries(v1: ChangelogEntry, v2: ChangelogEntry): number {
204
205
  if (v1.major !== v2.major) return v1.major - v2.major;
205
206
  if (v1.minor !== v2.minor) return v1.minor - v2.minor;
206
207
  return v1.patch - v2.patch;
@@ -232,7 +233,7 @@ export function getNewEntries(entries: ChangelogEntry[], lastVersion: string): C
232
233
  return [];
233
234
  }
234
235
 
235
- return entries.filter(entry => compareVersions(entry, parsedLastVersion) > 0);
236
+ return entries.filter(entry => compareChangelogEntries(entry, parsedLastVersion) > 0);
236
237
  }
237
238
 
238
239
  /**
@@ -328,7 +329,7 @@ export async function resolveStartupChangelogForDisplay(options: {
328
329
  }
329
330
  if (options.mode === "hidden") {
330
331
  const currentVersion = parseChangelogVersion(options.currentVersion);
331
- if (currentVersion && compareVersions(currentVersion, parsedLastVersion) > 0) {
332
+ if (currentVersion && compareChangelogEntries(currentVersion, parsedLastVersion) > 0) {
332
333
  await writeLastChangelogVersion(options.currentVersion, options.agentDir);
333
334
  }
334
335
  return undefined;
@@ -259,6 +259,14 @@ async function readTextViaPowerShell(): Promise<string | null> {
259
259
  }
260
260
  }
261
261
 
262
+ async function readTextFromX11Clipboard(): Promise<string> {
263
+ try {
264
+ return await spawnCapture(["xclip", "-selection", "clipboard", "-o"]);
265
+ } catch {
266
+ return await spawnCapture(["xsel", "--clipboard", "--output"]);
267
+ }
268
+ }
269
+
262
270
  /**
263
271
  * Read an image from the system clipboard.
264
272
  *
@@ -340,11 +348,11 @@ export async function readTextFromClipboard(): Promise<string> {
340
348
  return await spawnCapture(["wl-paste", "--type", "text/plain", "--no-newline"]);
341
349
  } catch {
342
350
  if (hasX11Display) {
343
- return await spawnCapture(["xclip", "-selection", "clipboard", "-o"]);
351
+ return await readTextFromX11Clipboard();
344
352
  }
345
353
  }
346
354
  } else if (hasX11Display) {
347
- return await spawnCapture(["xclip", "-selection", "clipboard", "-o"]);
355
+ return await readTextFromX11Clipboard();
348
356
  }
349
357
  } catch (error) {
350
358
  logger.warn("clipboard: failed to read clipboard text", { error: String(error) });
@@ -53,8 +53,10 @@ export async function openInEditor(
53
53
 
54
54
  const [editor, ...editorArgs] = editorCmd.split(" ");
55
55
  const stdio = options?.stdio ?? ["inherit", "inherit", "inherit"];
56
-
57
- const child = spawn(editor, [...editorArgs, tmpFile], { stdio, shell: process.platform === "win32" });
56
+ const child =
57
+ process.platform === "win32"
58
+ ? spawn(editor, [...editorArgs, tmpFile], { stdio, shell: true })
59
+ : spawn("/bin/sh", ["-c", `${editorCmd} "$1"`, "sh", tmpFile], { stdio });
58
60
  const { promise, reject, resolve } = Promise.withResolvers<number>();
59
61
  child.once("exit", (code, signal) => resolve(code ?? (signal ? -1 : 0)));
60
62
  child.once("error", error => reject(error));
package/src/utils/jj.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { $which } from "@oh-my-pi/pi-utils";
4
- import { LRUCache } from "lru-cache/raw";
4
+ import { LRUCache } from "@oh-my-pi/pi-utils/lru";
5
5
  import { withTimeoutSignal } from "./fetch-timeout";
6
6
  import * as git from "./git";
7
7
 
@@ -0,0 +1,17 @@
1
+ import { logger } from "@oh-my-pi/pi-utils";
2
+
3
+ const pendingCleanups = new Set<Promise<void>>();
4
+
5
+ /** Keep timed-out cleanup reachable until its resources really settle. */
6
+ export function trackLateCleanup(work: Promise<void>, context: Record<string, unknown>): void {
7
+ let tracked: Promise<void>;
8
+ tracked = work
9
+ .catch(error => {
10
+ logger.warn("Deferred cleanup failed", {
11
+ ...context,
12
+ error: error instanceof Error ? error.message : String(error),
13
+ });
14
+ })
15
+ .finally(() => pendingCleanups.delete(tracked));
16
+ pendingCleanups.add(tracked);
17
+ }
@@ -1,5 +1,4 @@
1
- import TurndownService from "turndown";
2
- import { gfm } from "turndown-plugin-gfm";
1
+ import TurndownService, { gfm } from "@oh-my-pi/pi-utils/turndown";
3
2
 
4
3
  type TurndownListParent = {
5
4
  nodeName: string;