fix(tui): suppressed tiny-title worker output
Stopped the tiny-title subprocess from inheriting stdout and stderr so native model runtime output cannot corrupt the interactive scrollback. Added a regression test for worker stdio configuration.\n\nFixes #2206
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed local tiny-title worker stdout/stderr leaking raw native model output such as `</title>` and cache/status lines into the interactive TUI scrollback ([#2206](https://github.com/can1357/oh-my-pi/issues/2206)).
|
||||
|
||||
## [15.10.9] - 2026-06-09
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -122,7 +122,7 @@ function tinyWorkerSpawnCmd(): string[] {
|
||||
}
|
||||
|
||||
interface SpawnedSubprocess {
|
||||
proc: Subprocess<"ignore", "inherit", "inherit">;
|
||||
proc: Subprocess<"ignore", "ignore", "ignore">;
|
||||
inbound: Set<(message: TinyTitleWorkerOutbound) => void>;
|
||||
errors: Set<(error: Error) => void>;
|
||||
/**
|
||||
@@ -147,10 +147,13 @@ export function createTinyTitleSubprocess(): SpawnedSubprocess {
|
||||
cmd: tinyWorkerSpawnCmd(),
|
||||
env: tinyWorkerEnv(),
|
||||
stdin: "ignore",
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
stdout: "ignore",
|
||||
stderr: "ignore",
|
||||
serialization: "advanced",
|
||||
windowsHide: true,
|
||||
// The worker is an implementation detail of the interactive TUI. Native
|
||||
// model runtimes may print progress or decoded text directly; never let
|
||||
// those bytes inherit the terminal and corrupt the chat scrollback.
|
||||
ipc(message) {
|
||||
for (const handler of inbound) handler(message as TinyTitleWorkerOutbound);
|
||||
},
|
||||
|
||||
@@ -20,12 +20,13 @@ import {
|
||||
TINY_TITLE_MODEL_OPTIONS,
|
||||
TINY_TITLE_MODEL_VALUES,
|
||||
} from "@oh-my-pi/pi-coding-agent/tiny/models";
|
||||
import { tinyTitleClient } from "@oh-my-pi/pi-coding-agent/tiny/title-client";
|
||||
import { createTinyTitleSubprocess, tinyTitleClient } from "@oh-my-pi/pi-coding-agent/tiny/title-client";
|
||||
import {
|
||||
generateSessionTitle,
|
||||
raceFirstNonNull,
|
||||
TITLE_LOCAL_FALLBACK_DELAY_MS,
|
||||
} from "@oh-my-pi/pi-coding-agent/utils/title-generator";
|
||||
import type { Subprocess } from "bun";
|
||||
|
||||
async function flushMicrotasks(turns = 4): Promise<void> {
|
||||
for (let i = 0; i < turns; i += 1) await Promise.resolve();
|
||||
@@ -60,6 +61,33 @@ function createRegistry(model: Model<Api>) {
|
||||
} as never;
|
||||
}
|
||||
|
||||
type TinyWorkerSpawnOptions = Bun.SpawnOptions.SpawnOptions<"ignore", "ignore", "ignore">;
|
||||
|
||||
type TinyWorkerSpawnCall = {
|
||||
options: TinyWorkerSpawnOptions & { cmd: string[] };
|
||||
};
|
||||
|
||||
function createTinyWorkerSpawnMock(calls: TinyWorkerSpawnCall[]) {
|
||||
function mockSpawn(options: TinyWorkerSpawnOptions & { cmd: string[] }): Subprocess<"ignore", "ignore", "ignore">;
|
||||
function mockSpawn(cmd: string[], options?: TinyWorkerSpawnOptions): Subprocess<"ignore", "ignore", "ignore">;
|
||||
function mockSpawn(
|
||||
first: string[] | (TinyWorkerSpawnOptions & { cmd: string[] }),
|
||||
second?: TinyWorkerSpawnOptions,
|
||||
): Subprocess<"ignore", "ignore", "ignore"> {
|
||||
const options = Array.isArray(first) ? { ...(second ?? {}), cmd: first } : first;
|
||||
calls.push({ options });
|
||||
return {
|
||||
pid: 12345,
|
||||
send: () => undefined,
|
||||
kill: () => true,
|
||||
unref: () => undefined,
|
||||
exited: Promise.resolve(0),
|
||||
} as unknown as Subprocess<"ignore", "ignore", "ignore">;
|
||||
}
|
||||
|
||||
return mockSpawn;
|
||||
}
|
||||
|
||||
function mockOnlineTitle(title: string | null) {
|
||||
return vi.spyOn(ai, "completeSimple").mockResolvedValue({
|
||||
stopReason: "stop",
|
||||
@@ -285,6 +313,20 @@ describe("tiny title generator routing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("tiny title subprocess", () => {
|
||||
it("does not inherit worker output into the interactive terminal", async () => {
|
||||
const calls: TinyWorkerSpawnCall[] = [];
|
||||
vi.spyOn(Bun, "spawn").mockImplementation(createTinyWorkerSpawnMock(calls));
|
||||
|
||||
const worker = createTinyTitleSubprocess();
|
||||
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0]?.options.stdout).toBe("ignore");
|
||||
expect(calls[0]?.options.stderr).toBe("ignore");
|
||||
await worker.proc.exited;
|
||||
});
|
||||
});
|
||||
|
||||
describe("providers.tinyModel schema", () => {
|
||||
it("keeps enum values and UI options in sync with the tiny model registry", () => {
|
||||
expect(getEnumValues("providers.tinyModel")).toEqual([...TINY_TITLE_MODEL_VALUES]);
|
||||
|
||||
Reference in New Issue
Block a user