fix(typing): keep tool-start ttl mode-safe (#452, thanks @thesash)
This commit is contained in:
@@ -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.
|
- 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
|
### 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: 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.
|
- 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.
|
- Doctor/Daemon: surface gateway runtime state + port collision diagnostics; warn on legacy workspace dirs.
|
||||||
|
|||||||
@@ -2,14 +2,18 @@ import { vi } from "vitest";
|
|||||||
|
|
||||||
import type { TypingController } from "./typing.js";
|
import type { TypingController } from "./typing.js";
|
||||||
|
|
||||||
export function createMockTypingController(): TypingController {
|
export function createMockTypingController(
|
||||||
|
overrides: Partial<TypingController> = {},
|
||||||
|
): TypingController {
|
||||||
return {
|
return {
|
||||||
onReplyStart: vi.fn(async () => {}),
|
onReplyStart: vi.fn(async () => {}),
|
||||||
startTypingLoop: vi.fn(async () => {}),
|
startTypingLoop: vi.fn(async () => {}),
|
||||||
startTypingOnText: vi.fn(async () => {}),
|
startTypingOnText: vi.fn(async () => {}),
|
||||||
refreshTypingTtl: vi.fn(),
|
refreshTypingTtl: vi.fn(),
|
||||||
|
isActive: vi.fn(() => false),
|
||||||
markRunComplete: vi.fn(),
|
markRunComplete: vi.fn(),
|
||||||
markDispatchIdle: vi.fn(),
|
markDispatchIdle: vi.fn(),
|
||||||
cleanup: vi.fn(),
|
cleanup: vi.fn(),
|
||||||
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { createMockTypingController } from "./test-helpers.js";
|
import { createMockTypingController } from "./test-helpers.js";
|
||||||
import { createTypingSignaler, resolveTypingMode } from "./typing-mode.js";
|
import { createTypingSignaler, resolveTypingMode } from "./typing-mode.js";
|
||||||
@@ -123,6 +123,36 @@ describe("createTypingSignaler", () => {
|
|||||||
expect(typing.startTypingOnText).not.toHaveBeenCalled();
|
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 () => {
|
it("suppresses typing when disabled", async () => {
|
||||||
const typing = createMockTypingController();
|
const typing = createMockTypingController();
|
||||||
const signaler = createTypingSignaler({
|
const signaler = createTypingSignaler({
|
||||||
|
|||||||
@@ -68,8 +68,9 @@ export function createTypingSignaler(params: {
|
|||||||
|
|
||||||
const signalToolStart = async () => {
|
const signalToolStart = async () => {
|
||||||
if (disabled) return;
|
if (disabled) return;
|
||||||
// Keep typing indicator alive during tool execution
|
if (!typing.isActive()) return;
|
||||||
await typing.startTypingLoop();
|
// Keep typing indicator alive during tool execution without changing mode semantics.
|
||||||
|
typing.refreshTypingTtl();
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export type TypingController = {
|
|||||||
startTypingLoop: () => Promise<void>;
|
startTypingLoop: () => Promise<void>;
|
||||||
startTypingOnText: (text?: string) => Promise<void>;
|
startTypingOnText: (text?: string) => Promise<void>;
|
||||||
refreshTypingTtl: () => void;
|
refreshTypingTtl: () => void;
|
||||||
|
isActive: () => boolean;
|
||||||
markRunComplete: () => void;
|
markRunComplete: () => void;
|
||||||
markDispatchIdle: () => void;
|
markDispatchIdle: () => void;
|
||||||
cleanup: () => void;
|
cleanup: () => void;
|
||||||
@@ -76,6 +77,8 @@ export function createTypingController(params: {
|
|||||||
}, typingTtlMs);
|
}, typingTtlMs);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isActive = () => active && !sealed;
|
||||||
|
|
||||||
const triggerTyping = async () => {
|
const triggerTyping = async () => {
|
||||||
if (sealed) return;
|
if (sealed) return;
|
||||||
await onReplyStart?.();
|
await onReplyStart?.();
|
||||||
@@ -138,6 +141,7 @@ export function createTypingController(params: {
|
|||||||
startTypingLoop,
|
startTypingLoop,
|
||||||
startTypingOnText,
|
startTypingOnText,
|
||||||
refreshTypingTtl,
|
refreshTypingTtl,
|
||||||
|
isActive,
|
||||||
markRunComplete,
|
markRunComplete,
|
||||||
markDispatchIdle,
|
markDispatchIdle,
|
||||||
cleanup,
|
cleanup,
|
||||||
|
|||||||
@@ -330,6 +330,7 @@ describe("typing controller idle", () => {
|
|||||||
startTypingLoop: vi.fn(async () => {}),
|
startTypingLoop: vi.fn(async () => {}),
|
||||||
startTypingOnText: vi.fn(async () => {}),
|
startTypingOnText: vi.fn(async () => {}),
|
||||||
refreshTypingTtl: vi.fn(),
|
refreshTypingTtl: vi.fn(),
|
||||||
|
isActive: vi.fn(() => false),
|
||||||
markRunComplete: vi.fn(),
|
markRunComplete: vi.fn(),
|
||||||
markDispatchIdle,
|
markDispatchIdle,
|
||||||
cleanup: vi.fn(),
|
cleanup: vi.fn(),
|
||||||
|
|||||||
Reference in New Issue
Block a user