fix(typing): keep tool-start ttl mode-safe (#452, thanks @thesash)

This commit is contained in:
Peter Steinberger
2026-01-08 06:18:11 +00:00
parent 29c5ed54b2
commit 6a81652ebf
6 changed files with 45 additions and 4 deletions

View File

@@ -2,14 +2,18 @@ import { vi } from "vitest";
import type { TypingController } from "./typing.js";
export function createMockTypingController(): TypingController {
export function createMockTypingController(
overrides: Partial<TypingController> = {},
): TypingController {
return {
onReplyStart: vi.fn(async () => {}),
startTypingLoop: vi.fn(async () => {}),
startTypingOnText: vi.fn(async () => {}),
refreshTypingTtl: vi.fn(),
isActive: vi.fn(() => false),
markRunComplete: vi.fn(),
markDispatchIdle: vi.fn(),
cleanup: vi.fn(),
...overrides,
};
}

View File

@@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { createMockTypingController } from "./test-helpers.js";
import { createTypingSignaler, resolveTypingMode } from "./typing-mode.js";
@@ -123,6 +123,36 @@ describe("createTypingSignaler", () => {
expect(typing.startTypingOnText).not.toHaveBeenCalled();
});
it("does not start typing on tool start when inactive", async () => {
const typing = createMockTypingController();
const signaler = createTypingSignaler({
typing,
mode: "message",
isHeartbeat: false,
});
await signaler.signalToolStart();
expect(typing.refreshTypingTtl).not.toHaveBeenCalled();
expect(typing.startTypingLoop).not.toHaveBeenCalled();
});
it("refreshes ttl on tool start when active", async () => {
const typing = createMockTypingController({
isActive: vi.fn(() => true),
});
const signaler = createTypingSignaler({
typing,
mode: "message",
isHeartbeat: false,
});
await signaler.signalToolStart();
expect(typing.refreshTypingTtl).toHaveBeenCalled();
expect(typing.startTypingLoop).not.toHaveBeenCalled();
});
it("suppresses typing when disabled", async () => {
const typing = createMockTypingController();
const signaler = createTypingSignaler({

View File

@@ -68,8 +68,9 @@ export function createTypingSignaler(params: {
const signalToolStart = async () => {
if (disabled) return;
// Keep typing indicator alive during tool execution
await typing.startTypingLoop();
if (!typing.isActive()) return;
// Keep typing indicator alive during tool execution without changing mode semantics.
typing.refreshTypingTtl();
};
return {

View File

@@ -3,6 +3,7 @@ export type TypingController = {
startTypingLoop: () => Promise<void>;
startTypingOnText: (text?: string) => Promise<void>;
refreshTypingTtl: () => void;
isActive: () => boolean;
markRunComplete: () => void;
markDispatchIdle: () => void;
cleanup: () => void;
@@ -76,6 +77,8 @@ export function createTypingController(params: {
}, typingTtlMs);
};
const isActive = () => active && !sealed;
const triggerTyping = async () => {
if (sealed) return;
await onReplyStart?.();
@@ -138,6 +141,7 @@ export function createTypingController(params: {
startTypingLoop,
startTypingOnText,
refreshTypingTtl,
isActive,
markRunComplete,
markDispatchIdle,
cleanup,