Skip to content

Commit 430bd73

Browse files
committed
Polish archived session resume flow
1 parent d7c6ead commit 430bd73

19 files changed

Lines changed: 364 additions & 159 deletions

File tree

packages/happy-app/sources/-session/SessionView.tsx

Lines changed: 121 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import { EmptyMessages } from '@/components/EmptyMessages';
1616
import { SessionActionsAnchor, SessionActionsPopover } from '@/components/SessionActionsPopover';
1717
import { VoiceAssistantStatusBar } from '@/components/VoiceAssistantStatusBar';
1818
import { useDraft } from '@/hooks/useDraft';
19-
import { useSessionQuickActions } from '@/hooks/useSessionQuickActions';
2019
import { Modal } from '@/modal';
2120
import { voiceHooks } from '@/realtime/hooks/voiceHooks';
2221
import { startRealtimeSession, stopRealtimeSession } from '@/realtime/RealtimeSession';
@@ -30,7 +29,7 @@ import { t } from '@/text';
3029
import { tracking, trackMessageSent } from '@/track';
3130
import { isRunningOnMac } from '@/utils/platform';
3231
import { useDeviceType, useHeaderHeight, useIsLandscape, useIsTablet } from '@/utils/responsive';
33-
import { formatPathRelativeToHome, getResumeCommand, getSessionAvatarId, getSessionName, useSessionStatus } from '@/utils/sessionUtils';
32+
import { formatPathRelativeToHome, getResumeCommandBlock, getSessionAvatarId, getSessionName, useSessionStatus } from '@/utils/sessionUtils';
3433
import { isVersionSupported, MINIMUM_CLI_VERSION } from '@/utils/versionUtils';
3534
import * as Clipboard from 'expo-clipboard';
3635
import { Ionicons } from '@expo/vector-icons';
@@ -236,14 +235,10 @@ function SessionViewLoaded({ sessionId, session }: { sessionId: string, session:
236235
const alwaysShowContextSize = useSetting('alwaysShowContextSize');
237236
const experiments = useSetting('experiments');
238237
const expResumeSession = useSetting('expResumeSession');
239-
const resumeCommand = getResumeCommand(session);
240-
const {
241-
canResume,
242-
canShowResume,
243-
resumeSession,
244-
resumeSessionSubtitle,
245-
resumingSession,
246-
} = useSessionQuickActions(session);
238+
const isArchivedSession = session.metadata?.lifecycleState === 'archived';
239+
const isDisconnected = !sessionStatus.isConnected;
240+
const isInactiveArchivedSession = isArchivedSession && isDisconnected;
241+
const resumeCommandBlock = getResumeCommandBlock(session);
247242

248243
// Use draft hook for auto-saving message drafts
249244
const { clearDraft } = useDraft(sessionId, message, setMessage);
@@ -340,7 +335,7 @@ function SessionViewLoaded({ sessionId, session }: { sessionId: string, session:
340335
</>
341336
) : null;
342337

343-
const input = sessionStatus.isConnected ? (
338+
const composer = (
344339
<AgentInput
345340
placeholder={t('session.inputPlaceholder')}
346341
value={message}
@@ -359,6 +354,7 @@ function SessionViewLoaded({ sessionId, session }: { sessionId: string, session:
359354
dotColor: sessionStatus.statusDotColor,
360355
isPulsing: sessionStatus.isPulsing
361356
}}
357+
blockSend={isDisconnected}
362358
onSend={() => {
363359
if (message.trim()) {
364360
setMessage('');
@@ -367,9 +363,9 @@ function SessionViewLoaded({ sessionId, session }: { sessionId: string, session:
367363
trackMessageSent();
368364
}
369365
}}
370-
onMicPress={micButtonState.onMicPress}
371-
isMicActive={micButtonState.isMicActive}
372-
onAbort={() => sessionAbort(sessionId)}
366+
onMicPress={isDisconnected ? undefined : micButtonState.onMicPress}
367+
isMicActive={isDisconnected ? false : micButtonState.isMicActive}
368+
onAbort={isDisconnected ? undefined : () => sessionAbort(sessionId)}
373369
showAbortButton={sessionStatus.state === 'thinking' || sessionStatus.state === 'waiting'}
374370
onFileViewerPress={experiments ? () => router.push(`/session/${sessionId}/files`) : undefined}
375371
autocompletePrefixes={['@', '/']}
@@ -389,61 +385,32 @@ function SessionViewLoaded({ sessionId, session }: { sessionId: string, session:
389385
} : undefined}
390386
alwaysShowContextSize={alwaysShowContextSize}
391387
/>
392-
) : canShowResume && expResumeSession ? (
393-
<CenteredInputWidth horizontalPadding={sessionInputHorizontalPadding}>
394-
<View style={{
395-
paddingHorizontal: 16,
396-
paddingTop: 12,
397-
paddingBottom: 10,
398-
gap: 10,
399-
}}>
400-
<Pressable
401-
onPress={resumeSession}
402-
style={{
403-
minHeight: 48,
404-
borderRadius: 14,
405-
backgroundColor: canResume ? theme.colors.button.primary.background : theme.colors.surfaceHigh,
406-
alignItems: 'center',
407-
justifyContent: 'center',
408-
flexDirection: 'row',
409-
gap: 8,
410-
opacity: resumingSession ? 0.7 : 1,
411-
}}
412-
>
413-
{resumingSession ? (
414-
<ActivityIndicator size="small" color={canResume ? theme.colors.button.primary.tint : theme.colors.textSecondary} />
415-
) : (
416-
<Ionicons
417-
name="play-circle-outline"
418-
size={18}
419-
color={canResume ? theme.colors.button.primary.tint : theme.colors.textSecondary}
420-
/>
421-
)}
422-
<Text style={{
423-
color: canResume ? theme.colors.button.primary.tint : theme.colors.textSecondary,
424-
fontSize: 15,
425-
fontWeight: '600',
426-
}}>
427-
{t('sessionInfo.resumeSession')}
428-
</Text>
429-
</Pressable>
430-
<Text style={{
431-
color: theme.colors.textSecondary,
432-
fontSize: 13,
433-
lineHeight: 18,
434-
textAlign: 'center',
435-
paddingHorizontal: 8,
436-
}}>
437-
{resumeSessionSubtitle}
438-
</Text>
439-
</View>
440-
</CenteredInputWidth>
441-
) : !sessionStatus.isConnected && resumeCommand ? (
388+
);
389+
390+
const archivedHint = isInactiveArchivedSession ? (
442391
<CenteredInputWidth horizontalPadding={sessionInputHorizontalPadding}>
443-
<ResumeCommandHint command={resumeCommand} />
392+
<InactiveArchivedHint
393+
resumeCommandBlock={expResumeSession ? resumeCommandBlock : null}
394+
/>
444395
</CenteredInputWidth>
445396
) : null;
446397

398+
const input = isInactiveArchivedSession ? (
399+
<>
400+
{archivedHint}
401+
{composer}
402+
</>
403+
) : (
404+
<>
405+
{expResumeSession && isDisconnected && resumeCommandBlock && (
406+
<CenteredInputWidth horizontalPadding={sessionInputHorizontalPadding}>
407+
<ResumeCommandHint resumeCommandBlock={resumeCommandBlock} />
408+
</CenteredInputWidth>
409+
)}
410+
{composer}
411+
</>
412+
);
413+
447414

448415
return (
449416
<>
@@ -531,43 +498,14 @@ function SessionViewLoaded({ sessionId, session }: { sessionId: string, session:
531498
)
532499
}
533500

534-
function ResumeCommandHint({ command }: { command: string }) {
501+
function ResumeCommandHint({ resumeCommandBlock }: {
502+
resumeCommandBlock: NonNullable<ReturnType<typeof getResumeCommandBlock>>;
503+
}) {
535504
const { theme } = useUnistyles();
536-
const [copied, setCopied] = React.useState(false);
505+
537506
return (
538507
<View style={{ paddingHorizontal: 16, paddingTop: 12, paddingBottom: 10, gap: 8 }}>
539-
<Pressable
540-
onPress={async () => {
541-
await Clipboard.setStringAsync(command);
542-
setCopied(true);
543-
setTimeout(() => setCopied(false), 2000);
544-
}}
545-
style={{
546-
minHeight: 48,
547-
borderRadius: 14,
548-
backgroundColor: theme.colors.surfaceHigh,
549-
alignItems: 'center',
550-
justifyContent: 'center',
551-
flexDirection: 'row',
552-
gap: 8,
553-
paddingHorizontal: 16,
554-
}}
555-
>
556-
<Ionicons name="terminal-outline" size={16} color={theme.colors.textSecondary} />
557-
<Text style={{
558-
color: theme.colors.text,
559-
fontSize: 13,
560-
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
561-
flex: 1,
562-
}} numberOfLines={1}>
563-
{command}
564-
</Text>
565-
<Ionicons
566-
name={copied ? 'checkmark' : 'copy-outline'}
567-
size={16}
568-
color={copied ? '#30D158' : theme.colors.textSecondary}
569-
/>
570-
</Pressable>
508+
<ResumeCommandCopyBlock resumeCommandBlock={resumeCommandBlock} />
571509
<Text style={{
572510
color: theme.colors.textSecondary,
573511
fontSize: 12,
@@ -581,6 +519,90 @@ function ResumeCommandHint({ command }: { command: string }) {
581519
);
582520
}
583521

522+
function InactiveArchivedHint(props: {
523+
resumeCommandBlock: NonNullable<ReturnType<typeof getResumeCommandBlock>> | null;
524+
}) {
525+
const { theme } = useUnistyles();
526+
const hintTextStyle = {
527+
color: theme.colors.agentEventText,
528+
fontSize: 13,
529+
lineHeight: 18,
530+
textAlign: 'left' as const,
531+
};
532+
533+
return (
534+
<View style={{
535+
paddingTop: 12,
536+
paddingBottom: 10,
537+
gap: 10,
538+
alignItems: 'stretch',
539+
}}>
540+
<View style={{ paddingHorizontal: 8, gap: 4 }}>
541+
<Text style={hintTextStyle}>
542+
{t('session.inactiveArchived')}
543+
</Text>
544+
{props.resumeCommandBlock && (
545+
<Text style={hintTextStyle}>
546+
{t('session.resumeFromTerminal')}
547+
</Text>
548+
)}
549+
</View>
550+
{props.resumeCommandBlock && (
551+
<ResumeCommandCopyBlock resumeCommandBlock={props.resumeCommandBlock} />
552+
)}
553+
</View>
554+
);
555+
}
556+
557+
function ResumeCommandCopyBlock({ resumeCommandBlock }: {
558+
resumeCommandBlock: NonNullable<ReturnType<typeof getResumeCommandBlock>>;
559+
}) {
560+
const { theme } = useUnistyles();
561+
const [copied, setCopied] = React.useState(false);
562+
563+
return (
564+
<Pressable
565+
onPress={async () => {
566+
await Clipboard.setStringAsync(resumeCommandBlock.copyText);
567+
setCopied(true);
568+
setTimeout(() => setCopied(false), 2000);
569+
}}
570+
style={{
571+
minHeight: 48,
572+
borderRadius: 14,
573+
backgroundColor: theme.colors.surfaceHigh,
574+
flexDirection: 'row',
575+
gap: 8,
576+
paddingHorizontal: 16,
577+
paddingVertical: 12,
578+
alignItems: 'flex-start',
579+
}}
580+
>
581+
<View style={{ flex: 1 }}>
582+
{resumeCommandBlock.lines.map((line, index) => (
583+
<Text
584+
key={`${line}-${index}`}
585+
style={{
586+
color: theme.colors.text,
587+
fontSize: 13,
588+
lineHeight: 18,
589+
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
590+
}}
591+
>
592+
{line}
593+
</Text>
594+
))}
595+
</View>
596+
<Ionicons
597+
name={copied ? 'checkmark' : 'copy-outline'}
598+
size={16}
599+
color={copied ? '#30D158' : theme.colors.textSecondary}
600+
style={{ marginTop: 1 }}
601+
/>
602+
</Pressable>
603+
);
604+
}
605+
584606
function CenteredInputWidth(props: {
585607
children: React.ReactNode;
586608
horizontalPadding: number;

0 commit comments

Comments
 (0)