Merge PR #6396: fix(tts): preserve playback across internal turns (@roboomp)

This commit is contained in:
can1357
2026-07-24 02:24:04 +02:00
3 changed files with 30 additions and 6 deletions
+1
View File
@@ -53,6 +53,7 @@
- Fixed PDF image reads returning stale extractions after same-path content replacement and racing concurrent cold reads by binding each cache generation to immutable source bytes and coalescing extraction with independent caller cancellation. ([#6368](https://github.com/can1357/oh-my-pi/issues/6368))
- Fixed direct edit calls failing with `File not found` for relative paths that `read` resolved to a unique nested workspace file. ([#6359](https://github.com/can1357/oh-my-pi/issues/6359))
- Fixed concurrent dead-kernel replacement in persistent Python sessions starting multiple generations and orphaning the losing process; callers now share one generation-scoped replacement, while resets and disposal invalidate and drain stale replacements ([#6367](https://github.com/can1357/oh-my-pi/issues/6367)).
- Fixed Assistant-mode TTS playback aborting when an agent continued after a tool call or automatic follow-up in the same run; internal continuation boundaries now preserve queued speech, while a new user message still interrupts prior playback. ([#6375](https://github.com/can1357/oh-my-pi/issues/6375))
- Fixed credential-shaped tokens (GitHub/GitLab/OpenAI/Anthropic key patterns) being redacted from outbound provider requests even with `secrets.enabled` off; the pattern redaction now follows the `secrets.enabled` ("Hide Secrets") setting like the secret obfuscator.
- Fixed a cancelled `lsp reload` reporting `Restarted <server>` while killing the server with no replacement: `reloadServer` swallowed `ToolAbortError`/tool timeout in both fallback `catch` blocks and fell through to `proc.kill()`. Cancellation and timeout now propagate; the rust-analyzer fallback only triggers on genuine method-not-found; and a wedged-connection teardown removes the client by identity and awaits confirmed process exit before claiming a restart (surfacing a truthful teardown error if the process outlives the kill). ([#6369](https://github.com/can1357/oh-my-pi/issues/6369))
- Fixed Ctrl-clicking a wrapped OAuth authorization URL opening only the clicked row's truncated fragment by preserving the complete hyperlink target on every rendered row.
@@ -163,7 +163,7 @@ export class EventController {
this.#handlers = {
agent_start: e => this.#handleAgentStart(e),
agent_end: e => this.#handleAgentEnd(e),
turn_start: async () => this.#handleTurnStart(),
turn_start: async () => {},
turn_end: async e => this.#handleTurnEnd(e),
message_start: e => this.#handleMessageStart(e),
message_update: e => this.#handleMessageUpdate(e),
@@ -466,6 +466,7 @@ export class EventController {
}
this.ctx.ui.requestRender();
} else if (event.message.role === "user") {
vocalizer.clear();
const textContent = this.ctx.getUserMessageText(event.message);
const imageBlocks =
typeof event.message.content === "string"
@@ -661,11 +662,6 @@ export class EventController {
}
}
/** A new turn interrupts any speech still queued/playing from the previous one. */
#handleTurnStart(): void {
vocalizer.clear();
}
/**
* Speak streamed assistant output as a side effect of the turn. The mode
* decides which deltas feed the vocalizer (the vocalizer re-checks enabled):
@@ -4,6 +4,7 @@ import { EventController } from "@oh-my-pi/pi-coding-agent/modes/controllers/eve
import { initTheme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
import type { InteractiveModeContext } from "@oh-my-pi/pi-coding-agent/modes/types";
import type { AgentSessionEvent } from "@oh-my-pi/pi-coding-agent/session/agent-session";
import { vocalizer } from "@oh-my-pi/pi-coding-agent/tts/vocalizer";
function createContext() {
const setWorkingMessage = vi.fn();
@@ -21,6 +22,11 @@ function createContext() {
transcriptMessageComponents: new WeakMap(),
pendingTools,
hideThinkingBlock: false,
getUserMessageText: () => "new prompt",
locallySubmittedUserSignatures: new Set<string>(),
addMessageToChat: vi.fn(),
editor: { setText: vi.fn() },
updatePendingMessagesDisplay: vi.fn(),
setWorkingMessage,
clearPinnedError: vi.fn(),
ensureLoadingAnimation,
@@ -61,6 +67,27 @@ describe("EventController aborted-turn working messages", () => {
resetSettingsForTest();
});
it("preserves playback across internal continuations and clears it for a user message", async () => {
const clear = vi.spyOn(vocalizer, "clear").mockImplementation(() => {});
const { ctx } = createContext();
const controller = new EventController(ctx);
await controller.handleEvent(AGENT_START);
await controller.handleEvent({ type: "turn_start" });
expect(clear).not.toHaveBeenCalled();
await controller.handleEvent({
type: "message_start",
message: {
role: "user",
content: [{ type: "text", text: "new prompt" }],
attribution: "user",
timestamp: Date.now(),
},
});
expect(clear).toHaveBeenCalledTimes(1);
});
it("suppresses late intent-driven working-message updates while aborting", async () => {
const { ctx, pendingTools, setWorkingMessage, session } = createContext();
const controller = new EventController(ctx);