mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 01:57:57 +00:00
* feat(native-chat): one queue header that becomes the pause row; no composer Resume or send dialog The queued cards now sit under one header row on desktop and phone: "Queued" and the count normally, and "Paused" with the count, Clear all and Resume while the host holds the queue after a Stop. A chevron folds only the cards; the choice is local to the view and resets when the queue empties. The composer's Send no longer turns into a second Resume, and a message sent into a paused queue no longer asks "Send message?": it sends, its accepted turn lifts the pause, and the cards follow it. Clear all deletes every card through the existing per-card delete. No wire or host change. * test(mobile): find the background-tasks strip's toggle inside the strip, not the queue header's * Make queued Clear all atomic across clients * Show queued message failures in collapsed headers * Use per-card delete for paused queue Clear all * Join repeated paused queue Clear actions
168 lines
7.1 KiB
TypeScript
168 lines
7.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'
|
|
import { useMobileNativeChatComposerTray } from './use-mobile-native-chat-composer-tray'
|
|
import { useMobileNativeChatVisualRenderer } from './MobileNativeChatVisual'
|
|
import { MobileNativeChatVisualContext } from './mobile-native-chat-visual-context'
|
|
|
|
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: string | undefined
|
|
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
|
|
// The chat is drawn only with a resolved agent; the fallback never reaches the screen.
|
|
const agent = controller.nativeChatAgent ?? ''
|
|
const folded = useMemo(
|
|
() => foldMobileNativeChatMessages(session.messages, agent),
|
|
[session.messages, agent]
|
|
)
|
|
const streaming = useMobileNativeChatStreamingBubble(
|
|
folded,
|
|
controller.nativeChatStreamingText,
|
|
controller.nativeChatStreamScopeKey,
|
|
controller.nativeChatStreamLive
|
|
)
|
|
const queued = controller.nativeChatQueued
|
|
const composerTray = useMobileNativeChatComposerTray({
|
|
queued: {
|
|
cards: queued.cards,
|
|
onSend: queued.send,
|
|
onDelete: queued.delete,
|
|
onClearAll: queued.clear,
|
|
onEdit: queued.edit,
|
|
pause: queued.pause,
|
|
onResume: queued.resume,
|
|
sessionKey: queued.sessionKey,
|
|
// Nothing steers into a turn a Stop is ending; the host holds such a send until it ends.
|
|
// The indicator's `stopping` is the display status, decided once in the session hook.
|
|
steerHeld: controller.nativeChatTurnIndicator?.stopping === true
|
|
},
|
|
backgroundTasks: controller.nativeChatBackgroundTasks
|
|
})
|
|
const visuals = useMobileNativeChatVisualRenderer(controller.nativeChatVisualSource)
|
|
if (!controller.showNativeChat) {
|
|
return null
|
|
}
|
|
return (
|
|
<View style={styles.overlay}>
|
|
<MobileNativeChatVisualContext.Provider value={visuals}>
|
|
<MobileNativeChatView
|
|
messages={session.messages}
|
|
folded={folded}
|
|
status={session.status}
|
|
error={session.error}
|
|
readFailedFinally={session.readFailedFinally === true}
|
|
agent={controller.nativeChatAgent}
|
|
agentWorking={controller.nativeChatAgentWorking}
|
|
canStop={controller.nativeChatCanStop}
|
|
structuredActivityUi={controller.nativeChatStructured}
|
|
turnIndicator={controller.nativeChatTurnIndicator}
|
|
workingStartedAt={controller.nativeChatWorkingStartedAt}
|
|
settledTurns={controller.nativeChatSettledTurns}
|
|
turnJournal={controller.nativeChatTurnJournal}
|
|
receipts={controller.nativeChatReceipts}
|
|
actionablePromptIds={controller.nativeChatActionablePromptIds}
|
|
streaming={streaming}
|
|
onStop={controller.handleNativeChatStop}
|
|
ask={controller.nativeChatAsk}
|
|
askKey={controller.nativeChatAskKey}
|
|
promptKey={controller.nativeChatPromptKey}
|
|
onDismissAsk={controller.dismissNativeChatAsk}
|
|
onAnswerAsk={controller.handleNativeChatAnswerAsk}
|
|
onCancelAsk={controller.handleNativeChatCancelAsk}
|
|
onCancelPrompt={controller.handleNativeChatCancelPrompt}
|
|
onCollapseAsk={controller.collapseNativeChatAsk}
|
|
onCollapsePrompt={controller.collapseNativeChatPrompt}
|
|
collapsedPrompt={controller.nativeChatCollapsedPrompt}
|
|
question={controller.nativeChatQuestion}
|
|
onAnswerQuestion={controller.handleNativeChatQuestionAnswer}
|
|
permission={controller.nativeChatPermission}
|
|
onRespondPermission={controller.handleNativeChatRespondPermission}
|
|
composerTray={composerTray}
|
|
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={
|
|
controller.nativeChatAcceptsImages
|
|
? () => void images.attachImage('library')
|
|
: undefined
|
|
}
|
|
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}
|
|
/>
|
|
</MobileNativeChatVisualContext.Provider>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
overlay: StyleSheet.absoluteFillObject
|
|
})
|