fix: delay typing until reply payload

main
Peter Steinberger 2025-12-23 13:55:01 +00:00
parent cba12a1abd
commit 863d26558a
2 changed files with 28 additions and 11 deletions

View File

@ -17,6 +17,7 @@
- Tailscale Funnel now requires password auth (no token-only public exposure). - 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 `/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. - 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`. - Canvas defaults/A2UI auto-nav aligned; debug status overlay centered; redundant await removed in `CanvasManager`.
- Gateway launchd loop fixed by removing redundant `kickstart -k`. - Gateway launchd loop fixed by removing redundant `kickstart -k`.
- CLI now hints when Peekaboo is unauthorized. - CLI now hints when Peekaboo is unauthorized.

View File

@ -201,11 +201,17 @@ export async function getReplyFromConfig(
if (!opts?.onReplyStart) return; if (!opts?.onReplyStart) return;
if (typingIntervalMs <= 0) return; if (typingIntervalMs <= 0) return;
if (typingTimer) return; if (typingTimer) return;
await triggerTyping(); await onReplyStart();
typingTimer = setInterval(() => { typingTimer = setInterval(() => {
void triggerTyping(); void triggerTyping();
}, typingIntervalMs); }, 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; let transcribedText: string | undefined;
// Optional audio transcription before templating/session handling. // Optional audio transcription before templating/session handling.
@ -646,8 +652,6 @@ export async function getReplyFromConfig(
return { text: "⚙️ Agent was aborted." }; return { text: "⚙️ Agent was aborted." };
} }
await startTypingLoop();
const isFirstTurnInSession = isNewSession || !systemSent; const isFirstTurnInSession = isNewSession || !systemSent;
const shouldInjectGroupIntro = const shouldInjectGroupIntro =
sessionCtx.ChatType === "group" && sessionCtx.ChatType === "group" &&
@ -865,8 +869,6 @@ export async function getReplyFromConfig(
return undefined; return undefined;
} }
await onReplyStart();
try { try {
const runId = crypto.randomUUID(); const runId = crypto.randomUUID();
const runResult = await runEmbeddedPiAgent({ const runResult = await runEmbeddedPiAgent({
@ -884,19 +886,23 @@ export async function getReplyFromConfig(
timeoutMs, timeoutMs,
runId, runId,
onPartialReply: opts?.onPartialReply onPartialReply: opts?.onPartialReply
? (payload) => ? async (payload) => {
opts.onPartialReply?.({ await startTypingOnText(payload.text);
await opts.onPartialReply?.({
text: payload.text, text: payload.text,
mediaUrls: payload.mediaUrls, mediaUrls: payload.mediaUrls,
}) });
}
: undefined, : undefined,
shouldEmitToolResult, shouldEmitToolResult,
onToolResult: opts?.onToolResult onToolResult: opts?.onToolResult
? (payload) => ? async (payload) => {
opts.onToolResult?.({ await startTypingOnText(payload.text);
await opts.onToolResult?.({
text: payload.text, text: payload.text,
mediaUrls: payload.mediaUrls, mediaUrls: payload.mediaUrls,
}) });
}
: undefined, : undefined,
}); });
@ -915,6 +921,16 @@ export async function getReplyFromConfig(
const payloadArray = runResult.payloads ?? []; const payloadArray = runResult.payloads ?? [];
if (payloadArray.length === 0) return undefined; 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) { if (sessionStore && sessionKey) {
const usage = runResult.meta.agentMeta?.usage; const usage = runResult.meta.agentMeta?.usage;