diff --git a/src/renderer/src/components/activity/ActivityPrototypePage.tsx b/src/renderer/src/components/activity/ActivityPrototypePage.tsx
index a99da7ee649..4b3a687d0ab 100644
--- a/src/renderer/src/components/activity/ActivityPrototypePage.tsx
+++ b/src/renderer/src/components/activity/ActivityPrototypePage.tsx
@@ -7,21 +7,19 @@ import {
Bell,
BellDot,
BellOff,
- GitBranch,
MessageSquareText,
Search,
- Settings
+ Settings,
+ TerminalSquare
} from 'lucide-react'
import { AgentIcon } from '@/lib/agent-catalog'
import { agentTypeToIconAgent, formatAgentTypeLabel } from '@/lib/agent-status'
import { activateTabAndFocusPane } from '@/lib/activate-tab-and-focus-pane'
-import { activateAndRevealWorktree } from '@/lib/worktree-activation'
import { useAppStore } from '@/store'
import type { RetainedAgentEntry } from '@/store/slices/agent-status'
import { getRepoMapFromState, getWorktreeMapFromState } from '@/store/selectors'
import { useSidebarResize } from '@/hooks/useSidebarResize'
-import { AgentStateDot, agentStateLabel, type AgentDotState } from '@/components/AgentStateDot'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
@@ -36,6 +34,7 @@ import { Input } from '@/components/ui/input'
import { Toggle } from '@/components/ui/toggle'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
+import { ACTIVITY_TERMINAL_PORTAL_TARGET_ID } from './activity-terminal-portal'
import type { Repo, TerminalTab, Worktree } from '../../../../shared/types'
import type {
AgentStatusEntry,
@@ -101,13 +100,6 @@ function formatRelativeTime(timestamp: number): string {
return relativeTimeFormatter.format(diffDays, 'day')
}
-function asDotState(state: AgentStatusState): AgentDotState {
- if (state === 'blocked' || state === 'waiting' || state === 'done') {
- return state
- }
- return 'idle'
-}
-
function paneIdFromPaneKey(paneKey: string): number | null {
const colon = paneKey.indexOf(':')
const tail = colon > 0 ? paneKey.slice(colon + 1) : ''
@@ -302,108 +294,6 @@ function EventRepoBadge({ repo }: { repo: Repo | null }): React.JSX.Element | nu
)
}
-function ActivityRow({
- event,
- density,
- onMarkRead
-}: {
- event: ActivityEvent
- density: ActivityDensity
- onMarkRead: (event: ActivityEvent) => void
-}): React.JSX.Element {
- const compact = density === 'compact'
- const dotState = asDotState(event.state)
-
- // Why (row click = navigate + ack): the inline "Jump to agent" button only
- // rendered when agentAlive (so retained-done rows had no jump affordance at
- // all) and in compact mode it was hover-only. Users naturally click anywhere
- // on the row expecting it to navigate, so the row itself takes over the
- // navigate role and subsumes the button. Per the design doc we drop the
- // agentAlive gate entirely (option 1): activateAndRevealWorktree is safe
- // unconditionally and activateTabAndFocusPane silently no-ops on a missing
- // tab id, so a stale-tab click is just a soft no-op.
- const openAgent = (clickEvent?: React.MouseEvent | React.KeyboardEvent): void => {
- clickEvent?.stopPropagation()
- onMarkRead(event)
- activateAndRevealWorktree(event.worktree.id)
- activateTabAndFocusPane(event.tab.id, paneIdFromPaneKey(event.entry.paneKey))
- }
-
- return (
-
{
- if (e.key === 'Enter' || e.key === ' ') {
- e.preventDefault()
- openAgent(e)
- }
- }}
- className={cn(
- 'group relative grid grid-cols-[2rem_minmax(0,1fr)_7.25rem] gap-3 border-b border-border px-3 cursor-pointer transition-colors hover:bg-accent/40',
- compact ? 'py-2' : 'py-3.5',
- event.unread && 'bg-accent/20'
- )}
- >
- {/* Why (left-edge bar): matches ThreadRow's unread treatment so the two
- surfaces share one unread vocabulary. A row-level marker keeps the
- state-icon column uncrowded — earlier attempts (mini-dot, bell glyph)
- competed with the AgentStateDot. */}
- {event.unread ? (
-
- ) : null}
-
-
-
-
-
-
-
-
- {event.entry.interrupted ? 'Interrupted' : agentStateLabel(dotState)}
-
-
-
-
-
-
-
- {agentTitle(event)}
-
-
-
-
-
-
-
-
- {formatAgentTypeLabel(event.agentType)}
-
-
-
-
- {agentSummary(event)}
-
-
-
-
- {event.worktree.displayName}
-
- {agentMeta(event)}
-
-
-
-
-
-
-
- )
-}
-
function ThreadRow({
thread,
density,
@@ -444,13 +334,17 @@ function ThreadRow({
) : null}
-
-
+
+
+ {/* Why (line-clamp-2 + smaller size): prompts can be long; clamping
+ to two lines lets users read more of the prompt while keeping
+ rows scannable. text-[13px] tightens the leading slightly so two
+ clamped lines don't dominate the row vertically. */}
@@ -566,17 +460,14 @@ export default function ActivityPrototypePage(): React.JSX.Element {
}, [allThreads, readFilter, query])
useEffect(() => {
- if (visibleThreads.length === 0) {
+ if (selectedPaneKey && !allThreads.some((thread) => thread.paneKey === selectedPaneKey)) {
setSelectedPaneKey(null)
- return
}
- if (!selectedPaneKey || !visibleThreads.some((thread) => thread.paneKey === selectedPaneKey)) {
- setSelectedPaneKey(visibleThreads[0].paneKey)
- }
- }, [selectedPaneKey, visibleThreads])
+ }, [allThreads, selectedPaneKey])
- const selectedThread =
- visibleThreads.find((thread) => thread.paneKey === selectedPaneKey) ?? visibleThreads[0] ?? null
+ const selectedThread = selectedPaneKey
+ ? (allThreads.find((thread) => thread.paneKey === selectedPaneKey) ?? null)
+ : null
const markThreadRead = (thread: AgentPaneThread): void => {
storeData.acknowledgeAgents([thread.paneKey])
@@ -586,9 +477,22 @@ export default function ActivityPrototypePage(): React.JSX.Element {
storeData.unacknowledgeAgents([thread.paneKey])
}
+ const activateThreadTerminal = (thread: AgentPaneThread): void => {
+ const state = useAppStore.getState()
+ if (state.activeRepoId !== thread.worktree.repoId) {
+ state.setActiveRepo(thread.worktree.repoId)
+ }
+ if (state.activeWorktreeId !== thread.worktree.id) {
+ state.setActiveWorktree(thread.worktree.id)
+ }
+ state.setActiveTabType('terminal')
+ activateTabAndFocusPane(thread.latestEvent.tab.id, paneIdFromPaneKey(thread.paneKey))
+ }
+
const selectThread = (thread: AgentPaneThread): void => {
setSelectedPaneKey(thread.paneKey)
markThreadRead(thread)
+ activateThreadTerminal(thread)
}
const toggleThreadRead = (thread: AgentPaneThread): void => {
@@ -599,10 +503,6 @@ export default function ActivityPrototypePage(): React.JSX.Element {
markThreadUnread(thread)
}
- const markRead = (event: ActivityEvent): void => {
- storeData.acknowledgeAgents([event.entry.paneKey])
- }
-
// Why (page padding): drop top + horizontal padding so the page extends to
// the window's left and right edges (matching how sidebars abut the chrome
// elsewhere). The titlebar (ActivityTitlebarControls) already provides the
@@ -722,7 +622,7 @@ export default function ActivityPrototypePage(): React.JSX.Element {
- {selectedThread.paneTitle}
+ {selectedThread.paneTitle}
@@ -730,21 +630,28 @@ export default function ActivityPrototypePage(): React.JSX.Element {
-
- {selectedThread.events.map((event) => (
-
- ))}
-
+ {/* Why: Terminal stays mounted in the hidden workspace tree while
+ Activity is open. This target lets that existing TerminalPane
+ move here instead of creating a second PTY/xterm owner. */}
+
) : (
-
- No activity yet.
+ {visibleThreads.length === 0 ? (
+ <>
+
+ No activity yet.
+ >
+ ) : (
+ <>
+
+ Select an agent to open its terminal.
+ >
+ )}
)}