From d87307a4f8764248f818add539b5c30db7701d68 Mon Sep 17 00:00:00 2001 From: nyk <93952610+0xNyk@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:05:41 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20hybrid=20mode=20=E2=80=94=20show=20gate?= =?UTF-8?q?way=20+=20local=20sessions=20simultaneously=20(#392)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Always merge both gateway and local (Claude Code, Codex, Hermes) sessions in the sessions API instead of gating on `include_local`. The conversation list now renders all sources unconditionally, using each session's `source` field for display logic rather than the global `dashboardMode`. - Add `localSessionsAvailable` store flag, set from capabilities `claudeHome` - Remove `include_local` query param and early-return in sessions API - Remove source-based filter and `dashboardMode` branching in conversation list - Show both gateway and local active/recent groups when data exists --- src/app/[[...panel]]/page.tsx | 7 ++- src/app/api/sessions/route.ts | 14 ++---- src/components/chat/conversation-list.tsx | 56 ++++++++++------------- src/store/index.ts | 4 ++ 4 files changed, 35 insertions(+), 46 deletions(-) diff --git a/src/app/[[...panel]]/page.tsx b/src/app/[[...panel]]/page.tsx index 84da7f7..3c915cc 100644 --- a/src/app/[[...panel]]/page.tsx +++ b/src/app/[[...panel]]/page.tsx @@ -87,7 +87,7 @@ export default function Home() { const tb = useTranslations('boot') const tp = useTranslations('page') const tc = useTranslations('common') - const { activeTab, setActiveTab, setCurrentUser, setDashboardMode, setGatewayAvailable, setCapabilitiesChecked, setSubscription, setDefaultOrgName, setUpdateAvailable, setOpenclawUpdate, showOnboarding, setShowOnboarding, liveFeedOpen, toggleLiveFeed, showProjectManagerModal, setShowProjectManagerModal, fetchProjects, setChatPanelOpen, bootComplete, setBootComplete, setAgents, setSessions, setProjects, setInterfaceMode, setMemoryGraphAgents, setSkillsData } = useMissionControl() + const { activeTab, setActiveTab, setCurrentUser, setDashboardMode, setGatewayAvailable, setLocalSessionsAvailable, setCapabilitiesChecked, setSubscription, setDefaultOrgName, setUpdateAvailable, setOpenclawUpdate, showOnboarding, setShowOnboarding, liveFeedOpen, toggleLiveFeed, showProjectManagerModal, setShowProjectManagerModal, fetchProjects, setChatPanelOpen, bootComplete, setBootComplete, setAgents, setSessions, setProjects, setInterfaceMode, setMemoryGraphAgents, setSkillsData } = useMissionControl() // Sync URL → Zustand activeTab const pathname = usePathname() @@ -286,6 +286,9 @@ export default function Home() { setDashboardMode('full') setGatewayAvailable(true) } + if (data?.claudeHome) { + setLocalSessionsAvailable(true) + } setCapabilitiesChecked(true) markStep('capabilities') @@ -362,7 +365,7 @@ export default function Home() { ]).catch(() => { /* panels will lazy-load as fallback */ }) // eslint-disable-next-line react-hooks/exhaustive-deps -- boot once on mount, not on every pathname change - }, [connect, router, setCurrentUser, setDashboardMode, setGatewayAvailable, setCapabilitiesChecked, setSubscription, setUpdateAvailable, setShowOnboarding, setAgents, setSessions, setProjects, setInterfaceMode, setMemoryGraphAgents, setSkillsData]) + }, [connect, router, setCurrentUser, setDashboardMode, setGatewayAvailable, setLocalSessionsAvailable, setCapabilitiesChecked, setSubscription, setUpdateAvailable, setShowOnboarding, setAgents, setSessions, setProjects, setInterfaceMode, setMemoryGraphAgents, setSkillsData]) if (!isClient || !bootComplete) { return diff --git a/src/app/api/sessions/route.ts b/src/app/api/sessions/route.ts index d777620..5094dca 100644 --- a/src/app/api/sessions/route.ts +++ b/src/app/api/sessions/route.ts @@ -16,26 +16,18 @@ export async function GET(request: NextRequest) { if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) try { - const { searchParams } = new URL(request.url) - const includeLocal = searchParams.get('include_local') === '1' const gatewaySessions = getAllGatewaySessions() const mappedGatewaySessions = mapGatewaySessions(gatewaySessions) - // Preserve existing behavior by default: when gateway sessions are present, - // return only gateway-backed sessions unless include_local=1 is requested. - if (mappedGatewaySessions.length > 0 && !includeLocal) { - return NextResponse.json({ sessions: mappedGatewaySessions }) - } - - // Local Claude + Codex sessions from disk/SQLite + // Always include local sessions alongside gateway sessions await syncClaudeSessions() const claudeSessions = getLocalClaudeSessions() const codexSessions = getLocalCodexSessions() const hermesSessions = getLocalHermesSessions() const localMerged = mergeLocalSessions(claudeSessions, codexSessions, hermesSessions) - if (mappedGatewaySessions.length === 0) { - return NextResponse.json({ sessions: localMerged }) + if (mappedGatewaySessions.length === 0 && localMerged.length === 0) { + return NextResponse.json({ sessions: [] }) } const merged = dedupeAndSortSessions([...mappedGatewaySessions, ...localMerged]) diff --git a/src/components/chat/conversation-list.tsx b/src/components/chat/conversation-list.tsx index 5f1350f..bd9c6be 100644 --- a/src/components/chat/conversation-list.tsx +++ b/src/components/chat/conversation-list.tsx @@ -133,10 +133,8 @@ export function ConversationList({ onNewConversation: _onNewConversation }: Conv activeConversation, setActiveConversation, markConversationRead, - dashboardMode, } = useMissionControl() const [search, setSearch] = useState('') - const isGatewayMode = dashboardMode !== 'local' // Context menu state const [ctxMenu, setCtxMenu] = useState<{ convId: string; x: number; y: number } | null>(null) @@ -247,9 +245,7 @@ export function ConversationList({ onNewConversation: _onNewConversation }: Conv const loadConversations = useCallback(async () => { try { - const sessionsUrl = dashboardMode === 'local' - ? '/api/sessions?include_local=1' - : '/api/sessions' + const sessionsUrl = '/api/sessions' const requests: Promise[] = [ fetch(sessionsUrl), fetch('/api/chat/session-prefs'), @@ -260,12 +256,6 @@ export function ConversationList({ onNewConversation: _onNewConversation }: Conv const prefs = prefsRes.ok ? readSessionPrefs(await prefsRes.json().catch(() => null)) : {} const providerSessions = sessionsData - .filter((s) => { - if (dashboardMode === 'local') { - return s?.source === 'local' && (s?.kind === 'claude-code' || s?.kind === 'codex-cli' || s?.kind === 'hermes') - } - return s?.source === 'gateway' - }) .map((s, idx: number) => { const lastActivityMs = Number(s.lastActivity || s.startTime || 0) const updatedAt = lastActivityMs > 1_000_000_000_000 @@ -283,7 +273,7 @@ export function ConversationList({ onNewConversation: _onNewConversation }: Conv : 'Gateway' const prefKey = `${sessionKind}:${s.id}` const pref = prefs[prefKey] || {} - const defaultName = dashboardMode === 'local' + const defaultName = s.source === 'local' ? `${kindLabel} • ${s.key || s.id}` : `${s.agent || 'Gateway'} • ${s.key || s.id}` const sessionName = pref.name || defaultName @@ -329,7 +319,7 @@ export function ConversationList({ onNewConversation: _onNewConversation }: Conv } catch (err) { log.error('Failed to load conversations:', err) } - }, [dashboardMode, setConversations]) + }, [setConversations]) useSmartPoll(loadConversations, 30000, { pauseWhenSseConnected: true }) @@ -448,7 +438,7 @@ export function ConversationList({ onNewConversation: _onNewConversation }: Conv {/* Header */}
- {isGatewayMode ? 'Gateway Sessions' : 'Sessions'} + Sessions
@@ -473,24 +463,7 @@ export function ConversationList({ onNewConversation: _onNewConversation }: Conv
) : ( <> - {dashboardMode === 'local' && activeLocalRows.length > 0 && ( -
-
- - Active -
- {activeLocalRows.map(renderConversationItem)} -
- )} - {dashboardMode === 'local' && inactiveLocalRows.length > 0 && ( -
-
- Recent -
- {inactiveLocalRows.map(renderConversationItem)} -
- )} - {isGatewayMode && activeGatewayRows.length > 0 && ( + {activeGatewayRows.length > 0 && (
@@ -499,7 +472,16 @@ export function ConversationList({ onNewConversation: _onNewConversation }: Conv {activeGatewayRows.map(renderConversationItem)}
)} - {isGatewayMode && inactiveGatewayRows.length > 0 && ( + {activeLocalRows.length > 0 && ( +
+
+ + Active Local +
+ {activeLocalRows.map(renderConversationItem)} +
+ )} + {inactiveGatewayRows.length > 0 && (
Recent @@ -507,6 +489,14 @@ export function ConversationList({ onNewConversation: _onNewConversation }: Conv {inactiveGatewayRows.map(renderConversationItem)}
)} + {inactiveLocalRows.length > 0 && ( +
+
+ Recent Local +
+ {inactiveLocalRows.map(renderConversationItem)} +
+ )} )}
diff --git a/src/store/index.ts b/src/store/index.ts index c2ea23e..5d2a19a 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -371,6 +371,7 @@ interface MissionControlStore { // Dashboard Mode (local vs full gateway) dashboardMode: 'full' | 'local' gatewayAvailable: boolean + localSessionsAvailable: boolean bannerDismissed: boolean capabilitiesChecked: boolean bootComplete: boolean @@ -378,6 +379,7 @@ interface MissionControlStore { defaultOrgName: string setDashboardMode: (mode: 'full' | 'local') => void setGatewayAvailable: (available: boolean) => void + setLocalSessionsAvailable: (available: boolean) => void dismissBanner: () => void setCapabilitiesChecked: (checked: boolean) => void setBootComplete: () => void @@ -598,6 +600,7 @@ export const useMissionControl = create()( // Dashboard Mode dashboardMode: 'local' as const, gatewayAvailable: false, + localSessionsAvailable: false, bannerDismissed: false, capabilitiesChecked: false, bootComplete: false, @@ -605,6 +608,7 @@ export const useMissionControl = create()( defaultOrgName: 'Default', setDashboardMode: (mode) => set({ dashboardMode: mode }), setGatewayAvailable: (available) => set({ gatewayAvailable: available }), + setLocalSessionsAvailable: (available) => set({ localSessionsAvailable: available }), dismissBanner: () => set({ bannerDismissed: true }), setCapabilitiesChecked: (checked) => set({ capabilitiesChecked: checked }), setBootComplete: () => set({ bootComplete: true }),