diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 3aa4adaa6..0db389807 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -18,6 +18,7 @@ - Fixed summarized `read` output stalling agents on elided regions by appending an explicit footer like `[NN lines across MM elided regions; read :raw or a line range like :1-9999 for verbatim content]`. The footer fires whenever the structural summarizer elided at least one span, so the model gets a concrete recovery selector instead of having to guess from a bare `...` / `{ .. }` marker. Surfaces `elidedLines` on `ReadToolDetails.summary` alongside the existing `elidedSpans`. ([#1046](https://github.com/can1357/oh-my-pi/issues/1046)) - Updated the `read` tool prompt to describe the new elision footer and instruct the model to follow `:raw` (or an explicit line range) when the elided body is actually needed, rather than guessing. - Fixed plugin extensions failing to load when their `peerDependencies` reference internal `pi-*` packages under any scope other than `@mariozechner` (e.g. `Cannot find module '@earendil-works/pi-tui'` from `@juicesharp/rpiv-ask-user-question`, or `Cannot find module '@oh-my-pi/pi-utils'` from `@oh-my-pi/swarm-extension`). The legacy-pi specifier shim now treats `@mariozechner`, `@earendil-works`, **and** the canonical `@oh-my-pi` itself as aliases for the same set of bundled in-process packages (`pi-agent-core`, `pi-ai`, `pi-coding-agent`, `pi-natives`, `pi-tui`, `pi-utils`), and additionally rewrites the upstream-only `pi-ai/oauth` subpath onto our `pi-ai/utils/oauth` layout. Restored the `Key` runtime helper export on `@oh-my-pi/pi-tui` to match upstream — plugins using `Key.enter` / `Key.ctrl("c")` (e.g. `@plannotator/pi-extension`, `@juicesharp/rpiv-ask-user-question`) no longer fail with `Export named 'Key' not found`. End-to-end verified against `@juicesharp/rpiv-ask-user-question`, `@oh-my-pi/swarm-extension`, and `@plannotator/pi-extension` — each now loads cleanly with all of its tools/commands/handlers registered. Plugins importing any of those scopes are remapped to the omp binary's own copy at load time, so peer deps are no longer dragged in from npm and there is exactly one module instance per package regardless of which scope name the plugin's manifest happened to declare. +- Fixed `omp commit` hanging after a successful commit instead of returning to the shell. The command now mirrors the `runPrintMode` exit pattern and calls `postmortem.quit(0)` once the pipeline resolves so lingering HTTP/2 keep-alive sockets, the Settings autosave timer, and other AgentSession background handles don't keep the event loop pinned. ([#1041](https://github.com/can1357/oh-my-pi/issues/1041)) ## [15.0.1] - 2026-05-14 ### Breaking Changes diff --git a/packages/coding-agent/src/commands/commit.ts b/packages/coding-agent/src/commands/commit.ts index 9e7bc2a35..428efd36f 100644 --- a/packages/coding-agent/src/commands/commit.ts +++ b/packages/coding-agent/src/commands/commit.ts @@ -1,6 +1,7 @@ /** * Generate and optionally push a commit with changelog updates. */ +import { postmortem } from "@oh-my-pi/pi-utils"; import { Command, Flags } from "@oh-my-pi/pi-utils/cli"; import { runCommitCommand } from "../commit"; import type { CommitCommandArgs } from "../commit/types"; @@ -31,6 +32,15 @@ export default class Commit extends Command { }; await initTheme(); + // The agentic commit flow opens HTTP/2 keep-alive sockets to the model + // provider (via `installH2Fetch`) and spins up an AgentSession with + // background async-job + extension machinery. `session.dispose()` releases + // what it can, but Bun's fetch keeps idle connections warm and a few + // timers (Settings autosave, OAuth refresh) stay armed long enough to + // pin the event loop after the commit is already written. Mirror the + // `runPrintMode` exit pattern from `main.ts` so the CLI returns to the + // shell instead of stranding the user on Ctrl+C (issue #1041). await runCommitCommand(cmd); + await postmortem.quit(0); } } diff --git a/packages/coding-agent/src/tools/bash.ts b/packages/coding-agent/src/tools/bash.ts index 1340589b5..3139224c3 100644 --- a/packages/coding-agent/src/tools/bash.ts +++ b/packages/coding-agent/src/tools/bash.ts @@ -292,7 +292,7 @@ export class BashTool implements AgentTool { #buildCompletedResult( result: BashResult | BashInteractiveResult, timeoutSec: number, - options: { requestedTimeoutSec?: number; notices?: string[]; terminalId?: string } = {}, + options: { requestedTimeoutSec?: number; notices?: readonly string[]; terminalId?: string } = {}, ): AgentToolResult { const outputLines = [this.#formatResultOutput(result)]; const notices = options.notices?.filter(Boolean) ?? []; @@ -315,7 +315,7 @@ export class BashTool implements AgentTool { label: string, previewText: string, timeoutSec: number, - options: { requestedTimeoutSec?: number; notices?: string[] } = {}, + options: { requestedTimeoutSec?: number; notices?: readonly string[] } = {}, ): AgentToolResult { const details: BashToolDetails = { timeoutSeconds: timeoutSec, diff --git a/packages/coding-agent/test/commit-command-exit.test.ts b/packages/coding-agent/test/commit-command-exit.test.ts new file mode 100644 index 000000000..5b2a88dc6 --- /dev/null +++ b/packages/coding-agent/test/commit-command-exit.test.ts @@ -0,0 +1,35 @@ +import { afterEach, describe, expect, it, vi } from "bun:test"; +import { postmortem } from "@oh-my-pi/pi-utils"; +import CommitCommand from "../src/commands/commit"; +import * as commitModule from "../src/commit"; +import * as themeModule from "../src/modes/theme/theme"; + +describe("omp commit command lifecycle (issue #1041)", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("forces process exit after the commit pipeline resolves", async () => { + const initThemeSpy = vi.spyOn(themeModule, "initTheme").mockResolvedValue(undefined); + const runCommitSpy = vi.spyOn(commitModule, "runCommitCommand").mockResolvedValue(undefined); + // Stub postmortem.quit so it records the exit code without actually + // terminating the test runner. Resolves immediately — the production + // implementation never returns, but the contract under test is that + // the call happens at all. + const quitSpy = vi.spyOn(postmortem, "quit").mockResolvedValue(undefined); + + const command = new CommitCommand([], { + bin: "omp", + version: "0.0.0-test", + commands: new Map(), + }); + + await command.run(); + + expect(initThemeSpy).toHaveBeenCalledTimes(1); + expect(runCommitSpy).toHaveBeenCalledTimes(1); + // Quit must come after the pipeline so we cannot regress the order. + expect(runCommitSpy.mock.invocationCallOrder[0]).toBeLessThan(quitSpy.mock.invocationCallOrder[0]); + expect(quitSpy).toHaveBeenCalledWith(0); + }); +});