Show a reply's steps, and catch up on a reply whose live push was missed
A reply that works through tools now shows its latest tool step under
"is thinking…" (not its closing note, which is the answer). While a reply
is due, the open conversation checks the session's latest messages every
few seconds (api/admin/bots/session-messages) and replays any it missed
as server:bot-message, so the thread, rail and unread all take it and the
thinking row can't stay up until a reload. MCP and custom tool steps read
in words ("Using Exa search (web search exa)").
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import type { BotMessageWithAttachments, WebSocketDataType } from "@/src/types";
|
||||
import postSessionApi from "../(functions)/post-session-api";
|
||||
|
||||
type Params = {
|
||||
session_id?: number | null;
|
||||
/**
|
||||
* The thread's last message is the user's, so a reply is due.
|
||||
*/
|
||||
awaiting_reply: boolean;
|
||||
/**
|
||||
* The thread's last message: only newer ones are replayed.
|
||||
*/
|
||||
last_message_id?: number | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* While a reply is due, checks the session's latest messages every
|
||||
* `AppData.BotReplyCatchUpMs`. Replies arrive live; this only catches one
|
||||
* whose push was missed (the socket was down at the time), so the
|
||||
* thinking row can't stay up until a reload. A missed message is replayed
|
||||
* as the `server:bot-message` it would have been, so the thread, the rail
|
||||
* and the unread flag all take it the usual way.
|
||||
*/
|
||||
export default function useReplyCatchUp({ session_id, awaiting_reply, last_message_id }: Params) {
|
||||
const last_message = useRef(Number(last_message_id) || 0);
|
||||
|
||||
last_message.current = Number(last_message_id) || 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (!awaiting_reply || !session_id) return;
|
||||
|
||||
let stale = false;
|
||||
|
||||
const timer = setInterval(async () => {
|
||||
const res = await postSessionApi<BotMessageWithAttachments>({
|
||||
path: "session-messages",
|
||||
body: { session_id },
|
||||
}).catch(() => null);
|
||||
|
||||
if (stale || !res?.success || !Array.isArray(res.payload)) return;
|
||||
|
||||
const missed = (res.payload as BotMessageWithAttachments[]).filter(
|
||||
(message) => Number(message.id) > last_message.current,
|
||||
);
|
||||
|
||||
for (const message of missed) {
|
||||
const data: WebSocketDataType = { event: "server:bot-message", data: message };
|
||||
|
||||
window.dispatchEvent(new CustomEvent("wsDataEvent", { detail: { data, message: data } }));
|
||||
}
|
||||
}, AppData.BotReplyCatchUpMs);
|
||||
|
||||
return () => {
|
||||
stale = true;
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, [awaiting_reply, session_id]);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useRef, useState } from "react";
|
||||
import useServerEvent from "@/src/hooks/use-server-event";
|
||||
import type { BotJobUpdateData } from "@/src/types";
|
||||
|
||||
type Params = {
|
||||
bot_id: number;
|
||||
/**
|
||||
* The open session: steps of replies in other sessions are left out.
|
||||
*/
|
||||
session_id?: number | null;
|
||||
/**
|
||||
* The thread's last message. A step belongs to the reply due after it,
|
||||
* so a new message (the reply itself, or another from the user) hides
|
||||
* it.
|
||||
*/
|
||||
last_message_id?: number | null;
|
||||
};
|
||||
|
||||
type Step = {
|
||||
text: string;
|
||||
after_message_id: number | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* The latest step of the reply running in the open session (a
|
||||
* `bots_queue_updates` row's text, from `server:bot-job-update` without a
|
||||
* task), so a reply that works through tools shows what it's doing. Only
|
||||
* tool steps: the closing note is the answer itself.
|
||||
*/
|
||||
export default function useReplyProgress({ bot_id, session_id, last_message_id }: Params) {
|
||||
const [step, setStep] = useState<Step | null>(null);
|
||||
const last_message = useRef(last_message_id || null);
|
||||
|
||||
last_message.current = last_message_id || null;
|
||||
|
||||
useServerEvent({
|
||||
event: "server:bot-job-update",
|
||||
onData: (update: BotJobUpdateData) => {
|
||||
if (update.bot_id != bot_id || update.task_id || !update.text) return;
|
||||
|
||||
/**
|
||||
* A turn without tools ends the reply: its text is the answer,
|
||||
* about to land as the message.
|
||||
*/
|
||||
if (update.kind == "note") return;
|
||||
if (!session_id || update.session_id != session_id) return;
|
||||
|
||||
setStep({ text: update.text, after_message_id: last_message.current });
|
||||
},
|
||||
});
|
||||
|
||||
return step && step.after_message_id == (last_message_id || null) ? step.text : null;
|
||||
}
|
||||
Reference in New Issue
Block a user