Files
orca/src/preload/api/native-chat-api.ts
T
Brennan Benson 98bdd653ab fix(native-chat): stop the spinner on a not-yet-flushed transcript (#16493)
* fix(native-chat): stop the spinner on a not-yet-flushed transcript

A brand-new agent session can take minutes to write its first JSONL line,
and one that is never prompted never writes it at all. The host emitted no
stream frame until the file resolved, so every native-chat client sat on a
bare spinner with the composer enabled but the transcript blank -- forever,
in the never-prompted case.

The resolve poll now reports the transcript as pending after a short grace,
and both host handlers emit a `pending: true` snapshot. It is deliberately
not a plain empty snapshot: an empty window sold as a settled read would
capture over retained history and unblock consumers that require a
trustworthy transcript (the launch-draft adoption would re-offer a prompt
the agent may already have taken).

Clients render it as the "start a chat" empty state while keeping the read
unsettled -- `awaiting-transcript` on mobile, an `awaiting` read phase on
desktop, which also stops the seed loop expiring into an error card for a
session that is simply new. New optional field only, so older clients
ignore it and still stop spinning.

* fix(native-chat): negotiate pending transcript frames
2026-08-25 15:06:47 -07:00

75 lines
2.3 KiB
TypeScript

import type {
AgentType,
NativeChatMessage,
NativeChatTurnLifecycle
} from '../../shared/native-chat-types'
// notFound marks a not-yet-on-disk miss (retry-worthy) vs a real read/parse error (#8401).
export type NativeChatReadSessionResult =
| {
messages: NativeChatMessage[]
lifecycle?: NativeChatTurnLifecycle
}
| { error: string; notFound?: true }
/** Messages appended to a live-tailed transcript since the previous emit. */
export type NativeChatAppendedMessages = NativeChatMessage[]
export type NativeChatSubscriptionFrame =
| {
type: 'snapshot'
messages: NativeChatMessage[]
hasMore: boolean
error?: string
lifecycle?: NativeChatTurnLifecycle
/** No transcript exists behind this window yet — render it, but do not
* treat it as a settled read of the session's history. */
pending?: boolean
}
| {
type: 'replacement'
messages: NativeChatMessage[]
hasMore: boolean
lifecycle?: NativeChatTurnLifecycle
}
| {
type: 'appended'
messages: NativeChatMessage[]
lifecycle?: NativeChatTurnLifecycle
}
/** Wire payload for the `nativeChat:appended` push channel. */
export type NativeChatAppendedPayload = {
subscriptionId: string
frame: NativeChatSubscriptionFrame
}
export type NativeChatSubscribeArgs = {
/** Unique per-caller id, echoed on every append so multiple live panes in
* one renderer don't cross-talk. */
subscriptionId: string
agent: AgentType
sessionId: string
/** Authoritative transcript path from the agent hook (providerSession). */
transcriptPath?: string
/** First snapshot size; later readSession calls grow this for pagination. */
limit?: number
}
export type NativeChatApi = {
/** Read the on-disk transcript for an agent + session id, windowed to the most recent `limit`
* turns. `transcriptPath` is the hook-reported authoritative path, preferred over the id glob. */
readSession: (
agent: AgentType,
sessionId: string,
limit?: number,
transcriptPath?: string
) => Promise<NativeChatReadSessionResult>
/** Live-tail a transcript. The first frame is a bounded race-safe snapshot;
* later frames contain only newly appended messages. */
subscribe: (
args: NativeChatSubscribeArgs,
onFrame: (frame: NativeChatSubscriptionFrame) => void
) => () => void
}