diff --git a/packages/happy-app/sources/app/(app)/session/[id]/info.tsx b/packages/happy-app/sources/app/(app)/session/[id]/info.tsx
index 59c12cf25..23c0a5eba 100644
--- a/packages/happy-app/sources/app/(app)/session/[id]/info.tsx
+++ b/packages/happy-app/sources/app/(app)/session/[id]/info.tsx
@@ -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);
@@ -355,6 +358,15 @@ function SessionInfoContent({ session }: { session: Session }) {
onPress={resumeSession}
/>
)}
+ {canResumeInPlace && (
+ }
+ onPress={resumeInPlace}
+ loading={resumingInPlace}
+ />
+ )}
{sessionStatus.isConnected && (
)
}
-// 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[][][],
@@ -310,26 +310,30 @@ function RenderTableBlock(props: {
style={style.tableScrollView}
>
- {/* Render each column as a vertical container */}
- {props.headers.map((header, colIndex) => (
-
- {/* Header cell for this column */}
-
+ {/* Header row */}
+
+ {props.headers.map((header, colIndex) => (
+
- {/* Data cells for this column */}
- {props.rows.map((row, rowIndex) => (
+ ))}
+
+ {/* Data rows */}
+ {props.rows.map((row, rowIndex) => (
+
+ {props.headers.map((_, colIndex) => (
@@ -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,
diff --git a/packages/happy-app/sources/hooks/useSessionQuickActions.ts b/packages/happy-app/sources/hooks/useSessionQuickActions.ts
index 13b49554e..a6c8e3837 100644
--- a/packages/happy-app/sources/hooks/useSessionQuickActions.ts
+++ b/packages/happy-app/sources/hooks/useSessionQuickActions.ts
@@ -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';
@@ -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);
@@ -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,
};
}
diff --git a/packages/happy-app/sources/sync/ops.ts b/packages/happy-app/sources/sync/ops.ts
index a406acf95..ece040930 100644
--- a/packages/happy-app/sources/sync/ops.ts
+++ b/packages/happy-app/sources/sync/ops.ts
@@ -502,6 +502,31 @@ export async function sessionKill(sessionId: string): Promise {
+ try {
+ const response = await apiSocket.sessionRPC(
+ 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)
diff --git a/packages/happy-app/sources/text/_default.ts b/packages/happy-app/sources/text/_default.ts
index fbf1d88ba..02c2c45b7 100644
--- a/packages/happy-app/sources/text/_default.ts
+++ b/packages/happy-app/sources/text/_default.ts
@@ -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.',
diff --git a/packages/happy-app/sources/text/translations/ca.ts b/packages/happy-app/sources/text/translations/ca.ts
index 0e4a4a9fa..a3fbfcfd7 100644
--- a/packages/happy-app/sources/text/translations/ca.ts
+++ b/packages/happy-app/sources/text/translations/ca.ts
@@ -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.',
diff --git a/packages/happy-app/sources/text/translations/en.ts b/packages/happy-app/sources/text/translations/en.ts
index 1a78bb3f9..bf006ebb2 100644
--- a/packages/happy-app/sources/text/translations/en.ts
+++ b/packages/happy-app/sources/text/translations/en.ts
@@ -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.',
diff --git a/packages/happy-app/sources/text/translations/es.ts b/packages/happy-app/sources/text/translations/es.ts
index 8a80378d3..6f88632e6 100644
--- a/packages/happy-app/sources/text/translations/es.ts
+++ b/packages/happy-app/sources/text/translations/es.ts
@@ -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.',
diff --git a/packages/happy-app/sources/text/translations/it.ts b/packages/happy-app/sources/text/translations/it.ts
index 6cf83b86a..749179e32 100644
--- a/packages/happy-app/sources/text/translations/it.ts
+++ b/packages/happy-app/sources/text/translations/it.ts
@@ -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.',
diff --git a/packages/happy-app/sources/text/translations/ja.ts b/packages/happy-app/sources/text/translations/ja.ts
index 4b552f303..bce86659b 100644
--- a/packages/happy-app/sources/text/translations/ja.ts
+++ b/packages/happy-app/sources/text/translations/ja.ts
@@ -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.',
diff --git a/packages/happy-app/sources/text/translations/pl.ts b/packages/happy-app/sources/text/translations/pl.ts
index d0a8586b4..3040ed34f 100644
--- a/packages/happy-app/sources/text/translations/pl.ts
+++ b/packages/happy-app/sources/text/translations/pl.ts
@@ -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.',
diff --git a/packages/happy-app/sources/text/translations/pt.ts b/packages/happy-app/sources/text/translations/pt.ts
index f4e167523..64d32b0de 100644
--- a/packages/happy-app/sources/text/translations/pt.ts
+++ b/packages/happy-app/sources/text/translations/pt.ts
@@ -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.',
diff --git a/packages/happy-app/sources/text/translations/ru.ts b/packages/happy-app/sources/text/translations/ru.ts
index 8895244c0..ccfb4188c 100644
--- a/packages/happy-app/sources/text/translations/ru.ts
+++ b/packages/happy-app/sources/text/translations/ru.ts
@@ -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.',
diff --git a/packages/happy-app/sources/text/translations/zh-Hans.ts b/packages/happy-app/sources/text/translations/zh-Hans.ts
index 1ef02fdef..83beb83a2 100644
--- a/packages/happy-app/sources/text/translations/zh-Hans.ts
+++ b/packages/happy-app/sources/text/translations/zh-Hans.ts
@@ -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.',
diff --git a/packages/happy-app/sources/text/translations/zh-Hant.ts b/packages/happy-app/sources/text/translations/zh-Hant.ts
index 4926595d0..e4f574ed6 100644
--- a/packages/happy-app/sources/text/translations/zh-Hant.ts
+++ b/packages/happy-app/sources/text/translations/zh-Hant.ts
@@ -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.',
diff --git a/packages/happy-cli/src/claude/claudeRemote.ts b/packages/happy-cli/src/claude/claudeRemote.ts
index d93215c8c..b063d1d2a 100644
--- a/packages/happy-cli/src/claude/claudeRemote.ts
+++ b/packages/happy-cli/src/claude/claudeRemote.ts
@@ -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,
claudeEnvVars?: Record,
@@ -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++) {
@@ -145,6 +148,8 @@ export async function claudeRemote(opts: {
}
};
+
+
// Push initial message
let messages = new PushableAsyncIterable();
messages.push({
diff --git a/packages/happy-cli/src/claude/claudeRemoteLauncher.ts b/packages/happy-cli/src/claude/claudeRemoteLauncher.ts
index 79f6e980f..1daa7a66a 100644
--- a/packages/happy-cli/src/claude/claudeRemoteLauncher.ts
+++ b/packages/happy-cli/src/claude/claudeRemoteLauncher.ts
@@ -16,6 +16,7 @@ import { RawJSONLines } from "@/claude/types";
import { OutgoingMessageQueue } from "./utils/OutgoingMessageQueue";
import { getToolName } from "./utils/getToolName";
import { getAskUserQuestionToolCallIds } from "./utils/questionNotification";
+import { registerResumeSessionHandler } from "./registerResumeSessionHandler";
interface PermissionsField {
date: number;
@@ -95,6 +96,7 @@ export async function claudeRemoteLauncher(session: Session): Promise<'switch' |
// When to abort
session.client.rpcHandlerManager.registerHandler('abort', doAbort); // When abort clicked
session.client.rpcHandlerManager.registerHandler('switch', doSwitch); // When switch clicked
+ registerResumeSessionHandler(session.client.rpcHandlerManager, session, abort); // When resume clicked
// Removed catch-all stdin handler - now handled by RemoteModeDisplay keyboard handlers
// Create permission handler
@@ -345,9 +347,11 @@ export async function claudeRemoteLauncher(session: Session): Promise<'switch' |
abortFuture = new Future();
let modeHash: string | null = null;
let mode: EnhancedMode | null = null;
+ const pendingResumeId = session.consumePendingResume();
try {
const remoteResult = await claudeRemote({
sessionId: session.sessionId,
+ resumeFromSessionId: pendingResumeId,
path: session.path,
allowedTools: session.allowedTools ?? [],
mcpServers: session.mcpServers,
diff --git a/packages/happy-cli/src/claude/registerResumeSessionHandler.ts b/packages/happy-cli/src/claude/registerResumeSessionHandler.ts
new file mode 100644
index 000000000..62d3c3607
--- /dev/null
+++ b/packages/happy-cli/src/claude/registerResumeSessionHandler.ts
@@ -0,0 +1,35 @@
+import { RpcHandlerManager } from "@/api/rpc/RpcHandlerManager";
+import { logger } from "@/lib";
+import { Session } from "./session";
+
+interface ResumeSessionRequest {
+ // No parameters needed
+}
+
+interface ResumeSessionResponse {
+ success: boolean;
+ message: string;
+}
+
+export function registerResumeSessionHandler(
+ rpcHandlerManager: RpcHandlerManager,
+ session: Session,
+ abortCurrentSession: () => Promise
+) {
+ rpcHandlerManager.registerHandler('resumeSession', async () => {
+ logger.debug('[resumeSession] Resume session request received');
+
+ const result = session.requestResume();
+ if (!result.success) {
+ return result;
+ }
+
+ // Abort the current Claude process so the loop restarts with --resume
+ await abortCurrentSession();
+
+ return {
+ success: true,
+ message: 'Session resume initiated'
+ };
+ });
+}
diff --git a/packages/happy-cli/src/claude/runClaude.ts b/packages/happy-cli/src/claude/runClaude.ts
index b626c62b6..cdf010766 100644
--- a/packages/happy-cli/src/claude/runClaude.ts
+++ b/packages/happy-cli/src/claude/runClaude.ts
@@ -263,6 +263,8 @@ export async function runClaude(credentials: Credentials, options: StartOptions
let currentAppendSystemPrompt: string | undefined = undefined; // Track current append system prompt
let currentAllowedTools: string[] | undefined = undefined; // Track current allowed tools
let currentDisallowedTools: string[] | undefined = undefined; // Track current disallowed tools
+
+
session.onUserMessage((message) => {
// Resolve permission mode from meta - pass through as-is, mapping happens at SDK boundary
diff --git a/packages/happy-cli/src/claude/session.ts b/packages/happy-cli/src/claude/session.ts
index 8fc67efca..62a01955e 100644
--- a/packages/happy-cli/src/claude/session.ts
+++ b/packages/happy-cli/src/claude/session.ts
@@ -25,6 +25,9 @@ export class Session {
sessionId: string | null;
mode: 'local' | 'remote' = 'local';
thinking: boolean = false;
+
+ /** When set, the next claudeRemote launch will use --resume with this session ID */
+ pendingResumeSessionId: string | null = null;
/** Callbacks to be notified when session ID is found/changed */
private sessionFoundCallbacks: ((sessionId: string) => void)[] = [];
@@ -144,6 +147,30 @@ export class Session {
logger.debug('[Session] Session ID cleared');
}
+ /**
+ * Request a session resume. Stores the current session ID so the next
+ * claudeRemote launch uses --resume to fork the session with full history.
+ */
+ requestResume = (): { success: boolean; message: string } => {
+ if (!this.sessionId) {
+ return { success: false, message: 'No active Claude session to resume from' };
+ }
+ this.pendingResumeSessionId = this.sessionId;
+ this.sessionId = null;
+ logger.debug(`[Session] Resume requested from session: ${this.pendingResumeSessionId}`);
+ return { success: true, message: 'Resume requested' };
+ }
+
+ /**
+ * Consume the pending resume session ID (called by claudeRemoteLauncher).
+ * Returns the session ID to resume from, or null if no resume was requested.
+ */
+ consumePendingResume = (): string | null => {
+ const id = this.pendingResumeSessionId;
+ this.pendingResumeSessionId = null;
+ return id;
+ }
+
/**
* Consume one-time Claude flags from claudeArgs after Claude spawn
* Handles: --resume (with or without session ID), --continue