Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions server/session-scanner/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
58 changes: 1 addition & 57 deletions server/ws-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ export class WsHandler {
private screenshotRequests = new Map<string, PendingScreenshot>()
private sessionsRevision = 0
private terminalsRevision = 0
private terminalRuntimeRevisions = new Map<string, number>()

private readonly serverInstanceId: string
private readonly bootId: string
// The runtime validator is authoritative here; we keep the field typed broadly because
Expand Down Expand Up @@ -1435,7 +1435,6 @@ export class WsHandler {
}
if (attachResult === 'duplicate') return
state.attachedTerminalIds.add(m.terminalId)
this.broadcastTerminalRuntimeUpdatedForId(m.terminalId)
return
}

Expand All @@ -1447,7 +1446,6 @@ export class WsHandler {
return
}
this.send(ws, { type: 'terminal.detached', terminalId: m.terminalId })
this.broadcastTerminalRuntimeUpdatedForId(m.terminalId)
return
}

Expand All @@ -1474,7 +1472,6 @@ export class WsHandler {
return
}
this.broadcastTerminalsChanged()
this.broadcastTerminalRuntimeUpdatedForId(m.terminalId)
return
}

Expand Down Expand Up @@ -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<unknown>
}
| 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({
Expand Down
11 changes: 0 additions & 11 deletions shared/ws-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof TerminalMetaUpdatedSchema>

export type CodexActivityListResponseMessage = z.infer<typeof CodexActivityListResponseSchema>
Expand Down Expand Up @@ -697,7 +687,6 @@ export type ServerMessage =
| TerminalTitleUpdatedMessage
| TerminalSessionAssociatedMessage
| TerminalsChangedMessage
| TerminalRuntimeUpdatedMessage
| TerminalMetaUpdatedMessage
| TerminalInventoryMessage
| CodexActivityListResponseMessage
Expand Down
7 changes: 6 additions & 1 deletion src/components/BackgroundSessions.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -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
Expand Down
34 changes: 26 additions & 8 deletions src/components/OverviewView.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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<TerminalOverview[]>([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
Expand Down Expand Up @@ -137,23 +151,25 @@ export default function OverviewView({ onOpenTab }: { onOpenTab?: () => void })
<TerminalCard
key={t.terminalId}
terminal={t}
isOpen={tabs.some((x) => 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) => {
await api.patch(`/api/terminals/${encodeURIComponent(t.terminalId)}`, {
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 } }))
}
Expand Down Expand Up @@ -192,15 +208,17 @@ export default function OverviewView({ onOpenTab }: { onOpenTab?: () => void })
<TerminalCard
key={t.terminalId}
terminal={t}
isOpen={tabs.some((x) => 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) => {
Expand Down
5 changes: 2 additions & 3 deletions src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -388,7 +389,6 @@ export default function Sidebar({
sessionType,
sessionId: item.sessionId,
cwd: item.cwd,
terminalId: runningTerminalId,
agentChatProviderSettings: providerSettings,
}),
}))
Expand Down Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions src/components/TabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand All @@ -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({
Expand Down
18 changes: 4 additions & 14 deletions src/components/TabContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -27,25 +28,16 @@ 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 <SessionView sessionId={tab.codingCliSessionId} hidden={hidden} />
}

// Build default content based on setting
let defaultContent: PaneContentInput
const resumeSessionType = getTabResumeSessionType(tab)

if (tab.terminalId) {
defaultContent = {
kind: 'terminal',
mode: tab.mode,
shell: tab.shell,
resumeSessionId: tab.resumeSessionId,
initialCwd: tab.initialCwd,
terminalId: tab.terminalId,
}
} else if (tab.resumeSessionId && resumeSessionType) {
if (tab.resumeSessionId && resumeSessionType) {
defaultContent = buildResumeContent({
sessionType: resumeSessionType,
sessionId: tab.resumeSessionId,
Expand All @@ -58,7 +50,6 @@ export default function TabContent({ tabId, hidden }: TabContentProps) {
shell: tab.shell,
resumeSessionId: tab.resumeSessionId,
initialCwd: tab.initialCwd,
terminalId: tab.terminalId,
}
} else if (defaultNewPane === 'ask') {
defaultContent = { kind: 'picker' }
Expand All @@ -81,7 +72,6 @@ export default function TabContent({ tabId, hidden }: TabContentProps) {
shell: tab.shell,
resumeSessionId: tab.resumeSessionId,
initialCwd: tab.initialCwd,
terminalId: tab.terminalId,
}
}

Expand Down
10 changes: 4 additions & 6 deletions src/components/TerminalView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1783,10 +1783,10 @@ export default function TerminalView({ tabId, paneId, paneContent, hidden }: Ter
})
terminalIdRef.current = newId
updateContent({ terminalId: newId, status: 'running' })
// Also update tab for title purposes
// Also update tab status
const currentTab = tabRef.current
if (currentTab) {
dispatch(updateTab({ id: currentTab.id, updates: { terminalId: newId, status: 'running' } }))
dispatch(updateTab({ id: currentTab.id, updates: { status: 'running' } }))
}
if (msg.effectiveResumeSessionId && msg.effectiveResumeSessionId !== contentRef.current?.resumeSessionId) {
updateContent({ resumeSessionId: msg.effectiveResumeSessionId })
Expand Down Expand Up @@ -1828,7 +1828,7 @@ export default function TerminalView({ tabId, paneId, paneContent, hidden }: Ter
if (exitTab) {
const code = typeof msg.exitCode === 'number' ? msg.exitCode : undefined
// Only modify title if user hasn't manually set it
const updates: { terminalId: undefined; status: 'exited'; title?: string } = { terminalId: undefined, status: 'exited' }
const updates: { status: 'exited'; title?: string } = { status: 'exited' }
if (!exitTab.titleSetByUser) {
updates.title = exitTab.title + (code !== undefined ? ` (exit ${code})` : '')
}
Expand Down Expand Up @@ -1940,11 +1940,9 @@ export default function TerminalView({ tabId, paneId, paneContent, hidden }: Ter
}
applySeqState(createAttachSeqState())
updateContent({ terminalId: undefined, createRequestId: newRequestId, status: 'creating' })
// Also clear the tab's terminalId to keep it in sync.
// This prevents openSessionTab from using the stale terminalId for dedup.
const currentTab = tabRef.current
if (currentTab) {
dispatch(updateTab({ id: currentTab.id, updates: { terminalId: undefined, status: 'creating' } }))
dispatch(updateTab({ id: currentTab.id, updates: { status: 'creating' } }))
}
} else if (current?.status === 'exited') {
term.writeln('\r\n[Terminal exited - use the + button or split to start a new session]\r\n')
Expand Down
Loading
Loading