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. + + )}
)}