+
+
+
+
+
+ Sleep — close all panels in this workspace to free memory.
+
+
+
+
+
+
+
+ {isMainWorktree ? 'The main workspace cannot be deleted.' : 'Delete workspace.'}
+
+
+
)
}
-// ─── Section: worktree tree ─────────────────────────────────────────
-
function WorktreeSection({
worktrees,
sortOption,
@@ -351,30 +519,16 @@ function WorktreeSection({
onSleep: (worktreeId: string) => void
onDelete: (worktreeId: string) => void
}): React.JSX.Element {
- // Why: these slices mutate frequently (runtimePaneTitlesByTabId updates on
- // every terminal OSC escape). Subscribing inside WorktreeSection — which
- // only mounts when the popover is open — prevents those updates from
- // re-rendering the always-mounted status-bar segment.
const tabsByWorktree = useAppStore((s) => s.tabsByWorktree)
const runtimePaneTitlesByTabId = useAppStore((s) => s.runtimePaneTitlesByTabId)
-
- // Why: WorktreeMemory is a lightweight snapshot; we look the real Worktree
- // record up from the store so rows can (a) disable Delete for the main
- // worktree, and (b) render the user-editable displayName instead of the
- // dirname. Use the shared cached selector so we don't duplicate the
- // WeakMap-cached Map the rest of the app already shares.
const worktreeById = useWorktreeMap()
- // Shared label resolver: prefer displayName, fall back to the dirname
- // carried on the memory snapshot. Used for both rendering and alpha-sort.
const labelFor = useCallback(
(wt: WorktreeMemory): string =>
worktreeById.get(wt.worktreeId)?.displayName?.trim() || wt.worktreeName,
[worktreeById]
)
- // Memoize grouping: popover polls every 2s, so without this we'd rebuild
- // the Map + arrays on every render even when nothing changed.
const repoGroups = useMemo(
() =>
sortRepoGroupsBy(bucketByRepo(worktrees), sortOption).map((group) => ({
@@ -384,9 +538,6 @@ function WorktreeSection({
[worktrees, sortOption, labelFor]
)
- // Why: when only one repo is active, the repo header row adds a useless
- // level of nesting — the worktrees are the interesting thing. Flatten
- // straight to worktree rows in that case.
const singleRepo = repoGroups.length === 1
const renderWorktree = (wt: WorktreeMemory): React.JSX.Element => {
@@ -448,217 +599,281 @@ function WorktreeSection({
)
}
-function WorktreeRow({
- worktree,
- storeRecord,
- isCollapsed,
- onToggle,
- onNavigate,
- onSleep,
- onDelete,
- tabsByWorktree,
- runtimePaneTitlesByTabId
+// ─── Sessions tab ───────────────────────────────────────────────────
+
+function SessionsTabPanel({
+ sessions,
+ sessionsError,
+ onCloseSegment,
+ onSessionsChanged
}: {
- worktree: WorktreeMemory
- storeRecord: Worktree | null
- isCollapsed: boolean
- onToggle: () => void
- onNavigate: () => void
- onSleep: () => void
- onDelete: () => void
- tabsByWorktree: Record
- runtimePaneTitlesByTabId: Record>
+ sessions: DaemonSession[]
+ sessionsError: boolean
+ onCloseSegment: () => void
+ // Why: kill actions in this panel mutate daemon state but the parent owns
+ // the polling timer + sessions list. Without an immediate refresh, killed
+ // rows linger up to the 10s poll interval. Mirror the old SessionsSegment
+ // behavior of refreshing right after the IPC settles.
+ onSessionsChanged: () => void
}): React.JSX.Element {
- const hasSessions = worktree.sessions.length > 0
- // Why: actions are only meaningful for real worktrees — orphan/unknown
- // rows are synthetic buckets with no row to act on.
- const showActions = worktree.worktreeId !== ORPHAN_WORKTREE_ID && storeRecord !== null
- const isMainWorktree = storeRecord?.isMainWorktree ?? false
- // Why: Worktree.displayName is the user-editable workspace name (set via
- // Rename). Fall back to the dirname-shaped worktreeName from the memory
- // snapshot for orphan/unresolved rows that have no store record.
- const rowLabel = storeRecord?.displayName?.trim() || worktree.worktreeName
+ const tabsByWorktree = useAppStore((s) => s.tabsByWorktree)
+ const ptyIdsByTabId = useAppStore((s) => s.ptyIdsByTabId)
+ const runtimePaneTitlesByTabId = useAppStore((s) => s.runtimePaneTitlesByTabId)
+ const workspaceSessionReady = useAppStore((s) => s.workspaceSessionReady)
+ const setActiveTab = useAppStore((s) => s.setActiveTab)
+ const setActiveView = useAppStore((s) => s.setActiveView)
+
+ const boundPtyIds = useMemo(
+ () => new Set(Object.values(ptyIdsByTabId).flat().filter(Boolean)),
+ [ptyIdsByTabId]
+ )
+
+ const ptyIdToTabId = useMemo(() => {
+ const map = new Map()
+ for (const [tabId, ptyIds] of Object.entries(ptyIdsByTabId)) {
+ for (const ptyId of ptyIds) {
+ map.set(ptyId, tabId)
+ }
+ }
+ return map
+ }, [ptyIdsByTabId])
+
+ const tabIdToWorktreeId = useMemo(() => {
+ const map = new Map()
+ for (const [worktreeId, tabs] of Object.entries(tabsByWorktree)) {
+ for (const tab of tabs) {
+ map.set(tab.id, worktreeId)
+ }
+ }
+ return map
+ }, [tabsByWorktree])
+
+ const ptyIdToTabTitle = useMemo(() => {
+ const tabById = new Map()
+ for (const tabs of Object.values(tabsByWorktree)) {
+ for (const tab of tabs) {
+ tabById.set(tab.id, tab)
+ }
+ }
+ const map = new Map()
+ for (const [tabId, ptyIds] of Object.entries(ptyIdsByTabId)) {
+ const tab = tabById.get(tabId)
+ if (!tab) {
+ continue
+ }
+ const runtimeTitles = runtimePaneTitlesByTabId[tabId]
+ const liveTitle = runtimeTitles
+ ? Object.values(runtimeTitles).find((t) => t?.trim())
+ : undefined
+ const title =
+ tab.customTitle?.trim() || liveTitle?.trim() || tab.title || tab.defaultTitle?.trim()
+ if (title) {
+ for (const ptyId of ptyIds) {
+ map.set(ptyId, title)
+ }
+ }
+ }
+ return map
+ }, [ptyIdsByTabId, tabsByWorktree, runtimePaneTitlesByTabId])
+
+ const orphanCount = workspaceSessionReady
+ ? sessions.filter((s) => !boundPtyIds.has(s.id)).length
+ : 0
+
+ const handleKill = useCallback(
+ async (id: string) => {
+ try {
+ await window.api.pty.kill(id)
+ } catch {
+ /* already dead */
+ }
+ onSessionsChanged()
+ },
+ [onSessionsChanged]
+ )
+
+ const handleKillOrphans = useCallback(async () => {
+ if (!workspaceSessionReady) {
+ return
+ }
+ const orphans = sessions.filter((s) => !boundPtyIds.has(s.id))
+ await Promise.allSettled(orphans.map((s) => window.api.pty.kill(s.id)))
+ onSessionsChanged()
+ }, [sessions, boundPtyIds, workspaceSessionReady, onSessionsChanged])
+
+ const handleNavigate = useCallback(
+ (tabId: string) => {
+ const worktreeId = tabIdToWorktreeId.get(tabId)
+ if (worktreeId) {
+ activateAndRevealWorktree(worktreeId)
+ }
+ setActiveView('terminal')
+ setActiveTab(tabId)
+ onCloseSegment()
+ },
+ [tabIdToWorktreeId, setActiveView, setActiveTab, onCloseSegment]
+ )
+
+ if (sessionsError && sessions.length === 0) {
+ return (
+
+ Terminal sessions unavailable.
+
+ )
+ }
return (
-
-
- {hasSessions ? (
-
- ) : (
- // Why this width: matches the chevron button's pl-2 + w-3 + pr-0.5
- // footprint so rows without sessions don't shift horizontally
- // relative to rows with sessions.
-
- )}
-
-
- {/* Why the relative wrapper + absolute actions: the sparkline
- reserves the space so the row width never changes on hover.
- The actions fade in on top of the sparkline (which fades out
- in the same transition), preventing the layout "jump" that
- happened when the sparkline was toggled via display:none. */}
-
-
-
-
- {showActions && (
- // Why pointer-events pairing: opacity alone leaves the buttons
- // clickable when invisible (touch devices have no hover state),
- // so the Delete button can fire on an accidental tap.
-
+ {!isBound && (
-
-
- Sleep — close all panels in this workspace to free memory.
-
-
-
-
-
-
-
- {isMainWorktree ? 'The main workspace cannot be deleted.' : 'Delete workspace.'}
-
-
-
- )}
-
-
+ )}
+
+ )
+ })}
-
-
- {!isCollapsed &&
- worktree.sessions.map((session) => (
- // Why: sessionId can be null/fall back to ptyId; combine with pid
- // so two sessions from the same process family don't collide in
- // React reconciliation.
-
)
-
- // Why: wrap real rows in the shared context menu so right-click exposes
}
-// ─── Segment (top-level) ────────────────────────────────────────────
+// ─── Top-level segment ──────────────────────────────────────────────
-export function MemoryStatusSegment({
+export function ResourceUsageStatusSegment({
iconOnly
}: {
- // Why: `compact` is accepted for uniformity with the other *StatusSegment
- // components but is not used — the icon/badge layout already fits inside
- // a compact status bar.
compact?: boolean
iconOnly: boolean
}): React.JSX.Element {
const snapshot = useAppStore((s) => s.memorySnapshot)
+ const memorySnapshotError = useAppStore((s) => s.memorySnapshotError)
const fetchSnapshot = useAppStore((s) => s.fetchMemorySnapshot)
- // Why: worktree metadata (map, skipDeleteWorktreeConfirm,
- // clearWorktreeDeleteState, openModal) is only needed at click time inside
- // `deleteWorktree`. This segment is always mounted in the status bar, so
- // subscribing to those slices at the top level would cause it (and every
- // descendant) to re-render on unrelated worktree metadata churn
- // (pin/rename/unread/session). The shared `runWorktreeDelete` helper pulls
- // them imperatively via `useAppStore.getState()` instead.
+ const workspaceSessionReady = useAppStore((s) => s.workspaceSessionReady)
+ const ptyIdsByTabId = useAppStore((s) => s.ptyIdsByTabId)
const [open, setOpen] = useState(false)
+ const [activeTab, setActiveTab] = useState('resources')
const [sortOption, setSortOption] = useState('memory')
const [collapsedRepos, setCollapsedRepos] = useState>(new Set())
const [collapsedWorktrees, setCollapsedWorktrees] = useState>(new Set())
- // Why: the Orca app breakdown (Main/Renderer/Other) is a diagnostic detail
- // most users don't need to see every time — collapse it by default and
- // surface the per-worktree usage, which is what people usually open this
- // popover to investigate.
const [appCollapsed, setAppCollapsed] = useState(true)
+ const [sessions, setSessions] = useState([])
+ const [sessionsError, setSessionsError] = useState(false)
- // Why: only poll while the popover is open. When closed, the badge shows
- // whatever value was last fetched — good enough for a passive indicator
- // and keeps us from waking the main process every few seconds.
+ const daemonActions = useDaemonActions({
+ onRestartSettled: () => {
+ // Why: after a restart, clear the stale error state so the error banner
+ // disappears until the next poll confirms the new state.
+ setSessionsError(false)
+ void fetchSnapshot()
+ void refreshSessions()
+ },
+ onKillAllSettled: () => {
+ void refreshSessions()
+ }
+ })
+
+ const refreshSessions = useCallback(async () => {
+ try {
+ const result = await window.api.pty.listSessions()
+ setSessions(result)
+ setSessionsError(false)
+ } catch {
+ setSessionsError(true)
+ }
+ }, [])
+
+ // Poll memory + sessions when popover is open. Sessions also poll in the
+ // background at a slower rate so the badge count stays reasonably fresh
+ // without keeping the Memory IPC hot.
useEffect(() => {
if (!open) {
return
}
void fetchSnapshot()
- const timer = window.setInterval(() => {
+ void refreshSessions()
+ const memTimer = window.setInterval(() => {
void fetchSnapshot()
}, POLL_MS)
- return () => window.clearInterval(timer)
- }, [open, fetchSnapshot])
+ const sessTimer = window.setInterval(() => {
+ void refreshSessions()
+ }, SESSIONS_POLL_MS)
+ return () => {
+ window.clearInterval(memTimer)
+ window.clearInterval(sessTimer)
+ }
+ }, [open, fetchSnapshot, refreshSessions])
- // Derived values are grouped into one memo so open/sort/collapse state
- // changes don't recompute them.
- const { totalMemory, totalCpu, hostShare, badgeLabel } = useMemo(() => {
+ useEffect(() => {
+ const interval = setInterval(() => void refreshSessions(), SESSIONS_POLL_MS)
+ void refreshSessions()
+ return () => clearInterval(interval)
+ }, [refreshSessions])
+
+ const boundPtyIds = useMemo(
+ () => new Set(Object.values(ptyIdsByTabId).flat().filter(Boolean)),
+ [ptyIdsByTabId]
+ )
+ const orphanCount = workspaceSessionReady
+ ? sessions.filter((s) => !boundPtyIds.has(s.id)).length
+ : 0
+
+ const { totalMemory, totalCpu, hostShare, memBadgeLabel } = useMemo(() => {
const memory = snapshot?.totalMemory ?? 0
const cpu = snapshot?.totalCpu ?? 0
const hostTotal = snapshot?.host.totalMemory ?? 0
@@ -666,14 +881,12 @@ export function MemoryStatusSegment({
totalMemory: memory,
totalCpu: cpu,
hostShare: hostTotal > 0 ? (memory / hostTotal) * 100 : 0,
- badgeLabel: snapshot ? formatMemory(memory) : '—'
+ memBadgeLabel: snapshot ? formatMemory(memory) : '—'
}
}, [snapshot])
- // Why empty deps: these callbacks only call the state setter returned by
- // useState, which React guarantees is stable across renders — so we don't
- // need to list it. Wrapping in useCallback keeps the reference stable across
- // the 2s polling re-renders so descendants can be memoized downstream.
+ const daemonUnreachable = sessionsError && memorySnapshotError !== null
+
const toggleRepo = useCallback((repoId: string): void => {
setCollapsedRepos((prev) => {
const next = new Set(prev)
@@ -698,17 +911,11 @@ export function MemoryStatusSegment({
})
}, [])
- // Deps intentionally empty: only uses the stable setOpen setter and
- // module-level imports (ORPHAN_WORKTREE_ID, activateAndRevealWorktree).
const navigateToWorktree = useCallback((worktreeId: string): void => {
- // Orphan bucket has a synthetic id with no real worktree to reveal.
if (worktreeId === ORPHAN_WORKTREE_ID) {
setOpen(false)
return
}
- // Why: returns false when the worktree has been deleted between
- // snapshot capture and this click. Leave the popover open in that
- // case so a silent no-op doesn't look like a broken button.
const result = activateAndRevealWorktree(worktreeId)
if (result === false) {
return
@@ -716,20 +923,19 @@ export function MemoryStatusSegment({
setOpen(false)
}, [])
- // Why this thin wrapper: the popover needs to close before the modal/toast
- // appears (Radix's outside-pointerdown would otherwise dismiss the dialog).
- // The actual decision tree lives in `runWorktreeDelete` so both the popover
- // and the sidebar context menu stay in sync.
const deleteWorktree = useCallback((worktreeId: string): void => {
setOpen(false)
runWorktreeDelete(worktreeId)
}, [])
- // Stable callback so onSleep prop identity doesn't churn across polls.
const handleSleep = useCallback((id: string): void => {
void runSleepWorktree(id)
}, [])
+ const closeSegment = useCallback(() => {
+ setOpen(false)
+ }, [])
+
return (
@@ -738,19 +944,38 @@ export function MemoryStatusSegment({
- Memory — {badgeLabel}
+ Resource usage — {memBadgeLabel} · {sessions.length} session
+ {sessions.length === 1 ? '' : 's'}
@@ -759,22 +984,34 @@ export function MemoryStatusSegment({
align="end"
sideOffset={8}
className="w-[26rem] p-0"
- // Why: the first focusable element inside is the CPU/memory/% span
- // (tabIndex=0 so the tooltip is keyboard-reachable). Without this,
- // Radix auto-focuses that span on open, which triggers its tooltip
- // and leaves it stuck until the user mouses over something else.
onOpenAutoFocus={(event) => event.preventDefault()}
>
- {/* Why: the popover trigger already announces "Memory & CPU", so a
- heading row is redundant. We lead with the totals — which is what
- most people open this for — on a single compact line. */}
+ {daemonUnreachable && (
+
+
+
+
Daemon is not responding
+
+ Resource snapshots and terminal sessions are unavailable.
+
+
+
+
+ )}
+
{snapshot && (
- {/* Why tabIndex on span triggers: Radix Tooltip needs a
- focusable child for keyboard reveal; plain isn't
- focusable by default. */}
)}
- {/* Why click-to-sort on the column headers: the headers already
- label the columns, so doubling them up with a separate sort
- control was pure redundancy. The active column is bolded so
- users can see at a glance which one drives the order. */}
- {snapshot && (
-
- )}
-
- {/* Why Orca App at the bottom: it's a constant baseline everyone has,
- so it's less informative than the per-worktree breakdown. Keep
- it available but out of the way. */}
- {snapshot && (
- setAppCollapsed((v) => !v)}
- />
- )}
-
- {!snapshot && (
-
Loading…
- )}
+
+
+
+
)
}
diff --git a/src/renderer/src/components/status-bar/SessionsStatusSegment.tsx b/src/renderer/src/components/status-bar/SessionsStatusSegment.tsx
deleted file mode 100644
index 72739372326..00000000000
--- a/src/renderer/src/components/status-bar/SessionsStatusSegment.tsx
+++ /dev/null
@@ -1,310 +0,0 @@
-import React, { useCallback, useEffect, useMemo, useState } from 'react'
-import { Terminal, Trash2 } from 'lucide-react'
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuSeparator,
- DropdownMenuTrigger
-} from '@/components/ui/dropdown-menu'
-import { useAppStore } from '../../store'
-import { activateAndRevealWorktree } from '@/lib/worktree-activation'
-
-type DaemonSession = { id: string; cwd: string; title: string }
-
-function shortCwd(cwd: string): string {
- if (!cwd) {
- return 'unknown'
- }
- const separator = cwd.includes('\\') ? '\\' : '/'
- const parts = cwd.split(/[\\/]+/).filter(Boolean)
- return parts.length > 2 ? parts.slice(-2).join(separator) : cwd
-}
-
-function sessionLabel(session: DaemonSession, tabTitle?: string): string {
- if (session.cwd) {
- return shortCwd(session.cwd)
- }
- // Why: daemon session IDs use the format `${worktreeId}@@${shortUuid}`.
- // When the shell hasn't emitted OSC 7 cwd updates (e.g. Claude Code agents),
- // fall back to showing the worktreeId portion so the user can identify which
- // worktree the session belongs to.
- const sep = session.id.lastIndexOf('@@')
- if (sep !== -1) {
- const worktreeId = session.id.slice(0, sep)
- return shortCwd(worktreeId)
- }
- // Why: for sessions with legacy numeric IDs (e.g. from LocalPtyProvider) or
- // sessions whose shells haven't emitted OSC 7, use the tab title from the
- // renderer store — it carries the live terminal title shown in the tab bar.
- // Fall back to the daemon-reported title (typically "shell") so orphan
- // sessions without a bound tab still display something meaningful.
- if (tabTitle) {
- return tabTitle
- }
- if (session.title) {
- return session.title
- }
- return 'unknown'
-}
-
-function SessionRow({
- session,
- isBound,
- tabId,
- tabTitle,
- onKill,
- onNavigate
-}: {
- session: DaemonSession
- isBound: boolean
- tabId: string | null
- tabTitle: string | undefined
- onKill: (id: string) => void
- onNavigate: (tabId: string) => void
-}): React.JSX.Element {
- return (
-
onNavigate(tabId) : undefined}
- >
-
-
-
- {sessionLabel(session, tabTitle)}
-
-
- {!isBound && (
-
- )}
-
- )
-}
-
-export function SessionsStatusSegment({
- compact: _compact,
- iconOnly
-}: {
- compact: boolean
- iconOnly: boolean
-}): React.JSX.Element {
- const [sessions, setSessions] = useState([])
- const [open, setOpen] = useState(false)
- const tabsByWorktree = useAppStore((s) => s.tabsByWorktree)
- const ptyIdsByTabId = useAppStore((s) => s.ptyIdsByTabId)
- const runtimePaneTitlesByTabId = useAppStore((s) => s.runtimePaneTitlesByTabId)
- const workspaceSessionReady = useAppStore((s) => s.workspaceSessionReady)
- const setActiveTab = useAppStore((s) => s.setActiveTab)
- const setActiveView = useAppStore((s) => s.setActiveView)
-
- const boundPtyIds = useMemo(
- () => new Set(Object.values(ptyIdsByTabId).flat().filter(Boolean)),
- [ptyIdsByTabId]
- )
-
- // Why: ptyIdsByTabId tracks all ptyIds a tab has ever been associated with
- // (including split panes). Build a reverse map so we can navigate from a
- // daemon session ID back to the tab that owns it.
- const ptyIdToTabId = useMemo(() => {
- const map = new Map()
- for (const [tabId, ptyIds] of Object.entries(ptyIdsByTabId)) {
- for (const ptyId of ptyIds) {
- map.set(ptyId, tabId)
- }
- }
- return map
- }, [ptyIdsByTabId])
-
- const tabIdToWorktreeId = useMemo(() => {
- const map = new Map()
- for (const [worktreeId, tabs] of Object.entries(tabsByWorktree)) {
- for (const tab of tabs) {
- map.set(tab.id, worktreeId)
- }
- }
- return map
- }, [tabsByWorktree])
-
- // Why: the daemon IPC response has sparse metadata (cwd may be null, title is
- // hardcoded to "shell"), but the renderer store already knows each tab's live
- // title. Build a ptyId→title map so the popover can show the same name as the
- // tab bar for bound sessions.
- const ptyIdToTabTitle = useMemo(() => {
- // Why: build a tabId→tab lookup once up-front instead of doing
- // O(totalTabs) .flat().find() per ptyIdsByTabId entry.
- const tabById = new Map()
- for (const tabs of Object.values(tabsByWorktree)) {
- for (const tab of tabs) {
- tabById.set(tab.id, tab)
- }
- }
- const map = new Map()
- for (const [tabId, ptyIds] of Object.entries(ptyIdsByTabId)) {
- const tab = tabById.get(tabId)
- if (!tab) {
- continue
- }
- // Why: runtimePaneTitlesByTabId holds the most current OSC-reported title
- // for each mounted pane — it updates on every title change regardless of
- // which split pane is focused. Prefer the first non-empty runtime pane
- // title, then the persisted tab.title (which only updates for the focused
- // pane), then the stable defaultTitle.
- const runtimeTitles = runtimePaneTitlesByTabId[tabId]
- const liveTitle = runtimeTitles
- ? Object.values(runtimeTitles).find((t) => t?.trim())
- : undefined
- const title =
- tab.customTitle?.trim() || liveTitle?.trim() || tab.title || tab.defaultTitle?.trim()
- if (title) {
- for (const ptyId of ptyIds) {
- map.set(ptyId, title)
- }
- }
- }
- return map
- }, [ptyIdsByTabId, tabsByWorktree, runtimePaneTitlesByTabId])
-
- const refresh = useCallback(async () => {
- try {
- const result = await window.api.pty.listSessions()
- setSessions(result)
- } catch {
- setSessions([])
- }
- }, [])
-
- useEffect(() => {
- if (open) {
- void refresh()
- }
- }, [open, refresh])
-
- useEffect(() => {
- const interval = setInterval(() => void refresh(), 10_000)
- void refresh()
- return () => clearInterval(interval)
- }, [refresh])
-
- const orphanCount = workspaceSessionReady
- ? sessions.filter((s) => !boundPtyIds.has(s.id)).length
- : 0
-
- const handleKill = useCallback(
- async (id: string) => {
- try {
- await window.api.pty.kill(id)
- } catch {
- /* already dead */
- }
- await refresh()
- },
- [refresh]
- )
-
- const handleKillOrphans = useCallback(async () => {
- if (!workspaceSessionReady) {
- return
- }
- const orphans = sessions.filter((s) => !boundPtyIds.has(s.id))
- await Promise.allSettled(orphans.map((s) => window.api.pty.kill(s.id)))
- await refresh()
- }, [sessions, boundPtyIds, refresh, workspaceSessionReady])
-
- const handleNavigate = useCallback(
- (tabId: string) => {
- const worktreeId = tabIdToWorktreeId.get(tabId)
- if (worktreeId) {
- // Why: opening a session from the status bar is another worktree jump,
- // so route it through the shared activation path before restoring the
- // specific tab inside that worktree.
- activateAndRevealWorktree(worktreeId)
- }
- setActiveView('terminal')
- setActiveTab(tabId)
- },
- [tabIdToWorktreeId, setActiveView, setActiveTab]
- )
-
- return (
-
-
-
-
-
-