Merge PR #6396: fix(tts): preserve playback across internal turns (@roboomp)
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user