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:
2026-10-19 16:28:53 +01:00
co-authored by Claude Opus 5.5
parent 425ae723cf
commit 61098a114c
11 changed files with 238 additions and 11 deletions
+13 -2
View File
@@ -48,10 +48,21 @@ describe("describeJobStep", () => {
tool_calls: [
{ id: "a", name: "browser_navigate", arguments: "{}" },
{ id: "b", name: "browser_click", arguments: "{}" },
{ id: "c", name: "some_mcp_tool", arguments: "{}" },
{ id: "c", name: "get_weather", arguments: "{}" },
],
})?.text,
).toBe("Using the browser, Using some_mcp_tool");
).toBe("Using the browser, Using get weather");
});
test("names an MCP server's tool in words", () => {
expect(
describeJobStep({
index: 0,
text: null,
candidate,
tool_calls: [{ id: "a", name: "exa_search__web_search_exa", arguments: "{}" }],
})?.text,
).toBe("Using Exa search (web search exa)");
});
test("keeps the first line of a text-only turn, cut to one line", () => {
+17 -1
View File
@@ -57,5 +57,21 @@ function firstLine({ text }: { text: string | null }) {
function toolLabel({ name }: { name: string }) {
const prefix = Object.keys(JobStepToolPrefixLabels).find((p) => name.startsWith(p));
return JobStepToolLabels[name] || (prefix && JobStepToolPrefixLabels[prefix]) || `Using ${name}`;
return JobStepToolLabels[name] || (prefix && JobStepToolPrefixLabels[prefix]) || `Using ${readableToolName({ name })}`;
}
/**
* A tool name in words: `get_weather` reads "get weather", and an MCP
* server's `<slug>__<tool>` reads "Exa search (web search exa)".
*/
function readableToolName({ name }: { name: string }) {
const words = (part: string) => part.replace(/_+/g, " ").trim();
const [server, ...rest] = name.split("__");
const tool = words(rest.join(" "));
if (!tool) return words(name);
const server_words = words(server || "");
return `${server_words.charAt(0).toUpperCase()}${server_words.slice(1)} (${tool})`;
}