diff --git a/CHANGELOG.md b/CHANGELOG.md index e619200ca..b8ffc68c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/auto-reply/reply/test-helpers.ts b/src/auto-reply/reply/test-helpers.ts index c9f49ac58..2bbef29ad 100644 --- a/src/auto-reply/reply/test-helpers.ts +++ b/src/auto-reply/reply/test-helpers.ts @@ -2,14 +2,18 @@ import { vi } from "vitest"; import type { TypingController } from "./typing.js"; -export function createMockTypingController(): TypingController { +export function createMockTypingController( + overrides: Partial = {}, +): 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, }; } diff --git a/src/auto-reply/reply/typing-mode.test.ts b/src/auto-reply/reply/typing-mode.test.ts index 9d3f06e1e..697b97af5 100644 --- a/src/auto-reply/reply/typing-mode.test.ts +++ b/src/auto-reply/reply/typing-mode.test.ts @@ -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({ diff --git a/src/auto-reply/reply/typing-mode.ts b/src/auto-reply/reply/typing-mode.ts index 839659c06..a445b6e2e 100644 --- a/src/auto-reply/reply/typing-mode.ts +++ b/src/auto-reply/reply/typing-mode.ts @@ -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 { diff --git a/src/auto-reply/reply/typing.ts b/src/auto-reply/reply/typing.ts index 7aba34c85..680163a92 100644 --- a/src/auto-reply/reply/typing.ts +++ b/src/auto-reply/reply/typing.ts @@ -3,6 +3,7 @@ export type TypingController = { startTypingLoop: () => Promise; startTypingOnText: (text?: string) => Promise; 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, diff --git a/src/web/auto-reply.test.ts b/src/web/auto-reply.test.ts index c5cf7544c..54e74c71c 100644 --- a/src/web/auto-reply.test.ts +++ b/src/web/auto-reply.test.ts @@ -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(),