Skip to content

Commit 7ced717

Browse files
thetronjohnsonclaude
authored andcommitted
implement tab and session persistence
- Add tab persistence service to save/restore tabs to localStorage - Add session persistence service to save/restore chat session data - Update TabContext to restore saved tabs and session data on app startup - Add settings toggle to enable/disable tab persistence - Fix TypeScript errors in AgentRunWithMetrics interface - Sessions now resume properly when app is reopened 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 5d210e2 commit 7ced717

6 files changed

Lines changed: 529 additions & 26 deletions

File tree

src/components/ClaudeCodeSession.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { WebviewPreview } from "./WebviewPreview";
3535
import type { ClaudeStreamMessage } from "./AgentExecution";
3636
import { useVirtualizer } from "@tanstack/react-virtual";
3737
import { useTrackEvent, useComponentMetrics, useWorkflowTracking } from "@/hooks";
38+
import { SessionPersistenceService } from "@/services/sessionPersistence";
3839

3940
interface ClaudeCodeSessionProps {
4041
/**
@@ -83,6 +84,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
8384
onProjectPathChange,
8485
}) => {
8586
const [projectPath, setProjectPath] = useState(initialProjectPath || session?.project_path || "");
87+
const [sessionRestoreData, setSessionRestoreData] = useState<{ projectId: string } | null>(null);
8688
const [messages, setMessages] = useState<ClaudeStreamMessage[]>([]);
8789
const [isLoading, setIsLoading] = useState(false);
8890
const [error, setError] = useState<string | null>(null);
@@ -308,6 +310,16 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
308310

309311
const history = await api.loadSessionHistory(session.id, session.project_id);
310312

313+
// Save session data for restoration
314+
if (history && history.length > 0) {
315+
SessionPersistenceService.saveSession(
316+
session.id,
317+
session.project_id,
318+
session.project_path,
319+
history.length
320+
);
321+
}
322+
311323
// Convert history to messages format
312324
const loadedMessages: ClaudeStreamMessage[] = history.map(entry => ({
313325
...entry,
@@ -544,6 +556,14 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
544556
if (!extractedSessionInfo) {
545557
const projectId = projectPath.replace(/[^a-zA-Z0-9]/g, '-');
546558
setExtractedSessionInfo({ sessionId: msg.session_id, projectId });
559+
560+
// Save session data for restoration
561+
SessionPersistenceService.saveSession(
562+
msg.session_id,
563+
projectId,
564+
projectPath,
565+
messages.length
566+
);
547567
}
548568

549569
// Switch to session-specific listeners

src/components/Settings.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { ProxySettings } from "./ProxySettings";
3434
import { AnalyticsConsent } from "./AnalyticsConsent";
3535
import { useTheme, useTrackEvent } from "@/hooks";
3636
import { analytics } from "@/lib/analytics";
37+
import { TabPersistenceService } from "@/services/tabPersistence";
3738

3839
interface SettingsProps {
3940
/**
@@ -99,11 +100,16 @@ export const Settings: React.FC<SettingsProps> = ({
99100
const [showAnalyticsConsent, setShowAnalyticsConsent] = useState(false);
100101
const trackEvent = useTrackEvent();
101102

103+
// Tab persistence state
104+
const [tabPersistenceEnabled, setTabPersistenceEnabled] = useState(true);
105+
102106
// Load settings on mount
103107
useEffect(() => {
104108
loadSettings();
105109
loadClaudeBinaryPath();
106110
loadAnalyticsSettings();
111+
// Load tab persistence setting
112+
setTabPersistenceEnabled(TabPersistenceService.isEnabled());
107113
}, []);
108114

109115
/**
@@ -711,6 +717,31 @@ export const Settings: React.FC<SettingsProps> = ({
711717
</div>
712718
</div>
713719
)}
720+
721+
{/* Tab Persistence Toggle */}
722+
<div className="flex items-center justify-between">
723+
<div className="space-y-1">
724+
<Label htmlFor="tab-persistence">Remember Open Tabs</Label>
725+
<p className="text-caption text-muted-foreground">
726+
Restore your tabs when you restart the app
727+
</p>
728+
</div>
729+
<Switch
730+
id="tab-persistence"
731+
checked={tabPersistenceEnabled}
732+
onCheckedChange={(checked) => {
733+
TabPersistenceService.setEnabled(checked);
734+
setTabPersistenceEnabled(checked);
735+
trackEvent.settingsChanged('tab_persistence_enabled', checked);
736+
setToast({
737+
message: checked
738+
? "Tab persistence enabled - your tabs will be restored on restart"
739+
: "Tab persistence disabled - tabs will not be saved",
740+
type: "success"
741+
});
742+
}}
743+
/>
744+
</div>
714745
</div>
715746
</div>
716747
</Card>

src/contexts/TabContext.tsx

Lines changed: 96 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import React, { createContext, useState, useContext, useCallback, useEffect } from 'react';
1+
import React, { createContext, useState, useContext, useCallback, useEffect, useRef } from 'react';
2+
import { TabPersistenceService } from '@/services/tabPersistence';
3+
import { SessionPersistenceService } from '@/services/sessionPersistence';
4+
import { api } from '@/lib/api';
25

36
export interface Tab {
47
id: string;
@@ -40,35 +43,102 @@ const MAX_TABS = 20;
4043
export const TabProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
4144
const [tabs, setTabs] = useState<Tab[]>([]);
4245
const [activeTabId, setActiveTabId] = useState<string | null>(null);
46+
const isInitialized = useRef(false);
47+
const saveTimeoutRef = useRef<NodeJS.Timeout>();
4348

44-
// Always start with a fresh Projects tab
49+
// Load tabs from storage on mount
4550
useEffect(() => {
46-
// Create default projects tab
47-
const defaultTab: Tab = {
48-
id: generateTabId(),
49-
type: 'projects',
50-
title: 'Projects',
51-
status: 'idle',
52-
hasUnsavedChanges: false,
53-
order: 0,
54-
createdAt: new Date(),
55-
updatedAt: new Date()
51+
const loadTabs = async () => {
52+
if (isInitialized.current) return;
53+
isInitialized.current = true;
54+
55+
// Migrate from old format if needed
56+
TabPersistenceService.migrateFromOldFormat();
57+
58+
// Try to load saved tabs
59+
const { tabs: savedTabs, activeTabId: savedActiveTabId } = TabPersistenceService.loadTabs();
60+
61+
if (savedTabs.length > 0) {
62+
// For chat tabs, restore session data
63+
const restoredTabs = await Promise.all(savedTabs.map(async (tab) => {
64+
if (tab.type === 'chat' && tab.sessionId) {
65+
// Check if session can be restored
66+
const sessionData = SessionPersistenceService.loadSession(tab.sessionId);
67+
if (sessionData) {
68+
// Create a Session object for the tab
69+
const session = SessionPersistenceService.createSessionFromRestoreData(sessionData);
70+
return {
71+
...tab,
72+
sessionData: session,
73+
initialProjectPath: sessionData.projectPath
74+
};
75+
}
76+
}
77+
return tab;
78+
}));
79+
80+
setTabs(restoredTabs);
81+
setActiveTabId(savedActiveTabId);
82+
} else {
83+
// Create default projects tab if no saved tabs
84+
const defaultTab: Tab = {
85+
id: generateTabId(),
86+
type: 'projects',
87+
title: 'Projects',
88+
status: 'idle',
89+
hasUnsavedChanges: false,
90+
order: 0,
91+
createdAt: new Date(),
92+
updatedAt: new Date()
93+
};
94+
setTabs([defaultTab]);
95+
setActiveTabId(defaultTab.id);
96+
}
5697
};
57-
setTabs([defaultTab]);
58-
setActiveTabId(defaultTab.id);
98+
99+
loadTabs();
59100
}, []);
60101

61-
// Tab persistence disabled - no longer saving to localStorage
62-
// useEffect(() => {
63-
// if (tabs.length > 0) {
64-
// const tabsToSave = tabs.map(tab => ({
65-
// ...tab,
66-
// createdAt: tab.createdAt.toISOString(),
67-
// updatedAt: tab.updatedAt.toISOString()
68-
// }));
69-
// localStorage.setItem(STORAGE_KEY, JSON.stringify(tabsToSave));
70-
// }
71-
// }, [tabs]);
102+
// Save tabs to localStorage with debounce
103+
useEffect(() => {
104+
// Don't save if not initialized
105+
if (!isInitialized.current) return;
106+
107+
// Clear existing timeout
108+
if (saveTimeoutRef.current) {
109+
clearTimeout(saveTimeoutRef.current);
110+
}
111+
112+
// Debounce saving to avoid excessive writes
113+
saveTimeoutRef.current = setTimeout(() => {
114+
TabPersistenceService.saveTabs(tabs, activeTabId);
115+
}, 500); // Wait 500ms after last change before saving
116+
117+
return () => {
118+
if (saveTimeoutRef.current) {
119+
clearTimeout(saveTimeoutRef.current);
120+
}
121+
};
122+
}, [tabs, activeTabId]);
123+
124+
// Save tabs immediately when window is about to close
125+
useEffect(() => {
126+
const handleBeforeUnload = () => {
127+
if (isInitialized.current && tabs.length > 0) {
128+
TabPersistenceService.saveTabs(tabs, activeTabId);
129+
}
130+
};
131+
132+
window.addEventListener('beforeunload', handleBeforeUnload);
133+
134+
return () => {
135+
window.removeEventListener('beforeunload', handleBeforeUnload);
136+
// Save one final time when component unmounts
137+
if (isInitialized.current && tabs.length > 0) {
138+
TabPersistenceService.saveTabs(tabs, activeTabId);
139+
}
140+
};
141+
}, [tabs, activeTabId]);
72142

73143
const generateTabId = () => {
74144
return `tab-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
@@ -152,7 +222,7 @@ export const TabProvider: React.FC<{ children: React.ReactNode }> = ({ children
152222
const closeAllTabs = useCallback(() => {
153223
setTabs([]);
154224
setActiveTabId(null);
155-
// localStorage.removeItem(STORAGE_KEY); // Persistence disabled
225+
TabPersistenceService.clearTabs();
156226
}, []);
157227

158228
const getTabsByType = useCallback((type: 'chat' | 'agent'): Tab[] => {

src/lib/api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ export interface AgentRunWithMetrics {
179179
session_id: string;
180180
status: string; // 'pending', 'running', 'completed', 'failed', 'cancelled'
181181
pid?: number;
182+
duration_ms?: number;
183+
total_tokens?: number;
182184
process_started_at?: string;
183185
created_at: string;
184186
completed_at?: string;

0 commit comments

Comments
 (0)