mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 10:08:13 +00:00
* feat(mobile): Resume in New CLI from a phone chat tab A chat tab on the phone had only Close in its long-press menu, so a chat that would not load left no way to reach its conversation. The phone now offers Resume in New CLI, like the desktop tab menu: the chat's host prepares the conversation and the phone opens a copy in a new terminal tab. The resume and fork startup builders move to src/shared so the desktop and the phone build the same command. * test(mobile): type the fake terminal create instead of casting its calls * fix(mobile): pass the host's fork source as is; match the quoted args in the test * test(mobile): give the fake host client the untyped mock the other client fakes use * style: format the shared resume builder and phone fork files * Fix mobile chat CLI fork creation on owning host * Fix mobile chat fork lint and split create owner helpers * Keep chat fork beside phone anchor across tab groups * Combine mobile terminal runtime type imports * Repair mobile create CI contracts after chat fork extraction * Classify chat fork preflight refusals and preserve mobile create recordings * Keep route-owned create cleanup quiet after unmount * Preserve typed runtime repair refusal in chat fork preflight * Preserve checked store through mobile chat fork preflight * Register mobile chat fork runtime fixture before test setup * Remove unused mobile create recording value * style(mobile): format the chat fork create files; move fork reply messages into the fork feedback module Formatting pushed the create hook past its line limit, so the fork-only reply message, partial-success warning and host capability check now live beside the other fork messages. Same order and copy as before. * Keep quick command mutation aligned with formatted request * Keep mobile chat CLI forks in terminal view * Pass mobile chat resolution its existing controller arguments * Match the mobile create fixture mock layout * Keep mobile chat CLI forks open when placement is unavailable * Brace placement contract fixture checks * test(mobile): keep the prompt controller mock under its line limit after the main merge * Expose pane admission in the runtime test fixture type * Complete the chat fork contract controller fixture
401 lines
14 KiB
TypeScript
401 lines
14 KiB
TypeScript
import { Platform } from 'react-native'
|
|
import { useClipboardWriter } from '../platform/clipboard'
|
|
import { Copy, FileText, Globe, RefreshCw, SquareTerminal } from 'lucide-react-native'
|
|
import { MobileSessionHeaderMoreActionsSheet } from './MobileSessionHeaderMoreActionsSheet'
|
|
import { QuickCommandsSheet } from './QuickCommandsSheet'
|
|
import { triggerSuccess, triggerError } from '../platform/haptics'
|
|
import { ActionSheetModal } from '../components/ActionSheetModal'
|
|
import { TextInputModal } from '../components/TextInputModal'
|
|
import { ConfirmModal } from '../components/ConfirmModal'
|
|
import { CustomKeyModal } from '../components/CustomKeyModal'
|
|
import { MobileDictationSetupSheet } from '../components/MobileDictationSetupSheet'
|
|
import { useWorkspaceOnPairedHost } from '../navigation/workspace-execution-host'
|
|
import { MobileBrowserTabActionSheet } from './MobileBrowserTabActionSheet'
|
|
import { getMobileTerminalActionSheetActions } from './mobile-terminal-action-sheet-actions'
|
|
import { getMobileNativeChatCliForkActions } from './mobile-native-chat-cli-fork'
|
|
import {
|
|
getRepoIdFromMobileWorktreeId,
|
|
isTerminalPhoneDisplayMode
|
|
} from './mobile-session-route-helpers'
|
|
import type { MobileSessionController } from './use-mobile-session-controller'
|
|
|
|
export function MobileSessionSheets({ controller }: { controller: MobileSessionController }) {
|
|
const {
|
|
worktreeId,
|
|
isFolderWorkspaceRoute,
|
|
isFloatingWorkspaceRoute,
|
|
client,
|
|
worktreeName,
|
|
sessionTabs,
|
|
pendingDiffNotesDelivery,
|
|
setPendingDiffNotesDelivery,
|
|
showCreateTabDrawer,
|
|
setShowCreateTabDrawer,
|
|
showQuickCommands,
|
|
setShowQuickCommands,
|
|
setShowCreateBrowserModal,
|
|
showCreateBrowserModal,
|
|
showHeaderMoreActions,
|
|
setShowHeaderMoreActions,
|
|
actionTarget,
|
|
setActionTarget,
|
|
markdownActionTarget,
|
|
setMarkdownActionTarget,
|
|
fileActionTarget,
|
|
setFileActionTarget,
|
|
browserActionTarget,
|
|
setBrowserActionTarget,
|
|
agentSessionActionTarget,
|
|
setAgentSessionActionTarget,
|
|
discardMarkdownTarget,
|
|
setDiscardMarkdownTarget,
|
|
leaveDrafts,
|
|
setLeaveDrafts,
|
|
setRenameTarget,
|
|
renameTarget,
|
|
setCustomKeys,
|
|
showCustomKeyModal,
|
|
setShowCustomKeyModal,
|
|
deleteKeyTarget,
|
|
setDeleteKeyTarget,
|
|
terminalModes,
|
|
showDictationSetup,
|
|
setShowDictationSetup,
|
|
browserScreencastSupported,
|
|
quickCommandsSupported,
|
|
quickCommandAgentPromptMaxLength,
|
|
showToast,
|
|
nativeChatTranscriptIsLocalReadable,
|
|
nativeChatController,
|
|
toggleTabChatView,
|
|
toggleDisplayMode,
|
|
readFileTab,
|
|
leaveSession,
|
|
discardMarkdownLocalContent,
|
|
confirmDiscardMarkdown,
|
|
handleDeleteCustomKey,
|
|
handleManageShortcuts,
|
|
handleClearTerminal,
|
|
handleCreateTerminal,
|
|
launchQuickCommand,
|
|
handleCreateMarkdownNote,
|
|
handleCreateBrowser,
|
|
handleBrowserNavigationCommand,
|
|
handleRenameTerminal,
|
|
handleCloseTerminal,
|
|
handleCloseSessionTab,
|
|
bulkCloseActions,
|
|
closeWithBulkActions,
|
|
createTabAgentActions,
|
|
sendDiffNotesAgentActions,
|
|
handlePanelTap,
|
|
openAgentSessionHistory,
|
|
showAgentSessionHistoryAction,
|
|
showChecksAction
|
|
} = controller
|
|
const clipboard = useClipboardWriter()
|
|
const onPairedHost = useWorkspaceOnPairedHost()
|
|
return (
|
|
<>
|
|
<MobileSessionHeaderMoreActionsSheet
|
|
visible={showHeaderMoreActions}
|
|
showAgentSessionHistory={showAgentSessionHistoryAction}
|
|
showChecks={showChecksAction}
|
|
onOpenAgentSessionHistory={openAgentSessionHistory}
|
|
onOpenChecks={() => handlePanelTap('pr')}
|
|
onClose={() => setShowHeaderMoreActions(false)}
|
|
/>
|
|
<QuickCommandsSheet
|
|
visible={showQuickCommands && quickCommandsSupported === true}
|
|
onClose={() => setShowQuickCommands(false)}
|
|
client={client}
|
|
repoId={
|
|
isFolderWorkspaceRoute || isFloatingWorkspaceRoute
|
|
? null
|
|
: getRepoIdFromMobileWorktreeId(worktreeId) || null
|
|
}
|
|
repoName={worktreeName || null}
|
|
agentPromptMaxLength={quickCommandAgentPromptMaxLength}
|
|
onLaunch={launchQuickCommand}
|
|
/>
|
|
<ActionSheetModal
|
|
visible={showCreateTabDrawer}
|
|
title="New Tab"
|
|
actions={[
|
|
...createTabAgentActions,
|
|
{
|
|
label: 'Terminal',
|
|
icon: SquareTerminal,
|
|
onPress: () => {
|
|
setShowCreateTabDrawer(false)
|
|
void handleCreateTerminal()
|
|
}
|
|
},
|
|
// Why: browser/markdown creation resolve a real worktree on the host
|
|
// (browser.tabCreate, files.createFile); the floating sentinel is
|
|
// terminal-only over RPC, so those options hide there.
|
|
...(isFloatingWorkspaceRoute
|
|
? []
|
|
: [
|
|
{
|
|
label: 'Browser',
|
|
icon: Globe,
|
|
closeBeforePress: true,
|
|
onPress: () => {
|
|
if (browserScreencastSupported !== true) {
|
|
showToast('Desktop update required for mobile browser streaming', 1600)
|
|
return
|
|
}
|
|
setShowCreateBrowserModal(true)
|
|
}
|
|
},
|
|
...(onPairedHost
|
|
? [
|
|
{
|
|
label: 'Markdown Note',
|
|
icon: FileText,
|
|
onPress: () => {
|
|
setShowCreateTabDrawer(false)
|
|
void handleCreateMarkdownNote()
|
|
}
|
|
}
|
|
]
|
|
: [])
|
|
])
|
|
]}
|
|
onClose={() => setShowCreateTabDrawer(false)}
|
|
/>
|
|
<ActionSheetModal
|
|
visible={pendingDiffNotesDelivery !== null}
|
|
title="Send Review Notes"
|
|
message="Choose an agent session for the current notes."
|
|
actions={[
|
|
...sendDiffNotesAgentActions,
|
|
{
|
|
label: 'Copy Notes',
|
|
icon: Copy,
|
|
onPress: () => {
|
|
const delivery = pendingDiffNotesDelivery
|
|
setPendingDiffNotesDelivery(null)
|
|
if (!delivery) {
|
|
return
|
|
}
|
|
void clipboard
|
|
.writeText(delivery.prompt)
|
|
.then(() => {
|
|
triggerSuccess()
|
|
showToast('Notes copied')
|
|
})
|
|
.catch(() => {
|
|
triggerError()
|
|
showToast("Couldn't copy notes", 1500)
|
|
})
|
|
}
|
|
}
|
|
]}
|
|
onClose={() => setPendingDiffNotesDelivery(null)}
|
|
/>
|
|
<ActionSheetModal
|
|
visible={actionTarget != null}
|
|
title={actionTarget?.title || 'Terminal'}
|
|
actions={getMobileTerminalActionSheetActions({
|
|
target: actionTarget,
|
|
tabs: sessionTabs.filter((tab) => tab.type === 'terminal'),
|
|
isTabChatView: nativeChatController.isTabChatView,
|
|
nativeChatTranscriptIsLocalReadable,
|
|
onDismiss: () => setActionTarget(null),
|
|
onToggleChat: toggleTabChatView,
|
|
isPhoneMode: (handle) => isTerminalPhoneDisplayMode(handle, terminalModes),
|
|
onToggleDisplayMode: (handle) => void toggleDisplayMode(handle),
|
|
onRename: setRenameTarget,
|
|
onClear: (target) => void handleClearTerminal(target),
|
|
onClose: (target) => void handleCloseTerminal(target),
|
|
onCloseSessionTab: (tab) => void handleCloseSessionTab(tab),
|
|
bulkCloseActions
|
|
})}
|
|
onClose={() => setActionTarget(null)}
|
|
/>
|
|
<ActionSheetModal
|
|
visible={markdownActionTarget != null}
|
|
title={markdownActionTarget?.title || 'Markdown'}
|
|
actions={[
|
|
{
|
|
label: 'Refresh',
|
|
icon: RefreshCw,
|
|
// Why: dirty refresh opens ConfirmModal; wait for this sheet's native
|
|
// Modal to unmount first (same dual-Modal race as tab Rename, #10331).
|
|
closeBeforePress: true,
|
|
onPress: () => {
|
|
const target = markdownActionTarget
|
|
if (target) {
|
|
discardMarkdownLocalContent(target)
|
|
}
|
|
}
|
|
},
|
|
{
|
|
label: 'Copy Path',
|
|
icon: FileText,
|
|
onPress: () => {
|
|
const target = markdownActionTarget
|
|
setMarkdownActionTarget(null)
|
|
if (target) {
|
|
void clipboard
|
|
.writeText(target.relativePath || target.filePath)
|
|
.then(() => showToast('Path copied'))
|
|
.catch(() => {
|
|
triggerError()
|
|
showToast("Couldn't copy path", 1500)
|
|
})
|
|
}
|
|
}
|
|
},
|
|
...closeWithBulkActions(markdownActionTarget, () => setMarkdownActionTarget(null))
|
|
]}
|
|
onClose={() => setMarkdownActionTarget(null)}
|
|
/>
|
|
<ActionSheetModal
|
|
visible={fileActionTarget != null}
|
|
title={fileActionTarget?.title || 'File'}
|
|
actions={[
|
|
{
|
|
label: 'Refresh',
|
|
icon: RefreshCw,
|
|
onPress: () => {
|
|
const target = fileActionTarget
|
|
setFileActionTarget(null)
|
|
if (target) {
|
|
void readFileTab(target)
|
|
}
|
|
}
|
|
},
|
|
...closeWithBulkActions(fileActionTarget, () => setFileActionTarget(null))
|
|
]}
|
|
onClose={() => setFileActionTarget(null)}
|
|
/>
|
|
<MobileBrowserTabActionSheet
|
|
target={browserActionTarget}
|
|
onClose={() => setBrowserActionTarget(null)}
|
|
onNavigate={handleBrowserNavigationCommand}
|
|
onCloseTab={handleCloseSessionTab}
|
|
bulkCloseActions={bulkCloseActions}
|
|
/>
|
|
<ActionSheetModal
|
|
visible={agentSessionActionTarget != null}
|
|
title={agentSessionActionTarget?.title || 'Chat'}
|
|
actions={[
|
|
...getMobileNativeChatCliForkActions({
|
|
controller,
|
|
tab: agentSessionActionTarget,
|
|
workspaceOnPairedHost: onPairedHost,
|
|
onDismiss: () => setAgentSessionActionTarget(null)
|
|
}),
|
|
...closeWithBulkActions(agentSessionActionTarget, () => setAgentSessionActionTarget(null))
|
|
]}
|
|
onClose={() => setAgentSessionActionTarget(null)}
|
|
/>
|
|
<ActionSheetModal
|
|
visible={leaveDrafts != null}
|
|
title="Unsaved markdown changes"
|
|
message="Copy or discard phone drafts before leaving."
|
|
actions={[
|
|
{
|
|
label: 'Copy All & Leave',
|
|
icon: FileText,
|
|
onPress: () => {
|
|
const drafts = leaveDrafts ?? []
|
|
const combined = drafts
|
|
.map((draft) => `# ${draft.title}\n\n${draft.content}`)
|
|
.join('\n\n---\n\n')
|
|
void clipboard
|
|
.writeText(combined)
|
|
.then(() => {
|
|
setLeaveDrafts(null)
|
|
leaveSession()
|
|
})
|
|
.catch(() => {
|
|
triggerError()
|
|
showToast("Couldn't copy drafts", 1500)
|
|
})
|
|
}
|
|
},
|
|
{
|
|
label: 'Discard & Leave',
|
|
destructive: true,
|
|
onPress: () => {
|
|
setLeaveDrafts(null)
|
|
leaveSession()
|
|
}
|
|
}
|
|
]}
|
|
onClose={() => setLeaveDrafts(null)}
|
|
/>
|
|
<ConfirmModal
|
|
visible={discardMarkdownTarget != null}
|
|
title="Discard Changes"
|
|
message="Replace the phone draft with the latest desktop file?"
|
|
confirmLabel="Discard"
|
|
destructive
|
|
onConfirm={confirmDiscardMarkdown}
|
|
onCancel={() => setDiscardMarkdownTarget(null)}
|
|
/>
|
|
<TextInputModal
|
|
visible={renameTarget != null}
|
|
title="Rename Terminal"
|
|
defaultValue={renameTarget?.title || 'Terminal'}
|
|
placeholder="Terminal name"
|
|
onSubmit={(value) => void handleRenameTerminal(value)}
|
|
onCancel={() => setRenameTarget(null)}
|
|
/>
|
|
<TextInputModal
|
|
visible={showCreateBrowserModal}
|
|
title="New Browser"
|
|
message="Enter a URL, or leave blank for a new tab."
|
|
defaultValue=""
|
|
placeholder="https://example.com"
|
|
submitLabel="Open"
|
|
allowEmpty
|
|
selectTextOnFocus
|
|
keyboardType={Platform.OS === 'ios' ? 'url' : 'default'}
|
|
onSubmit={(value) => {
|
|
void handleCreateBrowser(value).then((created) => {
|
|
if (created) {
|
|
setShowCreateBrowserModal(false)
|
|
}
|
|
})
|
|
}}
|
|
onCancel={() => setShowCreateBrowserModal(false)}
|
|
/>
|
|
<CustomKeyModal
|
|
visible={showCustomKeyModal}
|
|
onClose={() => setShowCustomKeyModal(false)}
|
|
onKeysChanged={setCustomKeys}
|
|
onManageShortcuts={handleManageShortcuts}
|
|
/>
|
|
<MobileDictationSetupSheet
|
|
visible={showDictationSetup}
|
|
client={client}
|
|
onClose={() => setShowDictationSetup(false)}
|
|
onReady={() => setShowDictationSetup(false)}
|
|
/>
|
|
<ActionSheetModal
|
|
visible={deleteKeyTarget != null}
|
|
title={deleteKeyTarget?.label ?? 'Shortcut'}
|
|
message="Remove this custom shortcut?"
|
|
actions={[
|
|
{
|
|
label: 'Remove',
|
|
destructive: true,
|
|
onPress: () => {
|
|
if (deleteKeyTarget) {
|
|
void handleDeleteCustomKey(deleteKeyTarget)
|
|
}
|
|
setDeleteKeyTarget(null)
|
|
}
|
|
}
|
|
]}
|
|
onClose={() => setDeleteKeyTarget(null)}
|
|
/>
|
|
</>
|
|
)
|
|
}
|