Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion packages/happy-app/sources/app/(app)/session/[id]/info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import { useSession, useIsDataReady } from '@/sync/storage';
import { getSessionName, useSessionStatus, formatOSPlatform, formatPathRelativeToHome, getSessionAvatarId } from '@/utils/sessionUtils';
import * as Clipboard from 'expo-clipboard';
import { Modal } from '@/modal';
import { sessionKill, sessionDelete } from '@/sync/ops';
import { sessionKill, sessionDelete, machineResumeSession } from '@/sync/ops';
import { useMachine } from '@/sync/storage';
import { isMachineOnline } from '@/utils/machineUtils';
import { sync } from '@/sync/sync';
import { useUnistyles } from 'react-native-unistyles';
import { layout } from '@/components/layout';
import { t } from '@/text';
Expand Down Expand Up @@ -200,6 +203,33 @@ function SessionInfoContent({ session }: { session: Session }) {
);
}, [performDelete]);

// Resume session
const machine = useMachine(session.metadata?.machineId || '');
const machineOnline = machine ? isMachineOnline(machine) : false;
const canResume = !sessionStatus.isConnected && !session.active
&& !!session.metadata?.claudeSessionId && !!session.metadata?.machineId;

const [resumingSession, performResume] = useHappyAction(async () => {
if (!session.metadata?.claudeSessionId || !session.metadata?.machineId || !session.metadata?.path) return;

const result = await machineResumeSession({
machineId: session.metadata.machineId,
directory: session.metadata.path,
claudeSessionId: session.metadata.claudeSessionId,
agent: (session.metadata?.flavor as 'claude' | 'codex' | 'gemini') || 'claude',
});

if (result.type !== 'success') {
throw new HappyError(
'errorMessage' in result ? result.errorMessage : t('sessionInfo.failedToResumeSession'),
false
);
}

await sync.refreshSessions();
router.replace(`/session/${result.sessionId}`);
});

const formatDate = useCallback((timestamp: number) => {
return new Date(timestamp).toLocaleString();
}, []);
Expand Down Expand Up @@ -309,6 +339,17 @@ function SessionInfoContent({ session }: { session: Session }) {

{/* Quick Actions */}
<ItemGroup title={t('sessionInfo.quickActions')}>
{canResume && (
<Item
title={t('sessionInfo.resumeSession')}
subtitle={machineOnline
? t('sessionInfo.resumeSessionSubtitle')
: t('sessionInfo.resumeSessionMachineOffline')}
icon={<Ionicons name="play-outline" size={29} color={machineOnline ? "#34C759" : "#8E8E93"} />}
onPress={machineOnline ? performResume : undefined}
loading={resumingSession}
/>
)}
{session.metadata?.machineId && (
<Item
title={t('sessionInfo.viewMachine')}
Expand Down
78 changes: 65 additions & 13 deletions packages/happy-app/sources/components/SessionsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@ import { useRouter } from 'expo-router';
import { Item } from './Item';
import { ItemGroup } from './ItemGroup';
import { useHappyAction } from '@/hooks/useHappyAction';
import { sessionDelete } from '@/sync/ops';
import { sessionDelete, machineResumeSession } from '@/sync/ops';
import { HappyError } from '@/utils/errors';
import { useMachine } from '@/sync/storage';
import { isMachineOnline } from '@/utils/machineUtils';
import { sync } from '@/sync/sync';
import { Modal } from '@/modal';

const stylesheet = StyleSheet.create((theme) => ({
Expand Down Expand Up @@ -179,12 +182,19 @@ const stylesheet = StyleSheet.create((theme) => ({
backgroundColor: theme.colors.groupped.background,
},
swipeAction: {
width: 112,
width: 80,
height: '100%',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: theme.colors.status.error,
},
swipeActionResume: {
width: 80,
height: '100%',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#34C759',
},
swipeActionText: {
marginTop: 4,
fontSize: 12,
Expand Down Expand Up @@ -362,6 +372,34 @@ const SessionItem = React.memo(({ session, selected, isFirst, isLast, isSingle }
);
}, [performDelete]);

const canResume = !sessionStatus.isConnected && !session.active
&& !!session.metadata?.claudeSessionId && !!session.metadata?.machineId;
const machine = useMachine(session.metadata?.machineId || '');
const machineOnline = machine ? isMachineOnline(machine) : false;

const [resumingSession, performResume] = useHappyAction(async () => {
if (!session.metadata?.claudeSessionId || !session.metadata?.machineId || !session.metadata?.path) return;
const result = await machineResumeSession({
machineId: session.metadata.machineId,
directory: session.metadata.path,
claudeSessionId: session.metadata.claudeSessionId,
agent: (session.metadata?.flavor as 'claude' | 'codex' | 'gemini') || 'claude',
});
if (result.type !== 'success') {
throw new HappyError(
'errorMessage' in result ? result.errorMessage : t('sessionInfo.failedToResumeSession'),
false
);
}
await sync.refreshSessions();
navigateToSession(result.sessionId);
});

const handleResume = React.useCallback(() => {
swipeableRef.current?.close();
performResume();
}, [performResume]);

const avatarId = React.useMemo(() => {
return getSessionAvatarId(session);
}, [session]);
Expand Down Expand Up @@ -446,16 +484,30 @@ const SessionItem = React.memo(({ session, selected, isFirst, isLast, isSingle }
}

const renderRightActions = () => (
<Pressable
style={styles.swipeAction}
onPress={handleDelete}
disabled={deletingSession}
>
<Ionicons name="trash-outline" size={20} color="#FFFFFF" />
<Text style={styles.swipeActionText} numberOfLines={2}>
{t('sessionInfo.deleteSession')}
</Text>
</Pressable>
<View style={{ flexDirection: 'row' }}>
{canResume && machineOnline && (
<Pressable
style={styles.swipeActionResume}
onPress={handleResume}
disabled={resumingSession}
>
<Ionicons name="play-outline" size={20} color="#FFFFFF" />
<Text style={styles.swipeActionText} numberOfLines={1}>
{t('sessionInfo.resumeSession')}
</Text>
</Pressable>
)}
<Pressable
style={styles.swipeAction}
onPress={handleDelete}
disabled={deletingSession}
>
<Ionicons name="trash-outline" size={20} color="#FFFFFF" />
<Text style={styles.swipeActionText} numberOfLines={2}>
{t('sessionInfo.deleteSession')}
</Text>
</Pressable>
</View>
);

return (
Expand All @@ -464,7 +516,7 @@ const SessionItem = React.memo(({ session, selected, isFirst, isLast, isSingle }
ref={swipeableRef}
renderRightActions={renderRightActions}
overshootRight={false}
enabled={!deletingSession}
enabled={!deletingSession && !resumingSession}
>
{itemContent}
</Swipeable>
Expand Down
35 changes: 35 additions & 0 deletions packages/happy-app/sources/sync/ops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,41 @@ export async function machineSpawnNewSession(options: SpawnSessionOptions): Prom
}
}

/**
* Resume a previous Claude session on a specific machine.
* Creates a new Happy session but continues the Claude conversation via --resume.
*/
export async function machineResumeSession(options: {
machineId: string;
directory: string;
claudeSessionId: string;
agent?: 'codex' | 'claude' | 'gemini';
environmentVariables?: Record<string, string>;
}): Promise<SpawnSessionResult> {
const { machineId, directory, claudeSessionId, agent, environmentVariables } = options;

try {
const result = await apiSocket.machineRPC<SpawnSessionResult, {
type: 'spawn-in-directory'
directory: string
sessionId: string
approvedNewDirectoryCreation: boolean
agent?: 'codex' | 'claude' | 'gemini'
environmentVariables?: Record<string, string>
}>(
machineId,
'spawn-happy-session',
{ type: 'spawn-in-directory', directory, sessionId: claudeSessionId, approvedNewDirectoryCreation: false, agent, environmentVariables }
);
return result;
} catch (error) {
return {
type: 'error',
errorMessage: error instanceof Error ? error.message : 'Failed to resume session'
};
}
}

/**
* Stop the daemon on a specific machine
*/
Expand Down
6 changes: 5 additions & 1 deletion packages/happy-app/sources/text/_default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,11 @@ export const en = {
deleteSessionWarning: 'This action cannot be undone. All messages and data associated with this session will be permanently deleted.',
failedToDeleteSession: 'Failed to delete session',
sessionDeleted: 'Session deleted successfully',

resumeSession: 'Resume Session',
resumeSessionSubtitle: 'Continue this conversation in a new session',
resumeSessionMachineOffline: 'Machine is offline',
failedToResumeSession: 'Failed to resume session',

},

components: {
Expand Down
4 changes: 4 additions & 0 deletions packages/happy-app/sources/text/translations/ca.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,10 @@ export const ca: TranslationStructure = {
deleteSessionWarning: 'Aquesta acció no es pot desfer. Tots els missatges i dades associats amb aquesta sessió s\'eliminaran permanentment.',
failedToDeleteSession: 'Error en eliminar la sessió',
sessionDeleted: 'Sessió eliminada amb èxit',
resumeSession: 'Reprendre la sessió',
resumeSessionSubtitle: 'Continuar aquesta conversa en una nova sessió',
resumeSessionMachineOffline: 'La màquina està fora de línia',
failedToResumeSession: 'Error en reprendre la sessió',

},

Expand Down
4 changes: 4 additions & 0 deletions packages/happy-app/sources/text/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,10 @@ export const en: TranslationStructure = {
deleteSessionWarning: 'This action cannot be undone. All messages and data associated with this session will be permanently deleted.',
failedToDeleteSession: 'Failed to delete session',
sessionDeleted: 'Session deleted successfully',
resumeSession: 'Resume Session',
resumeSessionSubtitle: 'Continue this conversation in a new session',
resumeSessionMachineOffline: 'Machine is offline',
failedToResumeSession: 'Failed to resume session',

},

Expand Down
4 changes: 4 additions & 0 deletions packages/happy-app/sources/text/translations/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,10 @@ export const es: TranslationStructure = {
deleteSessionWarning: 'Esta acción no se puede deshacer. Todos los mensajes y datos asociados con esta sesión se eliminarán permanentemente.',
failedToDeleteSession: 'Error al eliminar la sesión',
sessionDeleted: 'Sesión eliminada exitosamente',
resumeSession: 'Reanudar sesión',
resumeSessionSubtitle: 'Continuar esta conversación en una nueva sesión',
resumeSessionMachineOffline: 'La máquina está sin conexión',
failedToResumeSession: 'Error al reanudar la sesión',

},

Expand Down
4 changes: 4 additions & 0 deletions packages/happy-app/sources/text/translations/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,10 @@ export const it: TranslationStructure = {
deleteSessionWarning: 'Questa azione non può essere annullata. Tutti i messaggi e i dati associati a questa sessione verranno eliminati definitivamente.',
failedToDeleteSession: 'Impossibile eliminare la sessione',
sessionDeleted: 'Sessione eliminata con successo',
resumeSession: 'Riprendi sessione',
resumeSessionSubtitle: 'Continua questa conversazione in una nuova sessione',
resumeSessionMachineOffline: 'La macchina è offline',
failedToResumeSession: 'Impossibile riprendere la sessione',

},

Expand Down
4 changes: 4 additions & 0 deletions packages/happy-app/sources/text/translations/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,10 @@ export const ja: TranslationStructure = {
deleteSessionWarning: 'この操作は取り消せません。このセッションに関連するすべてのメッセージとデータが完全に削除されます。',
failedToDeleteSession: 'セッションの削除に失敗しました',
sessionDeleted: 'セッションが正常に削除されました',
resumeSession: 'セッションを再開',
resumeSessionSubtitle: 'この会話を新しいセッションで続ける',
resumeSessionMachineOffline: 'マシンがオフラインです',
failedToResumeSession: 'セッションの再開に失敗しました',

},

Expand Down
4 changes: 4 additions & 0 deletions packages/happy-app/sources/text/translations/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,10 @@ export const pl: TranslationStructure = {
deleteSessionWarning: 'Ta operacja jest nieodwracalna. Wszystkie wiadomości i dane powiązane z tą sesją zostaną trwale usunięte.',
failedToDeleteSession: 'Nie udało się usunąć sesji',
sessionDeleted: 'Sesja została pomyślnie usunięta',
resumeSession: 'Wznów sesję',
resumeSessionSubtitle: 'Kontynuuj tę rozmowę w nowej sesji',
resumeSessionMachineOffline: 'Maszyna jest offline',
failedToResumeSession: 'Nie udało się wznowić sesji',
},

components: {
Expand Down
4 changes: 4 additions & 0 deletions packages/happy-app/sources/text/translations/pt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,10 @@ export const pt: TranslationStructure = {
deleteSessionWarning: 'Esta ação não pode ser desfeita. Todas as mensagens e dados associados a esta sessão serão excluídos permanentemente.',
failedToDeleteSession: 'Falha ao excluir sessão',
sessionDeleted: 'Sessão excluída com sucesso',
resumeSession: 'Retomar sessão',
resumeSessionSubtitle: 'Continuar esta conversa em uma nova sessão',
resumeSessionMachineOffline: 'A máquina está offline',
failedToResumeSession: 'Falha ao retomar a sessão',

},

Expand Down
4 changes: 4 additions & 0 deletions packages/happy-app/sources/text/translations/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,10 @@ export const ru: TranslationStructure = {
deleteSessionWarning: 'Это действие нельзя отменить. Все сообщения и данные, связанные с этой сессией, будут удалены навсегда.',
failedToDeleteSession: 'Не удалось удалить сессию',
sessionDeleted: 'Сессия успешно удалена',
resumeSession: 'Возобновить сессию',
resumeSessionSubtitle: 'Продолжить этот разговор в новой сессии',
resumeSessionMachineOffline: 'Машина не в сети',
failedToResumeSession: 'Не удалось возобновить сессию',
},

components: {
Expand Down
4 changes: 4 additions & 0 deletions packages/happy-app/sources/text/translations/zh-Hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,10 @@ export const zhHans: TranslationStructure = {
deleteSessionWarning: '此操作无法撤销。与此会话相关的所有消息和数据将被永久删除。',
failedToDeleteSession: '删除会话失败',
sessionDeleted: '会话删除成功',
resumeSession: '恢复会话',
resumeSessionSubtitle: '在新会话中继续此对话',
resumeSessionMachineOffline: '机器离线',
failedToResumeSession: '恢复会话失败',

},

Expand Down
4 changes: 4 additions & 0 deletions packages/happy-app/sources/text/translations/zh-Hant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,10 @@ export const zhHant: TranslationStructure = {
deleteSessionWarning: '此操作無法復原。與此工作階段相關的所有訊息和資料將被永久刪除。',
failedToDeleteSession: '刪除工作階段失敗',
sessionDeleted: '工作階段刪除成功',
resumeSession: '恢復工作階段',
resumeSessionSubtitle: '在新的工作階段中繼續此對話',
resumeSessionMachineOffline: '機器離線',
failedToResumeSession: '恢復工作階段失敗',

},

Expand Down
Loading