mirror of
https://github.com/stablyai/orca.git
synced 2026-11-04 11:07:42 +00:00
fix(mobile): greet a just-started terminal Claude with "Start a chat" (#35219)
Claude reports its session at startup but writes no transcript until the first prompt, so the host answers pending and the phone said "Can't find this conversation yet". A session the running agent reported live is new; only remembered sessions keep the not-found copy.
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import { Text, View } from 'react-native'
|
||||
import type { NativeChatEmptyStateCopy } from '../../../src/shared/native-chat/native-chat-empty-state'
|
||||
import { styles } from './mobile-native-chat-view-styles'
|
||||
|
||||
export function MobileNativeChatEmptyStateView({ copy }: { copy: NativeChatEmptyStateCopy }) {
|
||||
return (
|
||||
<View style={styles.center}>
|
||||
<Text style={styles.emptyTitle}>{copy.title}</Text>
|
||||
<Text style={styles.emptySubtitle}>{copy.subtitle}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -177,6 +177,7 @@ export function MobileNativeChatOverlay({
|
||||
error={session.error}
|
||||
readFailedFinally={session.readFailedFinally === true}
|
||||
agent={controller.nativeChatAgent}
|
||||
sessionLive={controller.nativeChatSessionLive}
|
||||
agentWorking={controller.nativeChatTurnActive}
|
||||
awaitingInput={controller.nativeChatAwaitingInput}
|
||||
canStop={controller.nativeChatCanStop}
|
||||
|
||||
@@ -26,6 +26,7 @@ import type { NativeChatResolvedPrompt } from '../../../src/shared/native-chat-r
|
||||
import { colors } from '../theme/mobile-theme'
|
||||
import { useMobileColorScheme } from '../theme/use-mobile-color-scheme'
|
||||
import { styles } from './mobile-native-chat-view-styles'
|
||||
import { MobileNativeChatEmptyStateView } from './MobileNativeChatEmptyStateView'
|
||||
import { mobileNativeChatListFooter } from './mobile-native-chat-list-footer'
|
||||
import { mobileNativeChatListHeader } from './mobile-native-chat-list-header'
|
||||
import {
|
||||
@@ -75,6 +76,7 @@ type Props = ComposerTrayProps & {
|
||||
// Permanent read failure replaces the pane because further chat actions cannot succeed.
|
||||
readFailedFinally?: boolean
|
||||
agent?: string | null
|
||||
sessionLive?: boolean
|
||||
/** The turn is live: working, or waiting on the reader. */
|
||||
agentWorking?: boolean
|
||||
canStop?: boolean
|
||||
@@ -150,6 +152,7 @@ export function MobileNativeChatView({
|
||||
error,
|
||||
readFailedFinally = false,
|
||||
agent,
|
||||
sessionLive,
|
||||
agentWorking,
|
||||
canStop = agentWorking,
|
||||
awaitingInput = null,
|
||||
@@ -310,7 +313,7 @@ export function MobileNativeChatView({
|
||||
// Changing text scale relays out every row, so preserve the page’s reading anchor.
|
||||
const layoutKey = `${fontScale}`
|
||||
const footer = mobileNativeChatListFooter(liveStatus, turns.waitingRows, renderItem)
|
||||
const emptyState = mobileNativeChatEmptyState(status, agent ?? null, error)
|
||||
const emptyState = mobileNativeChatEmptyState(status, agent ?? null, error, sessionLive)
|
||||
|
||||
const lockReason = useSettledMobileNativeChatInputLock(inputLockReason)
|
||||
const composerPlaceholder = mobileNativeChatComposerPlaceholder(
|
||||
@@ -320,12 +323,7 @@ export function MobileNativeChatView({
|
||||
// Lock only terminal-backed Send: it can answer an agent prompt, while drafting is harmless and structured sends queue.
|
||||
const expandedPromptOwnsSend =
|
||||
!structuredActivityUi && !collapsedPrompt && (ask ?? permission ?? question) != null
|
||||
const emptyStateView = emptyState ? (
|
||||
<View style={styles.center}>
|
||||
<Text style={styles.emptyTitle}>{emptyState.title}</Text>
|
||||
<Text style={styles.emptySubtitle}>{emptyState.subtitle}</Text>
|
||||
</View>
|
||||
) : null
|
||||
const emptyStateView = emptyState ? <MobileNativeChatEmptyStateView copy={emptyState} /> : null
|
||||
|
||||
if (readFailedFinally && emptyStateView) {
|
||||
return <View style={[styles.root, { paddingBottom: bottomPad }]}>{emptyStateView}</View>
|
||||
|
||||
@@ -31,6 +31,7 @@ export type MobileNativeChatController = {
|
||||
showNativeChat: boolean
|
||||
showNativeChatRef: MutableRefObject<boolean>
|
||||
nativeChatAgent: string | null
|
||||
nativeChatSessionLive: boolean
|
||||
chatComposerScopeKey?: string | null
|
||||
reportQueuedEditError?: (message: string) => void
|
||||
chatDraftKey: string | null
|
||||
|
||||
@@ -41,6 +41,13 @@ describe('mobileNativeChatEmptyState', () => {
|
||||
}
|
||||
)
|
||||
|
||||
it('invites a first message when the running agent reported a session with no transcript yet', () => {
|
||||
expect(mobileNativeChatEmptyState('awaiting-transcript', 'claude', undefined, true)).toEqual({
|
||||
title: 'Start a chat with Claude',
|
||||
subtitle: 'Ask Claude to inspect code, explain output, or make a change.'
|
||||
})
|
||||
})
|
||||
|
||||
it('falls back to "the agent" when the agent is unknown', () => {
|
||||
expect(mobileNativeChatEmptyState('ready', null)?.title).toBe('Start a chat with the agent')
|
||||
})
|
||||
|
||||
@@ -24,14 +24,17 @@ import type { MobileNativeChatStatus } from './use-mobile-native-chat-session'
|
||||
export function mobileNativeChatEmptyState(
|
||||
status: MobileNativeChatStatus,
|
||||
agent: string | null,
|
||||
error?: string
|
||||
error?: string,
|
||||
sessionLive = false
|
||||
): NativeChatEmptyStateCopy | null {
|
||||
const agentLabel = agent ? formatAgentTypeLabel(agent) : 'the agent'
|
||||
switch (status) {
|
||||
// Missing history may mean new or unreadable; promise neither in the waiting copy.
|
||||
case 'waiting-session':
|
||||
case 'awaiting-transcript':
|
||||
return formatNativeChatEmptyStateCopy('notFound', agentLabel)
|
||||
// A session the running agent just reported has no file until its first prompt.
|
||||
case 'awaiting-transcript':
|
||||
return formatNativeChatEmptyStateCopy(sessionLive ? 'empty' : 'notFound', agentLabel)
|
||||
case 'ready':
|
||||
return formatNativeChatEmptyStateCopy('empty', agentLabel)
|
||||
case 'error': {
|
||||
|
||||
@@ -64,7 +64,8 @@ describe('resolveMobileNativeChat', () => {
|
||||
agent: 'claude',
|
||||
sessionId: 'sess-1',
|
||||
transcriptPath: '/tmp/claude-real-transcript.jsonl',
|
||||
agentConfirmed: true
|
||||
agentConfirmed: true,
|
||||
sessionLive: true
|
||||
})
|
||||
})
|
||||
|
||||
@@ -74,7 +75,13 @@ describe('resolveMobileNativeChat', () => {
|
||||
type: 'terminal',
|
||||
agentStatus: status({ agentType: 'codex' })
|
||||
})
|
||||
).toEqual({ agent: 'codex', sessionId: null, transcriptPath: null, agentConfirmed: true })
|
||||
).toEqual({
|
||||
agent: 'codex',
|
||||
sessionId: null,
|
||||
transcriptPath: null,
|
||||
agentConfirmed: true,
|
||||
sessionLive: false
|
||||
})
|
||||
})
|
||||
|
||||
it('admits OpenClaude with its distinct agent identity', () => {
|
||||
@@ -82,7 +89,8 @@ describe('resolveMobileNativeChat', () => {
|
||||
agent: 'openclaude',
|
||||
sessionId: null,
|
||||
transcriptPath: null,
|
||||
agentConfirmed: true
|
||||
agentConfirmed: true,
|
||||
sessionLive: false
|
||||
})
|
||||
})
|
||||
|
||||
@@ -98,7 +106,8 @@ describe('resolveMobileNativeChat', () => {
|
||||
agent,
|
||||
sessionId: 'real-session',
|
||||
transcriptPath: null,
|
||||
agentConfirmed: true
|
||||
agentConfirmed: true,
|
||||
sessionLive: true
|
||||
})
|
||||
expect(resolveMobileNativeChat(tab, false)).toBeNull()
|
||||
})
|
||||
@@ -155,7 +164,8 @@ describe('resolveMobileNativeChat', () => {
|
||||
agent: 'codex',
|
||||
sessionId: 'structured-1',
|
||||
transcriptPath: null,
|
||||
agentConfirmed: true
|
||||
agentConfirmed: true,
|
||||
sessionLive: true
|
||||
})
|
||||
})
|
||||
|
||||
@@ -170,7 +180,8 @@ describe('resolveMobileNativeChat', () => {
|
||||
agent: 'claude',
|
||||
sessionId: 'structured-1',
|
||||
transcriptPath: null,
|
||||
agentConfirmed: true
|
||||
agentConfirmed: true,
|
||||
sessionLive: true
|
||||
})
|
||||
})
|
||||
|
||||
@@ -179,7 +190,13 @@ describe('resolveMobileNativeChat', () => {
|
||||
(agent) => {
|
||||
expect(
|
||||
resolveMobileNativeChat({ type: 'agent-session', sessionId: 'structured-1', agent })
|
||||
).toEqual({ agent, sessionId: 'structured-1', transcriptPath: null, agentConfirmed: true })
|
||||
).toEqual({
|
||||
agent,
|
||||
sessionId: 'structured-1',
|
||||
transcriptPath: null,
|
||||
agentConfirmed: true,
|
||||
sessionLive: true
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -236,7 +253,8 @@ describe('withRememberedNativeChatIdentity', () => {
|
||||
agent: 'claude',
|
||||
sessionId: 'sess-1',
|
||||
transcriptPath: '/t/sess-1.jsonl',
|
||||
agentConfirmed: false
|
||||
agentConfirmed: false,
|
||||
sessionLive: false
|
||||
})
|
||||
expect(resolveMobileNativeChatSessionId(next)).toBe('sess-1')
|
||||
})
|
||||
@@ -258,7 +276,8 @@ describe('withRememberedNativeChatIdentity', () => {
|
||||
agent: 'codex',
|
||||
sessionId: null,
|
||||
transcriptPath: null,
|
||||
agentConfirmed: true
|
||||
agentConfirmed: true,
|
||||
sessionLive: false
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ export type MobileNativeChatResolution = {
|
||||
transcriptPath: string | null
|
||||
// Remembered identity alone permits reads only; typing could otherwise reach a bare shell.
|
||||
agentConfirmed: boolean
|
||||
// The running agent reported this session, so a missing transcript means no prompt yet.
|
||||
sessionLive: boolean
|
||||
}
|
||||
|
||||
export type MobileNativeChatIdentity = {
|
||||
@@ -43,7 +45,13 @@ export function resolveMobileNativeChat(
|
||||
if (tab.type === 'agent-session') {
|
||||
// Structured tabs use journals, so no per-agent transcript layout is needed.
|
||||
return tab.sessionId && isStructuredAgentId(tab.agent)
|
||||
? { agent: tab.agent, sessionId: tab.sessionId, transcriptPath: null, agentConfirmed: true }
|
||||
? {
|
||||
agent: tab.agent,
|
||||
sessionId: tab.sessionId,
|
||||
transcriptPath: null,
|
||||
agentConfirmed: true,
|
||||
sessionLive: true
|
||||
}
|
||||
: null
|
||||
}
|
||||
if (tab.type !== 'terminal') {
|
||||
@@ -70,7 +78,8 @@ export function resolveMobileNativeChat(
|
||||
transcriptPath: liveSession
|
||||
? (liveSession.transcriptPath ?? null)
|
||||
: (rememberedSession?.transcriptPath ?? null),
|
||||
agentConfirmed: evidenceAgent !== null
|
||||
agentConfirmed: evidenceAgent !== null,
|
||||
sessionLive: liveSession?.id != null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -225,6 +225,7 @@ export function useMobileNativeChatController(args: {
|
||||
showNativeChat,
|
||||
showNativeChatRef,
|
||||
nativeChatAgent: activeChatResolution?.agent ?? null,
|
||||
nativeChatSessionLive: activeChatResolution?.sessionLive === true,
|
||||
chatDraftKey,
|
||||
setChatComposerText,
|
||||
getChatComposerEditGeneration,
|
||||
|
||||
Reference in New Issue
Block a user