mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
* native-chat: render one indicator row for the live desktop turn The turn-timing row and the spinner+activity line were two rows saying "Working" at once. A settled turn keeps its own row; the live turn now has only the spinner row, labelled provider activity -> Thinking -> Working for N through the shared resolver. Reasoning is the turn's content, so it no longer becomes the activity label, and "Thinking" now means the turn is reasoning right now rather than that it has produced no output yet. * mobile: give the live turn row a spinner and the shared indicator label Mobile's per-turn row is already the only live indicator on the structured lane, but it pulsed a bare word and never showed what the provider said it was doing. It now renders a spinner beside the same resolved label desktop uses, and reads reasoning from the journal instead of inferring it from missing output. The bridge lane's four prompt/interrupt write seams move to one module so the controller stays under its line cap. * codex: mark streamed reasoning as reasoning too, and pin the provider markers The settled reasoning item carried the marker but the streaming one did not, so a live Codex turn - the only time the indicator is on screen - never read as reasoning. Both paths now stamp it; a plan document keeps its own presentation and must never read as reasoning. * fix(native-chat): tighten live turn reasoning state --------- Co-authored-by: Merge Sim <sim@local>
126 lines
5.1 KiB
TypeScript
126 lines
5.1 KiB
TypeScript
import { useMemo } from 'react'
|
|
import { StyleSheet, View } from 'react-native'
|
|
import { MobileNativeChatView, type MobileNativeChatInputLockReason } from './MobileNativeChatView'
|
|
import { foldMobileNativeChatMessages } from './mobile-native-chat-render-data'
|
|
import type { MobileNativeChatImageAttachments } from './use-mobile-native-chat-image-attachments'
|
|
import type { MobileNativeChatController } from './use-mobile-native-chat-controller'
|
|
import { useMobileNativeChatStreamingBubble } from './use-mobile-native-chat-streaming-bubble'
|
|
|
|
type Props = {
|
|
controller: MobileNativeChatController
|
|
/** Opens a tapped file reference (worktree-relative or absolute, optional
|
|
* :line(:col) suffix) through the shared tap-to-open flow. */
|
|
onOpenFile: (pathText: string) => void
|
|
/** Native-chat image attachments: picking adds a composer chip, and sending
|
|
* rides the pending images along with the message text (desktop parity). */
|
|
images: MobileNativeChatImageAttachments
|
|
onMicPress: () => void
|
|
micActive: boolean
|
|
dictationMode: 'toggle' | 'hold'
|
|
onMicPressIn: () => void
|
|
onMicPressOut: () => void
|
|
inputLockReason: MobileNativeChatInputLockReason | null
|
|
/** Latest send failure, rendered inline above the composer. */
|
|
sendErrorMessage: string | null
|
|
/** Drops that failure once a later send succeeds. */
|
|
onClearSendError: () => void
|
|
/** Stable host/worktree/tab identity for accepted-send completion fencing. */
|
|
sendSurfaceId: string
|
|
/** Reads the retained route's focus generation for accepted-send fencing. */
|
|
getSendCompletionGeneration: () => number
|
|
keyboardInset: number
|
|
}
|
|
|
|
/** Keeps the terminal mounted underneath chat so its PTY subscription survives
|
|
* view toggles while the native surface owns the visible composer. Also owns
|
|
* the streaming gate: this component stays mounted across those toggles, while
|
|
* the chat list below it does not. */
|
|
export function MobileNativeChatOverlay({
|
|
controller,
|
|
onOpenFile,
|
|
images,
|
|
onMicPress,
|
|
micActive,
|
|
dictationMode,
|
|
onMicPressIn,
|
|
onMicPressOut,
|
|
inputLockReason,
|
|
sendErrorMessage,
|
|
onClearSendError,
|
|
sendSurfaceId,
|
|
getSendCompletionGeneration,
|
|
keyboardInset
|
|
}: Props): React.JSX.Element | null {
|
|
const session = controller.nativeChatSession
|
|
const folded = useMemo(() => foldMobileNativeChatMessages(session.messages), [session.messages])
|
|
const streaming = useMobileNativeChatStreamingBubble(
|
|
folded,
|
|
controller.nativeChatStreamingText,
|
|
controller.nativeChatStreamScopeKey,
|
|
controller.nativeChatStreamLive
|
|
)
|
|
if (!controller.showNativeChat) {
|
|
return null
|
|
}
|
|
return (
|
|
<View style={styles.overlay}>
|
|
<MobileNativeChatView
|
|
messages={session.messages}
|
|
folded={folded}
|
|
status={session.status}
|
|
error={session.error}
|
|
agent={controller.nativeChatAgent}
|
|
agentWorking={controller.nativeChatAgentWorking}
|
|
canStop={controller.nativeChatCanStop}
|
|
structuredActivityUi={controller.nativeChatStructured}
|
|
turnIndicator={controller.nativeChatTurnIndicator}
|
|
workingStartedAt={controller.nativeChatWorkingStartedAt}
|
|
settledTurns={controller.nativeChatSettledTurns}
|
|
streaming={streaming}
|
|
onStop={controller.handleNativeChatStop}
|
|
ask={controller.nativeChatAsk}
|
|
askKey={controller.nativeChatAskKey}
|
|
onDismissAsk={controller.dismissNativeChatAsk}
|
|
onAnswerAsk={controller.handleNativeChatAnswerAsk}
|
|
onCancelAsk={controller.handleNativeChatCancelAsk}
|
|
question={controller.nativeChatQuestion}
|
|
onAnswerQuestion={controller.handleNativeChatQuestionAnswer}
|
|
permission={controller.nativeChatPermission}
|
|
onRespondPermission={controller.handleNativeChatRespondPermission}
|
|
onOpenFile={onOpenFile}
|
|
hasMore={session.hasMore}
|
|
loadingEarlier={session.loadingEarlier}
|
|
onLoadEarlier={session.loadEarlier}
|
|
onSend={images.sendNativeChat}
|
|
sendSurfaceId={sendSurfaceId}
|
|
getSendCompletionGeneration={getSendCompletionGeneration}
|
|
getComposerEditGeneration={controller.getChatComposerEditGeneration}
|
|
pending={controller.chatPending}
|
|
imagePreviewsByMessageId={controller.chatImagePreviewsByMessageId}
|
|
composerText={controller.chatComposerText}
|
|
onComposerTextChange={controller.setChatComposerText}
|
|
onAttachImage={() => void images.attachImage('library')}
|
|
attachments={images.attachments}
|
|
onRemoveAttachment={images.removeAttachment}
|
|
isAttaching={images.isAttaching}
|
|
onMicPress={onMicPress}
|
|
micActive={micActive}
|
|
dictationMode={dictationMode}
|
|
onMicPressIn={onMicPressIn}
|
|
onMicPressOut={onMicPressOut}
|
|
inputLockReason={inputLockReason}
|
|
sendErrorMessage={sendErrorMessage}
|
|
onClearSendError={onClearSendError}
|
|
filePaths={controller.nativeChatFilePaths}
|
|
onNeedFiles={controller.loadNativeChatFiles}
|
|
sessionOptions={controller.nativeChatSessionOptions}
|
|
keyboardInset={keyboardInset}
|
|
/>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
overlay: StyleSheet.absoluteFillObject
|
|
})
|