mirror of
https://github.com/stablyai/orca.git
synced 2026-10-08 16:02:37 +00:00
fix(mobile): restore pending native chat transcripts
Restore the existing transcriptPending capability and preserve pending snapshots through the shared chat presentation seam. A new agent can show its empty chat while keeping draft reconciliation gated until the real transcript arrives. Exercise capability negotiation and the pending-to-ready transition through the native adapter and React hook. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
@@ -10,6 +10,7 @@ export type MobileNativeChatStreamFrame = {
|
||||
messages?: NativeChatMessage[]
|
||||
hasMore?: boolean
|
||||
beforeOffset?: number
|
||||
pending?: boolean
|
||||
error?: string
|
||||
message?: string
|
||||
lifecycle?: NativeChatTurnLifecycle
|
||||
@@ -26,6 +27,7 @@ export type AppliedMobileNativeChatFrame =
|
||||
cursorInvalidated?: boolean
|
||||
lifecycle?: NativeChatTurnLifecycle
|
||||
windowReplaced?: boolean
|
||||
pending?: boolean
|
||||
}
|
||||
|
||||
function replayRetainedTailStart(
|
||||
@@ -82,6 +84,7 @@ export function applyMobileNativeChatStreamFrame(args: {
|
||||
if (!Array.isArray(frame.messages)) {
|
||||
return { kind: 'ignored' }
|
||||
}
|
||||
const pending = frame.type === 'snapshot' && frame.pending === true
|
||||
const replayStartIndex =
|
||||
frame.type === 'snapshot' && !replaceSnapshot && merger.list.length > 0
|
||||
? replayRetainedTailStart(merger, frame.messages, frame.hasMore)
|
||||
@@ -93,6 +96,7 @@ export function applyMobileNativeChatStreamFrame(args: {
|
||||
messages: merger.list,
|
||||
hasMore: frame.hasMore,
|
||||
windowReplaced: true,
|
||||
...(pending ? { pending: true } : {}),
|
||||
...(frame.beforeOffset == null ? {} : { beforeOffset: frame.beforeOffset }),
|
||||
...(frame.lifecycle === undefined ? {} : { lifecycle: frame.lifecycle })
|
||||
}
|
||||
@@ -104,6 +108,7 @@ export function applyMobileNativeChatStreamFrame(args: {
|
||||
return {
|
||||
kind: 'messages',
|
||||
messages,
|
||||
...(pending ? { pending: true } : {}),
|
||||
// Why: once the bounded live window drops its oldest row, the snapshot's
|
||||
// byte cursor no longer describes the oldest retained message.
|
||||
...(cursorInvalidated ? { cursorInvalidated: true } : {}),
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { createElement } from 'react'
|
||||
import { act, create } from 'react-test-renderer'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import { nativeHostSessionNativeChatOperations } from './native-host-session-native-chat-operations'
|
||||
import {
|
||||
useMobileNativeChatSession,
|
||||
type MobileNativeChatSession
|
||||
} from './use-mobile-native-chat-session'
|
||||
|
||||
describe('native chat before the first transcript flush', () => {
|
||||
it('negotiates pending snapshots and waits for the authoritative transcript', async () => {
|
||||
let state: MobileNativeChatSession | null = null
|
||||
let emit: (frame: unknown) => void = () => {}
|
||||
const subscribe: RpcClient['subscribe'] = vi.fn((_method, params, onData) => {
|
||||
emit = onData
|
||||
if (
|
||||
(params as { capabilities?: { transcriptPending?: number } }).capabilities
|
||||
?.transcriptPending === 1
|
||||
) {
|
||||
onData({ type: 'snapshot', messages: [], hasMore: false, pending: true })
|
||||
}
|
||||
return () => {}
|
||||
})
|
||||
const operations = nativeHostSessionNativeChatOperations({ subscribe } as unknown as RpcClient)
|
||||
function Harness() {
|
||||
state = useMobileNativeChatSession({
|
||||
operations,
|
||||
workspaceId: 'workspace',
|
||||
agent: 'claude',
|
||||
sessionId: 'session',
|
||||
transcriptPath: null,
|
||||
terminalId: 'terminal',
|
||||
clientId: 'device'
|
||||
})
|
||||
return null
|
||||
}
|
||||
let renderer: ReturnType<typeof create> | undefined
|
||||
try {
|
||||
await act(async () => {
|
||||
renderer = create(createElement(Harness))
|
||||
})
|
||||
expect(state).toMatchObject({
|
||||
status: 'awaiting-transcript',
|
||||
transcriptLoading: true,
|
||||
messages: []
|
||||
})
|
||||
await act(async () => emit({ type: 'snapshot', messages: [], hasMore: false }))
|
||||
expect(state).toMatchObject({ status: 'ready', transcriptLoading: false, messages: [] })
|
||||
expect(subscribe).toHaveBeenCalledTimes(1)
|
||||
} finally {
|
||||
act(() => renderer?.unmount())
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -46,6 +46,7 @@ export function nativeHostSessionNativeChatOperations(
|
||||
'nativeChat.subscribe',
|
||||
{
|
||||
...nativeChatReadParams(target, limit),
|
||||
capabilities: { transcriptPending: 1 },
|
||||
subscriptionId: buildNativeChatSubscriptionId(target.agent, target.sessionId)
|
||||
},
|
||||
(value) => onEvent(value as Parameters<typeof onEvent>[0])
|
||||
|
||||
@@ -218,7 +218,7 @@ export function useMobileNativeChatSession(args: {
|
||||
setLoadingEarlier(false)
|
||||
beforeOffsetRef.current = null
|
||||
}
|
||||
setRead({ operations, identity, status: 'ready' })
|
||||
setRead({ operations, identity, status: applied.pending ? 'awaiting-transcript' : 'ready' })
|
||||
},
|
||||
() => {
|
||||
if (!cancelled) {
|
||||
@@ -317,7 +317,7 @@ export function useMobileNativeChatSession(args: {
|
||||
messages: visibleMessages,
|
||||
lifecycle,
|
||||
status,
|
||||
transcriptLoading: status === 'loading',
|
||||
transcriptLoading: status === 'loading' || status === 'awaiting-transcript',
|
||||
error,
|
||||
hasMore,
|
||||
loadingEarlier,
|
||||
|
||||
Reference in New Issue
Block a user