diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index a71029661..de39ebcd9 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## [Unreleased] + ### Added - Chunk read formatting: `anchorStyle` (full / kind / bare), `read.anchorstyle` setting, and `chunked` flag on file display mode @@ -25,6 +26,7 @@ ### Removed +- Removed `waitForChildProcess` utility (child process termination now handled by native `killTree` from pi-natives) - `grep-chunk.md` (folded into unified grep template) - `startMacAppearanceObserver` export (use `MacAppearanceObserver.start()`) - `copyToClipboard` export from pi-natives diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index 494ec6392..054e24e58 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -49,8 +49,8 @@ import { modelsAreEqual, parseRateLimitReason, } from "@oh-my-pi/pi-ai"; -import { MacOSPowerAssertion, type SearchDb } from "@oh-my-pi/pi-natives"; -import { abortableSleep, getAgentDbPath, isEnoent, logger } from "@oh-my-pi/pi-utils"; +import { killTree, MacOSPowerAssertion, type SearchDb } from "@oh-my-pi/pi-natives"; +import { abortableSleep, getAgentDbPath, isEnoent, logger, setNativeKillTree } from "@oh-my-pi/pi-utils"; import type { AsyncJob, AsyncJobManager } from "../async"; import type { Rule } from "../capability/rule"; import { MODEL_ROLE_IDS, type ModelRegistry } from "../config/model-registry"; @@ -535,6 +535,8 @@ export class AgentSession { } constructor(config: AgentSessionConfig) { + setNativeKillTree(killTree); + this.agent = config.agent; this.sessionManager = config.sessionManager; this.settings = config.settings; diff --git a/packages/coding-agent/src/utils/child-process.ts b/packages/coding-agent/src/utils/child-process.ts deleted file mode 100644 index 8610ab171..000000000 --- a/packages/coding-agent/src/utils/child-process.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { ChildProcess } from "node:child_process"; - -const EXIT_STDIO_GRACE_MS = 100; - -/** - * Wait for a child process to terminate without hanging on inherited stdio handles. - * - * Daemonized descendants can inherit the child's stdout/stderr pipe handles. In that - * case the child emits `exit`, but `close` can hang forever even though the original - * process is already gone. We wait briefly for stdio to end, then forcibly stop - * tracking the inherited handles. - */ -export function waitForChildProcess(child: ChildProcess): Promise { - const { promise, resolve, reject } = Promise.withResolvers(); - - let settled = false; - let exited = false; - let exitCode: number | null = null; - let postExitTimer: NodeJS.Timeout | undefined; - let stdoutEnded = child.stdout === null; - let stderrEnded = child.stderr === null; - - const cleanup = () => { - if (postExitTimer) { - clearTimeout(postExitTimer); - postExitTimer = undefined; - } - child.removeListener("error", onError); - child.removeListener("exit", onExit); - child.removeListener("close", onClose); - child.stdout?.removeListener("end", onStdoutEnd); - child.stderr?.removeListener("end", onStderrEnd); - }; - - const finalize = (code: number | null) => { - if (settled) return; - settled = true; - cleanup(); - child.stdout?.destroy(); - child.stderr?.destroy(); - resolve(code); - }; - - const maybeFinalizeAfterExit = () => { - if (!exited || settled) return; - if (stdoutEnded && stderrEnded) { - finalize(exitCode); - } - }; - - const onStdoutEnd = () => { - stdoutEnded = true; - maybeFinalizeAfterExit(); - }; - - const onStderrEnd = () => { - stderrEnded = true; - maybeFinalizeAfterExit(); - }; - - const onError = (err: Error) => { - if (settled) return; - settled = true; - cleanup(); - reject(err); - }; - - const onExit = (code: number | null) => { - exited = true; - exitCode = code; - maybeFinalizeAfterExit(); - if (!settled) { - postExitTimer = setTimeout(() => finalize(code), EXIT_STDIO_GRACE_MS); - } - }; - - const onClose = (code: number | null) => { - finalize(code); - }; - - child.stdout?.once("end", onStdoutEnd); - child.stderr?.once("end", onStderrEnd); - child.once("error", onError); - child.once("exit", onExit); - child.once("close", onClose); - - return promise; -} diff --git a/packages/natives/CHANGELOG.md b/packages/natives/CHANGELOG.md index 071007408..9d19c17de 100644 --- a/packages/natives/CHANGELOG.md +++ b/packages/natives/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## [Unreleased] + ### Breaking Changes - Moved package entry point from `src/index.ts` to `native/index.js` — consumers must update imports to use the new native module path @@ -16,12 +17,16 @@ ### Changed +- Updated native module loader to check `XDG_DATA_HOME` environment variable for native addon location before falling back to `~/.omp/natives` +- Removed native binding validation function that checked for required exports at load time - Refactored build pipeline to use napi-rs generated bindings instead of hand-written TypeScript wrappers - Updated `build-native.ts` to generate runtime enum exports after native compilation - Updated `embed-native.ts` to output JavaScript instead of TypeScript for embedded addon metadata ### Removed +- Removed inline pi-utils helpers and dependency on `@oh-my-pi/pi-utils` from native module loader +- Removed `logger.time()` wrapper calls from native module loading - Removed all TypeScript wrapper modules from `src/` directory (appearance, ast, chunk, clipboard, glob, grep, highlight, html, image, keys, projfs, ps, pty, shell, text, work) - Removed `src/bindings.ts` and `src/index.ts` entry points - Removed `src/search-db.ts` and `src/search-db-types.ts` diff --git a/packages/natives/native/index.js b/packages/natives/native/index.js index e41da5123..3cf5c7363 100644 --- a/packages/natives/native/index.js +++ b/packages/natives/native/index.js @@ -7,24 +7,17 @@ const fs = require("node:fs"); const { createRequire } = require("node:module"); const os = require("node:os"); const path = require("node:path"); -// Inline the two pi-utils helpers this loader needs (getNativesDir, logger.time). -// Pi-natives used to require('@oh-my-pi/pi-utils') at module top, but Bun's -// static resolver rejects that import when pi-natives is installed via a -// tarball where its workspace:* dep cannot be resolved next to this nested -// CJS file. Inlining removes the hard dep. + function getNativesDir() { + const xdgDataHome = process.env.XDG_DATA_HOME; + if (xdgDataHome && fs.existsSync(path.join(xdgDataHome, "omp"))) { + return path.join(xdgDataHome, "omp", "natives"); + } return path.join(os.homedir(), ".omp", "natives"); } -const logger = { - time(_label, fn, ...args) { - return fn(...args); - }, -}; const packageJson = require("../package.json"); const { embeddedAddon } = require("./embedded-addon"); -// NOTE: TypeScript types are omitted in JS version - const require_ = createRequire(__filename); const platformTag = `${process.platform}-${process.arch}`; const packageVersion = packageJson.version; @@ -189,7 +182,6 @@ function loadNative() { const bindings = logger.time(`native:loadNative:require:${path.basename(candidate)}`, () => require_(candidate), ); - validateNative(bindings, candidate); if (process.env.PI_DEV) { console.log(`Loaded native addon from ${candidate}`); } @@ -234,66 +226,7 @@ function loadNative() { throw new Error(`Failed to load pi_natives native addon for ${addonLabel}.\n\nTried:\n${details}\n\n${helpMessage}`); } -function validateNative(bindings, source) { - const missing = []; - const checkFn = name => { - if (typeof bindings[name] !== "function") { - missing.push(name); - } - }; - checkFn("copyToClipboard"); - checkFn("readImageFromClipboard"); - checkFn("ChunkState"); - checkFn("formatAnchor"); - checkFn("glob"); - checkFn("fuzzyFind"); - checkFn("grep"); - checkFn("search"); - checkFn("hasMatch"); - checkFn("htmlToMarkdown"); - checkFn("highlightCode"); - checkFn("supportsLanguage"); - checkFn("getSupportedLanguages"); - checkFn("truncateToWidth"); - checkFn("sanitizeText"); - checkFn("wrapTextWithAnsi"); - checkFn("sliceWithWidth"); - checkFn("extractSegments"); - checkFn("matchesKittySequence"); - checkFn("executeShell"); - checkFn("PtySession"); - checkFn("SearchDb"); - checkFn("Shell"); - checkFn("parseKey"); - checkFn("matchesLegacySequence"); - checkFn("parseKittySequence"); - checkFn("matchesKey"); - checkFn("visibleWidth"); - checkFn("killTree"); - checkFn("listDescendants"); - checkFn("getWorkProfile"); - checkFn("invalidateFsScanCache"); - checkFn("astGrep"); - checkFn("astEdit"); - checkFn("detectMacOSAppearance"); - checkFn("MacAppearanceObserver"); - checkFn("MacOSPowerAssertion"); - checkFn("projfsOverlayProbe"); - checkFn("projfsOverlayStart"); - checkFn("projfsOverlayStop"); - if (missing.length) { - throw new Error( - `Native addon missing exports (${source}). Missing: ${missing.join(", ")}. ` + - "Rebuild with `bun --cwd=packages/natives run build:native`.", - ); - } -} - -module.exports = logger.time("native:loadNative", () => loadNative()); - -// Side effect: register native killTree with pi-utils. -const { setNativeKillTree } = require("@oh-my-pi/pi-utils"); -setNativeKillTree(module.exports.killTree); +module.exports = loadNative(); // --- generated const enum exports (do not edit) --- exports.AstMatchStrictness = {