fix(tts): prioritized playback interruption on escape
- Moved active playback cancellation ahead of ongoing loop and stream interruption. - Added an overlapping TTS and agent-turn regression. Fixes #6118
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed Esc aborting an ongoing agent turn instead of overlapping TTS playback, leaving speech uninterruptible ([#6118](https://github.com/can1357/oh-my-pi/issues/6118)).
|
||||
|
||||
## [17.0.5] - 2026-07-18
|
||||
|
||||
### Added
|
||||
|
||||
@@ -308,6 +308,14 @@ export class InputController {
|
||||
if (aborted) return;
|
||||
}
|
||||
|
||||
if (vocalizer.isSpeaking()) {
|
||||
// Playback from the completed response can overlap the next agent
|
||||
// turn. Silence it before interrupting any ongoing main-turn work.
|
||||
vocalizer.clear();
|
||||
this.ctx.lastEscapeTime = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.ctx.loopModeEnabled) {
|
||||
this.ctx.pauseLoop();
|
||||
if (this.ctx.session.isStreaming) {
|
||||
@@ -360,13 +368,6 @@ export class InputController {
|
||||
} else if (this.ctx.editor.getText().trim()) {
|
||||
// Esc must not destroy an in-progress draft.
|
||||
this.ctx.lastEscapeTime = 0;
|
||||
} else if (vocalizer.isSpeaking()) {
|
||||
// TTS buffers seconds of PCM past the streaming abort, so an Esc
|
||||
// arriving after the model stopped would otherwise fall through to
|
||||
// the double-Esc gesture while Kokoro reads on. Silence first;
|
||||
// tree/branch stays reachable via a second Esc.
|
||||
vocalizer.clear();
|
||||
this.ctx.lastEscapeTime = 0;
|
||||
} else {
|
||||
// Double-interrupt with empty editor triggers /tree, /branch, or nothing based on setting
|
||||
const action = settings.get("doubleEscapeAction");
|
||||
|
||||
@@ -47,7 +47,7 @@ For independent per-item chains (review → verify, fetch → extract → score)
|
||||
schema: FINDINGS_SCHEMA,
|
||||
});
|
||||
return await parallel(found.findings.map((f) => async () => ({
|
||||
...f,
|
||||
…f,
|
||||
verdict: await agent(
|
||||
`Refute if you can (default refuted when unsure): ${f.title}`,
|
||||
{ label: `verify:${f.file}`, schema: VERDICT_SCHEMA },
|
||||
@@ -57,8 +57,6 @@ For independent per-item chains (review → verify, fetch → extract → score)
|
||||
phase("Review");
|
||||
const results = await parallel(DIMENSIONS.map((d) => async () => reviewAndVerify(d)));
|
||||
const confirmed = results.flat().filter((f) => f.verdict.is_real);
|
||||
|
||||
|
||||
Reach for `pipeline()` only when a stage genuinely needs ALL of the previous stage first — dedup/merge across the whole set, early-exit on zero, or "compare against the other findings" — because its inter-stage barrier makes every item wait for the slowest peer:
|
||||
|
||||
**Python (`eval`, Python backend):**
|
||||
@@ -80,8 +78,6 @@ Reach for `pipeline()` only when a stage genuinely needs ALL of the previous sta
|
||||
const verdicts = await parallel(findings.map((f) => async () =>
|
||||
await agent(verifyPrompt(f), { schema: VERDICT_SCHEMA }),
|
||||
));
|
||||
|
||||
|
||||
Use ordinary code between calls to flatten/map/filter; don't add a barrier just for that. Nested `parallel()` pools each cap independently, so keep total fan-out sane.
|
||||
</structure>
|
||||
|
||||
|
||||
@@ -669,6 +669,24 @@ describe("InputController escape behavior", () => {
|
||||
expect(ctx.showUserMessageSelector).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("silences TTS before aborting an overlapping agent turn (#6118)", () => {
|
||||
const clear = vi.spyOn(vocalizer, "clear").mockImplementation(() => {});
|
||||
vi.spyOn(vocalizer, "isSpeaking").mockReturnValue(true);
|
||||
const { ctx, editor, spies } = createContext();
|
||||
const pauseLoop = vi.fn();
|
||||
ctx.loopModeEnabled = true;
|
||||
ctx.pauseLoop = pauseLoop;
|
||||
mutableSessionState(ctx).isStreaming = true;
|
||||
const controller = new InputController(ctx);
|
||||
|
||||
controller.setupKeyHandlers();
|
||||
editor.onEscape?.();
|
||||
|
||||
expect(clear).toHaveBeenCalledTimes(1);
|
||||
expect(pauseLoop).not.toHaveBeenCalled();
|
||||
expect(spies.abort).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("silences a still-audible vocalizer on Esc instead of opening the tree selector (#4521)", () => {
|
||||
const clear = vi.spyOn(vocalizer, "clear").mockImplementation(() => {});
|
||||
const isSpeaking = vi.spyOn(vocalizer, "isSpeaking").mockReturnValue(true);
|
||||
|
||||
Reference in New Issue
Block a user