diff --git a/CHANGELOG.md b/CHANGELOG.md index b59e1186d..18477ac74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - Tailscale Funnel now requires password auth (no token-only public exposure). - Group `/new` resets now work with @mentions so activation guidance appears on fresh sessions. - Group chat activation context is now injected into the system prompt at session start (and after activation changes), including /new greetings. +- Typing indicators now start only once a reply payload is produced (no "thinking" typing for silent runs). - Canvas defaults/A2UI auto-nav aligned; debug status overlay centered; redundant await removed in `CanvasManager`. - Gateway launchd loop fixed by removing redundant `kickstart -k`. - CLI now hints when Peekaboo is unauthorized. diff --git a/src/auto-reply/reply.ts b/src/auto-reply/reply.ts index f4f81a3f2..66239f3a7 100644 --- a/src/auto-reply/reply.ts +++ b/src/auto-reply/reply.ts @@ -201,11 +201,17 @@ export async function getReplyFromConfig( if (!opts?.onReplyStart) return; if (typingIntervalMs <= 0) return; if (typingTimer) return; - await triggerTyping(); + await onReplyStart(); typingTimer = setInterval(() => { void triggerTyping(); }, typingIntervalMs); }; + const startTypingOnText = async (text?: string) => { + const trimmed = text?.trim(); + if (!trimmed) return; + if (trimmed === SILENT_REPLY_TOKEN) return; + await startTypingLoop(); + }; let transcribedText: string | undefined; // Optional audio transcription before templating/session handling. @@ -646,8 +652,6 @@ export async function getReplyFromConfig( return { text: "⚙️ Agent was aborted." }; } - await startTypingLoop(); - const isFirstTurnInSession = isNewSession || !systemSent; const shouldInjectGroupIntro = sessionCtx.ChatType === "group" && @@ -865,8 +869,6 @@ export async function getReplyFromConfig( return undefined; } - await onReplyStart(); - try { const runId = crypto.randomUUID(); const runResult = await runEmbeddedPiAgent({ @@ -884,19 +886,23 @@ export async function getReplyFromConfig( timeoutMs, runId, onPartialReply: opts?.onPartialReply - ? (payload) => - opts.onPartialReply?.({ + ? async (payload) => { + await startTypingOnText(payload.text); + await opts.onPartialReply?.({ text: payload.text, mediaUrls: payload.mediaUrls, - }) + }); + } : undefined, shouldEmitToolResult, onToolResult: opts?.onToolResult - ? (payload) => - opts.onToolResult?.({ + ? async (payload) => { + await startTypingOnText(payload.text); + await opts.onToolResult?.({ text: payload.text, mediaUrls: payload.mediaUrls, - }) + }); + } : undefined, }); @@ -915,6 +921,16 @@ export async function getReplyFromConfig( const payloadArray = runResult.payloads ?? []; if (payloadArray.length === 0) return undefined; + const shouldSignalTyping = payloadArray.some((payload) => { + const trimmed = payload.text?.trim(); + if (trimmed && trimmed !== SILENT_REPLY_TOKEN) return true; + if (payload.mediaUrl) return true; + if (payload.mediaUrls && payload.mediaUrls.length > 0) return true; + return false; + }); + if (shouldSignalTyping) { + await onReplyStart(); + } if (sessionStore && sessionKey) { const usage = runResult.meta.agentMeta?.usage;