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

@@ -19,6 +19,7 @@
- CLI: remove `update`, `gateway-daemon`, `gateway {install|uninstall|start|stop|restart|daemon status|wake|send|agent}`, and `telegram` commands; use `daemon` for service control, `send`/`agent`/`wake` for RPC, and `nodes canvas` for canvas ops.
### Fixes
- Auto-reply: keep typing indicators alive during tool execution without changing typing-mode semantics. Thanks @thesash for PR #452.
- macOS: harden Voice Wake tester/runtime (pause trigger, mic persistence, local-only tester) and keep transcript logs private. Thanks @xadenryan for PR #438.
- macOS: preserve node bridge tunnel port override so remote nodes connect on the bridge port. Thanks @sircrumpet for PR #364.
- Doctor/Daemon: surface gateway runtime state + port collision diagnostics; warn on legacy workspace dirs.

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,

View File

@@ -330,6 +330,7 @@ describe("typing controller idle", () => {
startTypingLoop: vi.fn(async () => {}),
startTypingOnText: vi.fn(async () => {}),
refreshTypingTtl: vi.fn(),
isActive: vi.fn(() => false),
markRunComplete: vi.fn(),
markDispatchIdle,
cleanup: vi.fn(),