diff --git a/server/session-scanner/service.ts b/server/session-scanner/service.ts index 38af37e2..c7f21bc8 100644 --- a/server/session-scanner/service.ts +++ b/server/session-scanner/service.ts @@ -167,6 +167,21 @@ export class SessionRepairService extends EventEmitter { } if (this.queue.has(sessionId)) { + // Check disk cache before waiting for queue processing. + // Sessions discovered at startup are enqueued before cache is consulted, + // so this avoids blocking on the queue when the cache has a valid result. + const cachedPath = this.resolveCachedPath(sessionId) + if (cachedPath) { + const cached = await this.cache.get(cachedPath, { allowStaleMs: ACTIVE_CACHE_GRACE_MS }) + if (cached) { + const normalized = cached.sessionId === sessionId + ? cached + : { ...cached, sessionId } + this.queue.seedResult(sessionId, normalized) + await this.ensureSessionArtifacts(normalized) + return normalized + } + } return this.queue.waitFor(sessionId, timeoutMs) } diff --git a/server/ws-handler.ts b/server/ws-handler.ts index d5c4037c..7531c096 100644 --- a/server/ws-handler.ts +++ b/server/ws-handler.ts @@ -309,7 +309,7 @@ export class WsHandler { private screenshotRequests = new Map() private sessionsRevision = 0 private terminalsRevision = 0 - private terminalRuntimeRevisions = new Map() + private readonly serverInstanceId: string private readonly bootId: string // The runtime validator is authoritative here; we keep the field typed broadly because @@ -1435,7 +1435,6 @@ export class WsHandler { } if (attachResult === 'duplicate') return state.attachedTerminalIds.add(m.terminalId) - this.broadcastTerminalRuntimeUpdatedForId(m.terminalId) return } @@ -1447,7 +1446,6 @@ export class WsHandler { return } this.send(ws, { type: 'terminal.detached', terminalId: m.terminalId }) - this.broadcastTerminalRuntimeUpdatedForId(m.terminalId) return } @@ -1474,7 +1472,6 @@ export class WsHandler { return } this.broadcastTerminalsChanged() - this.broadcastTerminalRuntimeUpdatedForId(m.terminalId) return } @@ -2037,59 +2034,6 @@ export class WsHandler { }) } - private resolveTerminalRuntimePayload(terminalId: string): { - terminalId: string - title: string - status: 'running' | 'detached' | 'exited' - cwd?: string - pid?: number - } | null { - const record = this.registry.get(terminalId) as - | { - terminalId: string - title: string - status: 'running' | 'exited' - cwd?: string - pty?: { pid?: number } - clients?: Set - } - | null - | undefined - - if (!record) return null - - return { - terminalId: record.terminalId, - title: record.title, - status: record.status === 'exited' - ? 'exited' - : ((record.clients?.size ?? 0) > 0 ? 'running' : 'detached'), - ...(record.cwd ? { cwd: record.cwd } : {}), - ...(typeof record.pty?.pid === 'number' ? { pid: record.pty.pid } : {}), - } - } - - broadcastTerminalRuntimeUpdated(msg: { - terminalId: string - title: string - status: 'running' | 'detached' | 'exited' - cwd?: string - pid?: number - }): void { - const revision = (this.terminalRuntimeRevisions.get(msg.terminalId) ?? 0) + 1 - this.terminalRuntimeRevisions.set(msg.terminalId, revision) - this.broadcastAuthenticated({ - type: 'terminal.runtime.updated', - revision, - ...msg, - }) - } - - private broadcastTerminalRuntimeUpdatedForId(terminalId: string): void { - const payload = this.resolveTerminalRuntimePayload(terminalId) - if (!payload) return - this.broadcastTerminalRuntimeUpdated(payload) - } broadcastTerminalMetaUpdated(msg: { upsert?: TerminalMeta[]; remove?: string[] }): void { const parsed = TerminalMetaUpdatedSchema.safeParse({ diff --git a/shared/ws-protocol.ts b/shared/ws-protocol.ts index fb0190b8..69078dd3 100644 --- a/shared/ws-protocol.ts +++ b/shared/ws-protocol.ts @@ -484,16 +484,6 @@ export type TerminalsChangedMessage = { revision: number } -export type TerminalRuntimeUpdatedMessage = { - type: 'terminal.runtime.updated' - terminalId: string - revision: number - status: 'running' | 'detached' | 'exited' - title: string - cwd?: string - pid?: number -} - export type TerminalMetaUpdatedMessage = z.infer export type CodexActivityListResponseMessage = z.infer @@ -697,7 +687,6 @@ export type ServerMessage = | TerminalTitleUpdatedMessage | TerminalSessionAssociatedMessage | TerminalsChangedMessage - | TerminalRuntimeUpdatedMessage | TerminalMetaUpdatedMessage | TerminalInventoryMessage | CodexActivityListResponseMessage diff --git a/src/components/BackgroundSessions.tsx b/src/components/BackgroundSessions.tsx index c0732be8..657c75bd 100644 --- a/src/components/BackgroundSessions.tsx +++ b/src/components/BackgroundSessions.tsx @@ -1,9 +1,11 @@ import { useCallback, useEffect, useMemo } from 'react' +import { nanoid } from 'nanoid' import { getWsClient } from '@/lib/ws-client' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { useAppDispatch, useAppSelector } from '@/store/hooks' import { addTab } from '@/store/tabsSlice' +import { initLayout } from '@/store/panesSlice' import { fetchTerminalDirectoryWindow } from '@/store/terminalDirectoryThunks' type BackgroundTerminal = { @@ -97,7 +99,10 @@ export default function BackgroundSessions() { size="sm" variant="outline" onClick={() => { - dispatch(addTab({ title: t.title, terminalId: t.terminalId, status: 'running', mode: (t.mode as any) || 'shell', resumeSessionId: t.resumeSessionId })) + const tabId = nanoid() + const mode = ((t.mode as any) || 'shell') as string + dispatch(addTab({ id: tabId, title: t.title, status: 'running', mode: mode as any, resumeSessionId: t.resumeSessionId })) + dispatch(initLayout({ tabId, content: { kind: 'terminal', mode: mode as any, terminalId: t.terminalId, status: 'running', resumeSessionId: t.resumeSessionId } })) }} > Attach diff --git a/src/components/OverviewView.tsx b/src/components/OverviewView.tsx index 4f06400b..00ab071b 100644 --- a/src/components/OverviewView.tsx +++ b/src/components/OverviewView.tsx @@ -1,8 +1,11 @@ -import { useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' +import { nanoid } from 'nanoid' import { api } from '@/lib/api' import { useAppDispatch, useAppSelector } from '@/store/hooks' import { addTab, setActiveTab, updateTab } from '@/store/tabsSlice' +import { initLayout } from '@/store/panesSlice' import { getWsClient } from '@/lib/ws-client' +import { collectTerminalIds } from '@/lib/pane-utils' import { cn } from '@/lib/utils' import { RefreshCw, Circle, Play, Pencil, Trash2, Sparkles, ExternalLink } from 'lucide-react' import { ContextIds } from '@/components/context-menu/context-menu-constants' @@ -44,9 +47,20 @@ function formatDuration(ms: number): string { export default function OverviewView({ onOpenTab }: { onOpenTab?: () => void }) { const dispatch = useAppDispatch() const tabs = useAppSelector((s) => s.tabs.tabs) + const paneLayouts = useAppSelector((s) => s.panes.layouts) const ws = useMemo(() => getWsClient(), []) + const findTabByTerminalId = useCallback((terminalId: string) => { + for (const tab of tabs) { + const layout = paneLayouts[tab.id] + if (layout && collectTerminalIds(layout).includes(terminalId)) { + return tab + } + } + return undefined + }, [tabs, paneLayouts]) + const [items, setItems] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) @@ -137,15 +151,17 @@ export default function OverviewView({ onOpenTab }: { onOpenTab?: () => void }) x.terminalId === t.terminalId)} + isOpen={!!findTabByTerminalId(t.terminalId)} onOpen={() => { - const existing = tabs.find((x) => x.terminalId === t.terminalId) + const existing = findTabByTerminalId(t.terminalId) if (existing) { dispatch(setActiveTab(existing.id)) onOpenTab?.() return } - dispatch(addTab({ title: t.title, terminalId: t.terminalId, status: 'running', mode: 'shell' })) + const tabId = nanoid() + dispatch(addTab({ id: tabId, title: t.title, status: 'running', mode: 'shell' })) + dispatch(initLayout({ tabId, content: { kind: 'terminal', mode: 'shell', terminalId: t.terminalId, status: 'running' } })) onOpenTab?.() }} onRename={async (title, description) => { @@ -153,7 +169,7 @@ export default function OverviewView({ onOpenTab }: { onOpenTab?: () => void }) titleOverride: title || undefined, descriptionOverride: description || undefined, }) - const existing = tabs.find((x) => x.terminalId === t.terminalId) + const existing = findTabByTerminalId(t.terminalId) if (existing && title) { dispatch(updateTab({ id: existing.id, updates: { title } })) } @@ -192,15 +208,17 @@ export default function OverviewView({ onOpenTab }: { onOpenTab?: () => void }) x.terminalId === t.terminalId)} + isOpen={!!findTabByTerminalId(t.terminalId)} onOpen={() => { - const existing = tabs.find((x) => x.terminalId === t.terminalId) + const existing = findTabByTerminalId(t.terminalId) if (existing) { dispatch(setActiveTab(existing.id)) onOpenTab?.() return } - dispatch(addTab({ title: t.title, terminalId: t.terminalId, status: 'exited', mode: 'shell' })) + const tabId = nanoid() + dispatch(addTab({ id: tabId, title: t.title, status: 'exited', mode: 'shell' })) + dispatch(initLayout({ tabId, content: { kind: 'terminal', mode: 'shell', terminalId: t.terminalId, status: 'exited' } })) onOpenTab?.() }} onRename={async (title, description) => { diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 577a0756..cd498b9c 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -19,6 +19,7 @@ import { getInstalledPerfAuditBridge } from '@/lib/perf-audit-bridge' import { fetchSessionWindow } from '@/store/sessionsThunks' import { mergeSessionMetadataByKey } from '@/lib/session-metadata' import { collectBusySessionKeys } from '@/lib/pane-activity' +import { selectPrimaryTerminalIdForTab } from '@/store/selectors/paneTerminalSelectors' import type { ChatSessionState } from '@/store/agentChatTypes' import type { PaneRuntimeActivityRecord } from '@/store/paneRuntimeActivitySlice' @@ -388,7 +389,6 @@ export default function Sidebar({ sessionType, sessionId: item.sessionId, cwd: item.cwd, - terminalId: runningTerminalId, agentChatProviderSettings: providerSettings, }), })) @@ -421,9 +421,8 @@ export default function Sidebar({ { id: 'settings' as const, label: 'Settings', icon: Settings, shortcut: ',' }, ] - const activeTab = tabs.find((t) => t.id === activeTabId) const activeSessionKey = activeSessionKeyFromPanes - const activeTerminalId = activeTab?.terminalId + const activeTerminalId = useAppSelector((s) => activeTabId ? selectPrimaryTerminalIdForTab(s, activeTabId) : undefined) const hasLoadedSidebarWindow = typeof sidebarWindow?.lastLoadedAt === 'number' const sidebarWindowHasItems = (sidebarWindow?.projects ?? []).some((project) => (project.sessions?.length ?? 0) > 0) const visibleQuery = appliedQuery || requestedQuery diff --git a/src/components/TabBar.tsx b/src/components/TabBar.tsx index 63e54458..a90ac573 100644 --- a/src/components/TabBar.tsx +++ b/src/components/TabBar.tsx @@ -185,7 +185,6 @@ export default function TabBar({ sidebarCollapsed, onToggleSidebar }: TabBarProp shell: tab.shell, createRequestId: tab.createRequestId, status: tab.status, - terminalId: tab.terminalId, resumeSessionId: tab.resumeSessionId, initialCwd: tab.initialCwd, }, @@ -202,7 +201,7 @@ export default function TabBar({ sidebarCollapsed, onToggleSidebar }: TabBarProp return Array.from(new Set(ids)) } } - return tab.terminalId ? [tab.terminalId] : [] + return [] }, [paneLayouts]) const getBusyPaneIds = useCallback((tab: Tab): string[] => getBusyPaneIdsForTab({ diff --git a/src/components/TabContent.tsx b/src/components/TabContent.tsx index fd941817..4ea91e1b 100644 --- a/src/components/TabContent.tsx +++ b/src/components/TabContent.tsx @@ -15,6 +15,7 @@ interface TabContentProps { export default function TabContent({ tabId, hidden }: TabContentProps) { const tab = useAppSelector((s) => s.tabs.tabs.find((t) => t.id === tabId)) + const hasLayout = useAppSelector((s) => !!s.panes.layouts[tabId]) const defaultNewPane = useAppSelector((s) => s.settings.settings.panes?.defaultNewPane || 'ask') const previousHiddenRef = useRef(hidden) @@ -27,8 +28,8 @@ export default function TabContent({ tabId, hidden }: TabContentProps) { if (!tab) return null - // For coding CLI session views with no terminal, use SessionView - if (tab.codingCliSessionId && !tab.terminalId) { + // For coding CLI session views with no terminal pane, use SessionView + if (tab.codingCliSessionId && !hasLayout) { return