Skip to main content

ADR-007: Implement Multi-Session Architecture

Date: 2025-10-06 Status: Accepted Deciders: Development Team Tags: architecture, ux, sessions

Context​

Users need the ability to:

  • Work on multiple projects simultaneously
  • Switch between different llm conversations
  • Keep separate editor states per session
  • Maintain independent terminal contexts
  • Isolate file workspaces

User Requirement: "we need tabs across the top of the main space below the header so we can have multiple sessions simultaneously"

Decision​

Implement a tab-based multi-session architecture where each session maintains:

  • Independent editor tabs and content
  • Separate llm conversation history
  • Isolated file explorer state
  • Dedicated terminal instance
  • Session-specific settings

Rationale​

Why Multi-Session Tabs​

  1. Context Switching: Quick switching between projects/tasks
  2. Parallel Work: Multiple llm conversations simultaneously
  3. Isolation: Each session independent, no cross-contamination
  4. Productivity: No need to close/reopen workspaces
  5. Familiar UX: Browser tabs pattern everyone knows
  6. Persistence: Sessions saved across browser restarts

User Experience Benefits​

  • Fast Switching: Cmd+1-9 shortcuts for sessions
  • Visual Indicators: See which sessions have unsaved changes
  • Drag & Drop: Reorder sessions
  • Context Preservation: Return to exact state
  • Session History: Restore closed sessions

Architecture​

Data Model​

Session Entity​

interface Session {
id: string; // Unique session ID
name: string; // User-defined name
icon?: string; // Optional icon
createdAt: Date;
updatedAt: Date;

// State references
editorState: {
tabs: editorTab[];
activeTabId: string | null;
scrollPosition: number;
};

llmState: {
messages: Message[];
primaryModel: string | null;
secondaryModel: string | null;
mode: WorkflowMode;
config: llmConfig;
};

fileState: {
workspaceRoot: string;
expandedFolders: string[];
selectedFile: string | null;
};

terminalState: {
cwd: string;
history: string[];
buffer: string;
};

// Metadata
isDirty: boolean; // Unsaved changes
isActive: boolean; // Currently active
order: number; // Tab order
}

Session Store​

interface SessionStore {
sessions: Session[];
activeSessionId: string | null;

// Actions
createSession: (name?: string) => void;
closeSession: (id: string) => void;
switchSession: (id: string) => void;
renameSession: (id: string, name: string) => void;
duplicateSession: (id: string) => void;
reorderSessions: (fromIndex: number, toIndex: number) => void;

// Persistence
saveSession: (id: string) => Promise<void>;
loadSession: (id: string) => Promise<Session>;
restoreLastSession: () => Promise<void>;
}

export const useSessionStore = create<SessionStore>()(
persist(
(set, get) => ({
sessions: [],
activeSessionId: null,

createSession: (name) => {
const newSession: Session = {
id: nanoid(),
name: name || `Session ${get().sessions.length + 1}`,
createdAt: new Date(),
updatedAt: new Date(),
editorState: { tabs: [], activeTabId: null, scrollPosition: 0 },
llmState: {
messages: [],
primaryModel: null,
secondaryModel: null,
mode: 'single',
config: defaultllmConfig
},
fileState: {
workspaceRoot: '/',
expandedFolders: [],
selectedFile: null
},
terminalState: {
cwd: '~',
history: [],
buffer: ''
},
isDirty: false,
isActive: true,
order: get().sessions.length
};

set(state => ({
sessions: [...state.sessions, newSession],
activeSessionId: newSession.id
}));
},

closeSession: (id) => {
const sessions = get().sessions.filter(s => s.id !== id);
const activeId = get().activeSessionId === id
? sessions[0]?.id || null
: get().activeSessionId;

set({ sessions, activeSessionId: activeId });
},

switchSession: (id) => {
set(state => ({
sessions: state.sessions.map(s => ({
...s,
isActive: s.id === id
})),
activeSessionId: id
}));
},

// ... other actions
}),
{
name: 'session-storage',
storage: createJSONStorage(() => fdbStorage), // Use FoundationDB
partialize: (state) => ({
sessions: state.sessions,
activeSessionId: state.activeSessionId
})
}
)
);

UI Implementation​

Session Tabs Component​

import { Tabs, TabList, Tab, IconButton, Badge } from '@chakra-ui/react';
import { FiPlus, FiX, FiEdit2 } from 'react-icons/fi';

export default function session-tabs() {
const { sessions, activeSessionId, switchSession, closeSession, createSession } =
useSessionStore();

const activeIndex = sessions.findIndex(s => s.id === activeSessionId);

return (
<Flex
h="40px"
bg="editor.sidebar"
borderBottom="1px"
borderColor="editor.border"
align="center"
px={2}
>
<Tabs
index={activeIndex}
onChange={(index) => switchSession(sessions[index].id)}
variant="unstyled"
flex={1}
>
<TabList>
{sessions.map((session) => (
<Tab
key={session.id}
position="relative"
fontSize="sm"
_selected={{
bg: 'editor.bg',
borderBottom: '2px solid',
borderColor: 'blue.500'
}}
>
<HStack spacing={2}>
{session.icon && <Text>{session.icon}</Text>}
<Text>{session.name}</Text>
{session.isDirty && (
<Badge colorScheme="orange" variant="solid" w="6px" h="6px" borderRadius="full" p={0} />
)}
</HStack>
<IconButton
icon={<FiX />}
size="xs"
variant="ghost"
ml={2}
onClick={(e) => {
e.stopPropagation();
closeSession(session.id);
}}
aria-label="Close session"
/>
</Tab>
))}
</TabList>
</Tabs>

<IconButton
icon={<FiPlus />}
size="sm"
variant="ghost"
onClick={() => createSession()}
aria-label="New session"
/>
</Flex>
);
}

Session Context Provider​

export function SessionProvider({ children }: { children: ReactNode }) {
const activeSessionId = useSessionStore(state => state.activeSessionId);
const activeSession = useSessionStore(state =>
state.sessions.find(s => s.id === activeSessionId)
);

// Auto-save session on state changes
useEffect(() => {
if (!activeSession) return;

const saveTimeout = setTimeout(() => {
useSessionStore.getState().saveSession(activeSession.id);
}, 500);

return () => clearTimeout(saveTimeout);
}, [activeSession]);

// Restore last session on mount
useEffect(() => {
useSessionStore.getState().restoreLastSession();
}, []);

if (!activeSession) {
return <EmptyState />;
}

return (
<SessionContext.Provider value={activeSession}>
{children}
</SessionContext.Provider>
);
}

Session-Aware Components​

// editor that respects current session
export function editorPanel() {
const session = useSession();
const { tabs, activeTabId } = session.editorState;

const activeTab = tabs.find(t => t.id === activeTabId);

return (
<Box h="100%">
<editorTabs tabs={tabs} activeTabId={activeTabId} />
<editor
value={activeTab?.content || ''}
language={activeTab?.language || 'plaintext'}
onChange={(content) => {
updateSessioneditorContent(session.id, activeTabId, content);
}}
/>
</Box>
);
}

// Chat panel that uses session's conversation
export function ChatPanel() {
const session = useSession();
const { messages, primaryModel, mode } = session.llmState;

return (
<Box h="100%">
<ModelSelector model={primaryModel} sessionId={session.id} />
<MessageList messages={messages} />
<ChatInput sessionId={session.id} />
</Box>
);
}

Keyboard Shortcuts​

export function useSessionShortcuts() {
useHotkeys('cmd+t', () => useSessionStore.getState().createSession());
useHotkeys('cmd+w', () => {
const activeId = useSessionStore.getState().activeSessionId;
if (activeId) useSessionStore.getState().closeSession(activeId);
});

// Switch to session 1-9
for (let i = 1; i <= 9; i++) {
useHotkeys(`cmd+${i}`, () => {
const session = useSessionStore.getState().sessions[i - 1];
if (session) useSessionStore.getState().switchSession(session.id);
});
}

// Cycle sessions
useHotkeys('cmd+shift+]', () => {
const { sessions, activeSessionId } = useSessionStore.getState();
const currentIndex = sessions.findIndex(s => s.id === activeSessionId);
const nextIndex = (currentIndex + 1) % sessions.length;
useSessionStore.getState().switchSession(sessions[nextIndex].id);
});
}

Persistence Strategy​

Save Strategy​

  1. Auto-save: Every 500ms after changes
  2. Explicit Save: Cmd+S saves current session
  3. On Switch: Save current before switching
  4. On Close: Save before closing browser

Load Strategy​

  1. On Mount: Restore last active session
  2. On Login: Load user's sessions from FoundationDB
  3. On Error: Fallback to OPFS cache
  4. On Conflict: Show merge UI

Session Management​

Session Lifecycle​

class SessionManager {
async createSession(name?: string): Promise<Session> {
const session = generateSession(name);
await fdbService.saveSession(session);
return session;
}

async closeSession(id: string): Promise<void> {
const session = await fdbService.loadSession(id);

if (session.isDirty) {
const shouldSave = await confirmSave();
if (shouldSave) {
await fdbService.saveSession(session);
}
}

await fdbService.deleteSession(id);
}

async duplicateSession(id: string): Promise<Session> {
const original = await fdbService.loadSession(id);
const duplicate = {
...original,
id: nanoid(),
name: `${original.name} (Copy)`,
createdAt: new Date()
};

await fdbService.saveSession(duplicate);
return duplicate;
}

async exportSession(id: string): Promise<Blob> {
const session = await fdbService.loadSession(id);
const json = JSON.stringify(session, null, 2);
return new Blob([json], { type: 'application/json' });
}

async importSession(file: File): Promise<Session> {
const text = await file.text();
const session = JSON.parse(text) as Session;
session.id = nanoid(); // New ID
await fdbService.saveSession(session);
return session;
}
}

References​