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
12 changes: 12 additions & 0 deletions packages/happy-app/sources/app/(app)/session/[id]/info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,10 @@ function SessionInfoContent({ session }: { session: Session }) {
const sessionName = getSessionName(session);
const sessionStatus = useSessionStatus(session);
const {
canResumeInPlace,
canShowResume,
resumeInPlace,
resumingInPlace,
resumeSession,
resumeSessionSubtitle,
} = useSessionQuickActions(session);
Expand Down Expand Up @@ -355,6 +358,15 @@ function SessionInfoContent({ session }: { session: Session }) {
onPress={resumeSession}
/>
)}
{canResumeInPlace && (
<Item
title={t('sessionInfo.resumeInPlace')}
subtitle={t('sessionInfo.resumeInPlaceSubtitle')}
icon={<Ionicons name="refresh-circle-outline" size={29} color="#007AFF" />}
onPress={resumeInPlace}
loading={resumingInPlace}
/>
)}
{sessionStatus.isConnected && (
<Item
title={t('sessionInfo.archiveSession')}
Expand Down
13 changes: 13 additions & 0 deletions packages/happy-app/sources/components/SessionActionsPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,11 @@ export function SessionActionsPopover({
archiveSession,
canArchive,
canCopySessionMetadata,
canResumeInPlace,
canShowResume,
copySessionMetadata,
openDetails,
resumeInPlace,
resumeSession,
} = useSessionQuickActions(session, {
onAfterArchive,
Expand All @@ -146,6 +148,15 @@ export function SessionActionsPopover({
},
];

if (canResumeInPlace) {
items.push({
id: 'resume-in-place',
icon: 'refresh-circle-outline',
label: t('sessionInfo.resumeInPlace'),
onPress: resumeInPlace,
});
}

if (canArchive) {
items.push({
id: 'archive',
Expand Down Expand Up @@ -178,9 +189,11 @@ export function SessionActionsPopover({
archiveSession,
canArchive,
canCopySessionMetadata,
canResumeInPlace,
canShowResume,
copySessionMetadata,
openDetails,
resumeInPlace,
resumeSession,
]);

Expand Down
63 changes: 32 additions & 31 deletions packages/happy-app/sources/components/markdown/MarkdownView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -286,9 +286,9 @@ function RenderSpans(props: RenderSpanProps) {
</>)
}

// Table rendering uses column-first layout to ensure consistent column widths.
// Each column is rendered as a vertical container with all its cells (header + data).
// This ensures that cells in the same column have the same width, determined by the widest content.
// Table rendering uses row-first layout to ensure consistent row heights.
// Each row is rendered as a horizontal container with all its cells.
// This ensures that cells in the same row have the same height.
function RenderTableBlock(props: {
headers: MarkdownSpan[][],
rows: MarkdownSpan[][][],
Expand All @@ -310,26 +310,30 @@ function RenderTableBlock(props: {
style={style.tableScrollView}
>
<View style={style.tableContent}>
{/* Render each column as a vertical container */}
{props.headers.map((header, colIndex) => (
<View
key={`column-${colIndex}`}
style={[
style.tableColumn,
colIndex === columnCount - 1 && style.tableColumnLast
]}
>
{/* Header cell for this column */}
<View style={[style.tableCell, style.tableHeaderCell, style.tableCellFirst]}>
{/* Header row */}
<View style={style.tableRow}>
{props.headers.map((header, colIndex) => (
<View
key={`header-${colIndex}`}
style={[
style.tableCell,
style.tableHeaderCell,
colIndex < columnCount - 1 && style.tableCellBorderRight,
]}
>
<Text style={style.tableHeaderText}><RenderSpans spans={header} baseStyle={style.tableHeaderText} onLinkPress={props.onLinkPress} selectable={props.selectable} /></Text>
</View>
{/* Data cells for this column */}
{props.rows.map((row, rowIndex) => (
))}
</View>
{/* Data rows */}
{props.rows.map((row, rowIndex) => (
<View key={`row-${rowIndex}`} style={[style.tableRow, isLastRow(rowIndex) && style.tableRowLast]}>
{props.headers.map((_, colIndex) => (
<View
key={`cell-${rowIndex}-${colIndex}`}
style={[
style.tableCell,
isLastRow(rowIndex) && style.tableCellLast
colIndex < columnCount - 1 && style.tableCellBorderRight,
]}
>
<Text style={style.tableCellText}><RenderSpans spans={row[colIndex] ?? []} baseStyle={style.tableCellText} onLinkPress={props.onLinkPress} selectable={props.selectable} /></Text>
Expand Down Expand Up @@ -591,28 +595,25 @@ const style = StyleSheet.create((theme) => ({
flexGrow: 0,
},
tableContent: {
flexDirection: 'row',
},
tableColumn: {
flexDirection: 'column',
borderRightWidth: 1,
borderRightColor: theme.colors.divider,
},
tableColumnLast: {
borderRightWidth: 0,
tableRow: {
flexDirection: 'row',
borderBottomWidth: 1,
borderBottomColor: theme.colors.divider,
},
tableRowLast: {
borderBottomWidth: 0,
},
tableCell: {
flex: 1,
paddingHorizontal: 12,
paddingVertical: 8,
borderBottomWidth: 1,
borderBottomColor: theme.colors.divider,
alignItems: 'flex-start',
},
tableCellFirst: {
borderTopWidth: 0,
},
tableCellLast: {
borderBottomWidth: 0,
tableCellBorderRight: {
borderRightWidth: 1,
borderRightColor: theme.colors.divider,
},
tableHeaderCell: {
backgroundColor: theme.colors.surfaceHigh,
Expand Down
17 changes: 16 additions & 1 deletion packages/happy-app/sources/hooks/useSessionQuickActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react';
import { useHappyAction } from '@/hooks/useHappyAction';
import { useNavigateToSession } from '@/hooks/useNavigateToSession';
import { Modal } from '@/modal';
import { machineResumeSession, sessionKill } from '@/sync/ops';
import { machineResumeSession, sessionKill, sessionResume } from '@/sync/ops';
import { maybeCleanupWorktree } from '@/hooks/useWorktreeCleanup';
import { storage, useLocalSetting, useMachine, useSetting } from '@/sync/storage';
import { Machine, Session } from '@/sync/storageTypes';
Expand Down Expand Up @@ -169,6 +169,18 @@ export function useSessionQuickActions(
}
});

// In-place resume: fork the active session with full history (fixes corrupted context)
const [resumingInPlace, performResumeInPlace] = useHappyAction(async () => {
const result = await sessionResume(session.id);
if (!result.success) {
throw new HappyError(result.message, false);
}
});

const resumeInPlace = React.useCallback(() => {
performResumeInPlace();
}, [performResumeInPlace]);

const [archivingSession, performArchive] = useHappyAction(async () => {
await maybeCleanupWorktree(session.id, session.metadata?.path, session.metadata?.machineId);

Expand All @@ -193,11 +205,14 @@ export function useSessionQuickActions(
canArchive: sessionStatus.isConnected,
canCopySessionMetadata: __DEV__ || devModeEnabled,
canResume: resumeAvailability.canResume,
canResumeInPlace: sessionStatus.isConnected,
canShowResume: resumeAvailability.canShowResume,
copySessionMetadata,
openDetails,
resumeInPlace,
resumeSession,
resumeSessionSubtitle: resumeAvailability.subtitle,
resumingInPlace,
resumingSession,
};
}
25 changes: 25 additions & 0 deletions packages/happy-app/sources/sync/ops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,31 @@ export async function sessionKill(sessionId: string): Promise<SessionKillRespons
}
}

// Resume session types
interface SessionResumeResponse {
success: boolean;
message: string;
}

/**
* Resume the current Claude session in-place (fork with full history)
*/
export async function sessionResume(sessionId: string): Promise<SessionResumeResponse> {
try {
const response = await apiSocket.sessionRPC<SessionResumeResponse, {}>(
sessionId,
'resumeSession',
{}
);
return response;
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Unknown error'
};
}
}

/**
* Permanently delete a session from the server
* This will remove the session and all its associated data (messages, usage reports, access keys)
Expand Down
2 changes: 2 additions & 0 deletions packages/happy-app/sources/text/_default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,8 @@ export const en = {
quickActions: 'Quick Actions',
viewMachine: 'View Machine',
viewMachineSubtitle: 'View machine details and sessions',
resumeInPlace: 'Restart Session',
resumeInPlaceSubtitle: 'Start fresh with full conversation history',
resumeSession: 'Resume Session',
resumeSessionSubtitle: 'Resume this session on the same machine',
resumeSessionSameMachineOnly: 'This session can only be resumed on the same machine it started on.',
Expand Down
2 changes: 2 additions & 0 deletions packages/happy-app/sources/text/translations/ca.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,8 @@ export const ca: TranslationStructure = {
viewMachine: 'Veure la màquina',
viewMachineSubtitle: 'Veure detalls de la màquina i sessions',
resumeSession: 'Resume Session',
resumeInPlace: 'Reiniciar sessió',
resumeInPlaceSubtitle: 'Reiniciar amb l\'historial complet de conversa',
resumeSessionSubtitle: 'Resume this session on the same machine',
resumeSessionSameMachineOnly: 'This session can only be resumed on the same machine it started on.',
resumeSessionMachineOffline: 'This machine is offline. Resume is only available while it is online.',
Expand Down
2 changes: 2 additions & 0 deletions packages/happy-app/sources/text/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,8 @@ export const en: TranslationStructure = {
viewMachine: 'View Machine',
viewMachineSubtitle: 'View machine details and sessions',
resumeSession: 'Resume Session',
resumeInPlace: 'Restart Session',
resumeInPlaceSubtitle: 'Start fresh with full conversation history',
resumeSessionSubtitle: 'Resume this session on the same machine',
resumeSessionSameMachineOnly: 'This session can only be resumed on the same machine it started on.',
resumeSessionMachineOffline: 'This machine is offline. Resume is only available while it is online.',
Expand Down
2 changes: 2 additions & 0 deletions packages/happy-app/sources/text/translations/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,8 @@ export const es: TranslationStructure = {
viewMachine: 'Ver máquina',
viewMachineSubtitle: 'Ver detalles de máquina y sesiones',
resumeSession: 'Resume Session',
resumeInPlace: 'Reiniciar sesión',
resumeInPlaceSubtitle: 'Reiniciar con el historial completo de conversación',
resumeSessionSubtitle: 'Resume this session on the same machine',
resumeSessionSameMachineOnly: 'This session can only be resumed on the same machine it started on.',
resumeSessionMachineOffline: 'This machine is offline. Resume is only available while it is online.',
Expand Down
2 changes: 2 additions & 0 deletions packages/happy-app/sources/text/translations/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,8 @@ export const it: TranslationStructure = {
viewMachine: 'Visualizza macchina',
viewMachineSubtitle: 'Visualizza dettagli e sessioni della macchina',
resumeSession: 'Resume Session',
resumeInPlace: 'Riavvia sessione',
resumeInPlaceSubtitle: 'Ricomincia con la cronologia completa della conversazione',
resumeSessionSubtitle: 'Resume this session on the same machine',
resumeSessionSameMachineOnly: 'This session can only be resumed on the same machine it started on.',
resumeSessionMachineOffline: 'This machine is offline. Resume is only available while it is online.',
Expand Down
2 changes: 2 additions & 0 deletions packages/happy-app/sources/text/translations/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,8 @@ export const ja: TranslationStructure = {
viewMachine: 'マシンを表示',
viewMachineSubtitle: 'マシンの詳細とセッションを表示',
resumeSession: 'Resume Session',
resumeInPlace: 'セッションを再起動',
resumeInPlaceSubtitle: '会話履歴を保持したまま再開',
resumeSessionSubtitle: 'Resume this session on the same machine',
resumeSessionSameMachineOnly: 'This session can only be resumed on the same machine it started on.',
resumeSessionMachineOffline: 'This machine is offline. Resume is only available while it is online.',
Expand Down
2 changes: 2 additions & 0 deletions packages/happy-app/sources/text/translations/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,8 @@ export const pl: TranslationStructure = {
viewMachine: 'Zobacz maszynę',
viewMachineSubtitle: 'Zobacz szczegóły maszyny i sesje',
resumeSession: 'Resume Session',
resumeInPlace: 'Uruchom sesję ponownie',
resumeInPlaceSubtitle: 'Zacznij od nowa z pełną historią rozmowy',
resumeSessionSubtitle: 'Resume this session on the same machine',
resumeSessionSameMachineOnly: 'This session can only be resumed on the same machine it started on.',
resumeSessionMachineOffline: 'This machine is offline. Resume is only available while it is online.',
Expand Down
2 changes: 2 additions & 0 deletions packages/happy-app/sources/text/translations/pt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,8 @@ export const pt: TranslationStructure = {
viewMachine: 'Ver máquina',
viewMachineSubtitle: 'Ver detalhes da máquina e sessões',
resumeSession: 'Resume Session',
resumeInPlace: 'Reiniciar sessão',
resumeInPlaceSubtitle: 'Reiniciar com o histórico completo da conversa',
resumeSessionSubtitle: 'Resume this session on the same machine',
resumeSessionSameMachineOnly: 'This session can only be resumed on the same machine it started on.',
resumeSessionMachineOffline: 'This machine is offline. Resume is only available while it is online.',
Expand Down
2 changes: 2 additions & 0 deletions packages/happy-app/sources/text/translations/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,8 @@ export const ru: TranslationStructure = {
viewMachine: 'Посмотреть машину',
viewMachineSubtitle: 'Посмотреть детали машины и сессии',
resumeSession: 'Resume Session',
resumeInPlace: 'Перезапустить сессию',
resumeInPlaceSubtitle: 'Начать заново с полной историей разговора',
resumeSessionSubtitle: 'Resume this session on the same machine',
resumeSessionSameMachineOnly: 'This session can only be resumed on the same machine it started on.',
resumeSessionMachineOffline: 'This machine is offline. Resume is only available while it is online.',
Expand Down
2 changes: 2 additions & 0 deletions packages/happy-app/sources/text/translations/zh-Hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,8 @@ export const zhHans: TranslationStructure = {
viewMachine: '查看设备',
viewMachineSubtitle: '查看设备详情和会话',
resumeSession: 'Resume Session',
resumeInPlace: '重启会话',
resumeInPlaceSubtitle: '保留完整对话历史重新开始',
resumeSessionSubtitle: 'Resume this session on the same machine',
resumeSessionSameMachineOnly: 'This session can only be resumed on the same machine it started on.',
resumeSessionMachineOffline: 'This machine is offline. Resume is only available while it is online.',
Expand Down
2 changes: 2 additions & 0 deletions packages/happy-app/sources/text/translations/zh-Hant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,8 @@ export const zhHant: TranslationStructure = {
viewMachine: '查看裝置',
viewMachineSubtitle: '查看裝置詳情和工作階段',
resumeSession: 'Resume Session',
resumeInPlace: '重啟會話',
resumeInPlaceSubtitle: '保留完整對話歷史重新開始',
resumeSessionSubtitle: 'Resume this session on the same machine',
resumeSessionSameMachineOnly: 'This session can only be resumed on the same machine it started on.',
resumeSessionMachineOffline: 'This machine is offline. Resume is only available while it is online.',
Expand Down
11 changes: 8 additions & 3 deletions packages/happy-cli/src/claude/claudeRemote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export async function claudeRemote(opts: {

// Fixed parameters
sessionId: string | null,
/** Override: resume from this session ID instead of continuing the current one */
resumeFromSessionId?: string | null,
path: string,
mcpServers?: Record<string, any>,
claudeEnvVars?: Record<string, string>,
Expand All @@ -43,11 +45,12 @@ export async function claudeRemote(opts: {
}) {

// Check if session is valid
let startFrom = opts.sessionId;
if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
// resumeFromSessionId takes priority (in-place resume from web UI)
let startFrom = opts.resumeFromSessionId ?? opts.sessionId;
if (startFrom && !claudeCheckSession(startFrom, opts.path)) {
startFrom = null;
}

// Extract --resume from claudeArgs if present (for first spawn)
if (!startFrom && opts.claudeArgs) {
for (let i = 0; i < opts.claudeArgs.length; i++) {
Expand Down Expand Up @@ -145,6 +148,8 @@ export async function claudeRemote(opts: {
}
};



// Push initial message
let messages = new PushableAsyncIterable<SDKUserMessage>();
messages.push({
Expand Down
Loading