mirror of
https://github.com/stablyai/orca.git
synced 2026-09-25 08:02:31 +00:00
* fix(native-chat): stop rendering tool output as the agent's streaming reply A tool result could appear in native chat as a raw, un-collapsed "assistant" bubble that never went away for the rest of the turn — on mobile it showed up as a wall of a source file's contents, prefixed by "Exit code 1". Providers publish a tool's stdout/error as `lastAssistantMessage` so status cards and dashboard rows can preview what the agent just did. Native chat reuses that same field as its live streaming bubble, so the preview rendered as prose. For Claude the preview is *only ever* tool output mid-turn: claude-tool-fields writes real prose exclusively at Stop, so the bubble could never contain an actual streaming reply. It also could not be retired. The bubble hides once a transcript assistant block leads with the streamed text, and tool output never lands in one — so the only remaining exit was the turn ending, which is why a long tool-heavy turn pinned it on screen. Carry provenance instead of changing what the status surfaces show: mark the writes that come from a tool result/error, keep the flag in lockstep with the value it describes through the listener merge, and have both native-chat streaming paths ignore a flagged preview. Status cards, dashboard rows and automation capture are untouched. The wire field is optional, so an older host that never sends it keeps today's behavior rather than silently suppressing previews. * fix(native-chat): preserve tool output provenance through renderer sync * fix(native-chat): retain preview provenance in Claude roster state * test(native-chat): cover restored tool preview provenance --------- Co-authored-by: Merge Sim <sim@local>
70 lines
2.6 KiB
TypeScript
70 lines
2.6 KiB
TypeScript
// Shared derivation for the in-flight "streaming" assistant bubble. While an
|
|
// agent works, its hook preview (lastAssistantMessage) is shown as a synthetic
|
|
// assistant message so the user sees the reply build in real time, before the
|
|
// completed turn is flushed to the transcript. Desktop and mobile both use this
|
|
// so the show/hide rule can't drift between platforms.
|
|
|
|
import type { NativeChatMessage } from './native-chat-types'
|
|
|
|
/** The synthetic streaming bubble's stable id (kept stable so the list keys it
|
|
* consistently across ticks and the real turn can replace it cleanly). */
|
|
export const NATIVE_CHAT_STREAMING_ID = 'streaming'
|
|
|
|
/** Concatenated text of an assistant message's text blocks, trimmed. */
|
|
function assistantText(message: NativeChatMessage | undefined): string {
|
|
if (!message || message.role !== 'assistant') {
|
|
return ''
|
|
}
|
|
return message.blocks
|
|
.filter((b) => b.type === 'text')
|
|
.map((b) => (b.type === 'text' ? b.text : ''))
|
|
.join('')
|
|
.trim()
|
|
}
|
|
|
|
/**
|
|
* Decide the streaming text to show, or null to show nothing. Returns the
|
|
* preview only while it leads the transcript — i.e. it's longer than (and not
|
|
* already contained in) the last assistant turn. Once the real turn lands with
|
|
* the same (or more) text, the preview is suppressed so the bubble doesn't
|
|
* duplicate or flicker as the transcript catches up.
|
|
*
|
|
* `working` gates it: a stale preview from a finished turn never shows.
|
|
*
|
|
* `previewIsToolOutput` hard-gates it: several providers publish a tool's stdout or
|
|
* error as `lastAssistantMessage` so status cards can preview it. That text is not the
|
|
* reply, and it never appears in a transcript assistant block — so the catch-up rules
|
|
* below can never retire it and it would sit in the chat until the turn ended.
|
|
*/
|
|
export function deriveNativeChatStreamingText(args: {
|
|
messages: readonly NativeChatMessage[]
|
|
previewText: string | null | undefined
|
|
working: boolean
|
|
previewIsToolOutput?: boolean
|
|
}): string | null {
|
|
const { messages, previewText, working, previewIsToolOutput } = args
|
|
if (!working || previewIsToolOutput) {
|
|
return null
|
|
}
|
|
const text = previewText?.trim()
|
|
if (!text) {
|
|
return null
|
|
}
|
|
const lastText = assistantText(messages.at(-1))
|
|
if (lastText.includes(text) || text.length <= lastText.length) {
|
|
return null
|
|
}
|
|
return text
|
|
}
|
|
|
|
/** Build the synthetic streaming assistant message for the given text. */
|
|
export function nativeChatStreamingMessage(text: string): NativeChatMessage {
|
|
return {
|
|
id: NATIVE_CHAT_STREAMING_ID,
|
|
role: 'assistant',
|
|
blocks: [{ type: 'text', text }],
|
|
timestamp: null,
|
|
source: 'hook'
|
|
}
|
|
}
|