{presentation.hasMetaRow ? (
diff --git a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-idle-child-agents.tsx b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-idle-child-agents.tsx
index 3c4c67f5de9..06087b14b19 100644
--- a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-idle-child-agents.tsx
+++ b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-idle-child-agents.tsx
@@ -108,7 +108,7 @@ export function IdleChildAgentGroupRow({
className={cn(
'worktree-agent-row-hover',
layout === 'slot'
- ? 'flex h-[19px] w-full min-w-0 items-center gap-1.5 rounded-sm pr-2 pl-1 text-left text-2xs leading-none'
+ ? 'flex h-[19px] w-full min-w-0 items-center gap-1.5 rounded-sm pr-1.5 pl-1 text-left text-2xs leading-none'
: 'flex h-6 min-w-0 items-center gap-1 rounded-sm text-left text-[11px] leading-none',
layout === 'full' && '-ml-1 w-[calc(100%+0.25rem)] pl-1 pr-2',
layout === 'compact' && 'w-full px-1',
diff --git a/src/renderer/src/components/sidebar/workspace-card/surface/worktree-card-surface.tsx b/src/renderer/src/components/sidebar/workspace-card/surface/worktree-card-surface.tsx
index 57afd9dba5f..60ef696ade1 100644
--- a/src/renderer/src/components/sidebar/workspace-card/surface/worktree-card-surface.tsx
+++ b/src/renderer/src/components/sidebar/workspace-card/surface/worktree-card-surface.tsx
@@ -60,7 +60,11 @@ export function WorktreeCardSurface({ card }: { card: WorktreeCardController }):
: undefined
}
)
- const parentCardContent = slotLayout ?
: templateContent
+ const parentCardContent = slotLayout ? (
+
+ ) : (
+ templateContent
+ )
const { cardStyle } = geometryFor(!slotLayout && frame.statusLane)
const titleOnlyCard = frame.singleLine
@@ -68,19 +72,16 @@ export function WorktreeCardSurface({ card }: { card: WorktreeCardController }):
- {lineageChildren}
-
- ) : lineageChildren ? (
-
diff --git a/src/renderer/src/components/sidebar/worktree-card-children-chip.tsx b/src/renderer/src/components/sidebar/worktree-card-children-chip.tsx
new file mode 100644
index 00000000000..1fa689cdeb6
--- /dev/null
+++ b/src/renderer/src/components/sidebar/worktree-card-children-chip.tsx
@@ -0,0 +1,93 @@
+import React from 'react'
+import { ChevronDown, CircleX } from 'lucide-react'
+import { AgentQuestionIcon } from '@/components/AgentQuestionIcon'
+import { AgentWorkingSpinner } from '@/components/AgentWorkingSpinner'
+import { Button } from '@/components/ui/button'
+import { translate } from '@/i18n/i18n'
+import { cn } from '@/lib/utils'
+import { useLineageChildCounts, type LineageChildCounts } from './worktree-card-slot-rollup'
+
+const EMPTY_CHILD_IDS: readonly string[] = []
+
+// Most urgent first, matching the slot's priority.
+const ORDER: readonly (keyof LineageChildCounts)[] = ['needs', 'failed', 'unread', 'working']
+
+function countLabel(kind: keyof LineageChildCounts, count: number): string {
+ switch (kind) {
+ case 'needs':
+ return translate('sidebar.cardSlot.childNeeds', '{{count}} need you', {
+ count,
+ defaultValue_one: '{{count}} needs you'
+ })
+ case 'failed':
+ return translate('sidebar.cardSlot.childFailed', '{{count}} failed', { count })
+ case 'unread':
+ return translate('sidebar.cardSlot.childUnread', '{{count}} unread', { count })
+ case 'working':
+ return translate('sidebar.cardSlot.childWorking', '{{count}} working', { count })
+ }
+}
+
+// The slot's glyphs, so a count reads the same as the row it stands for.
+function CountGlyph({ kind }: { kind: keyof LineageChildCounts }): React.JSX.Element {
+ switch (kind) {
+ case 'needs':
+ return
+ case 'failed':
+ return
+ case 'working':
+ return
+ case 'unread':
+ return
+ }
+}
+
+/** "6 children", then only the children that need attention, so a quiet subtree stays short. */
+export function WorktreeCardChildrenChip({
+ childWorktreeIds = EMPTY_CHILD_IDS,
+ collapsed,
+ label,
+ ariaLabel,
+ onToggle
+}: {
+ childWorktreeIds?: readonly string[]
+ collapsed: boolean
+ label: string
+ ariaLabel: string
+ onToggle?: React.MouseEventHandler
+}): React.JSX.Element {
+ const counts = useLineageChildCounts(childWorktreeIds)
+ const shown = ORDER.filter((kind) => counts[kind] > 0)
+ const description = shown.map((kind) => countLabel(kind, counts[kind])).join(', ')
+ return (
+
+ )
+}
diff --git a/src/renderer/src/components/sidebar/worktree-card-slot-agent-line-row.tsx b/src/renderer/src/components/sidebar/worktree-card-slot-agent-line-row.tsx
index fcf58ec10c1..c33d9c1c30b 100644
--- a/src/renderer/src/components/sidebar/worktree-card-slot-agent-line-row.tsx
+++ b/src/renderer/src/components/sidebar/worktree-card-slot-agent-line-row.tsx
@@ -188,7 +188,7 @@ export const SlotAgentLineRow = React.memo(function SlotAgentLineRow({
aria-expanded={hasChildren ? childrenExpanded : undefined}
aria-current={view?.current ? 'true' : undefined}
className={cn(
- 'group/slot-line worktree-agent-row-hover flex h-[19px] w-full min-w-0 cursor-pointer items-center gap-1.5 rounded-sm pr-2 pl-1 text-left text-2xs leading-none outline-none focus-visible:ring-1 focus-visible:ring-worktree-sidebar-ring',
+ 'group/slot-line worktree-agent-row-hover flex h-[19px] w-full min-w-0 cursor-pointer items-center gap-1.5 rounded-sm pr-1.5 pl-1 text-left text-2xs leading-none outline-none focus-visible:ring-1 focus-visible:ring-worktree-sidebar-ring',
highlighted && 'bg-worktree-sidebar-accent',
sendTarget?.status === 'sending' && 'cursor-progress opacity-75',
sendTarget?.status === 'disabled' && 'cursor-default opacity-60'
diff --git a/src/renderer/src/components/sidebar/worktree-card-slot-agent-signals.ts b/src/renderer/src/components/sidebar/worktree-card-slot-agent-signals.ts
index 96279285b04..5d43f70a35b 100644
--- a/src/renderer/src/components/sidebar/worktree-card-slot-agent-signals.ts
+++ b/src/renderer/src/components/sidebar/worktree-card-slot-agent-signals.ts
@@ -51,7 +51,7 @@ export function buildSlotAgentSignals(
lineByPaneKey.set(agent.paneKey, {
agent,
dotState,
- signal: { kind, at: slotLineAt(agent, kind) }
+ signal: { kind, at: slotLineAt(agent, kind), monitoring: dotState === 'monitoring' }
})
})
diff --git a/src/renderer/src/components/sidebar/worktree-card-slot-fixture-seeds.ts b/src/renderer/src/components/sidebar/worktree-card-slot-fixture-seeds.ts
new file mode 100644
index 00000000000..151c12e1f2e
--- /dev/null
+++ b/src/renderer/src/components/sidebar/worktree-card-slot-fixture-seeds.ts
@@ -0,0 +1,116 @@
+import { useAppStore } from '@/store'
+import type { AgentStatusEntry } from '../../../../shared/agent-status/contracts/agent-status-types'
+import type { TerminalTab } from '../../../../shared/workspace-layout/terminal-tab-types'
+import type { Worktree } from '../../../../shared/worktree/types'
+import { worktreeCardStyleSettingsUpdate } from '../../../../shared/worktree/card-style'
+import * as agentExpansion from './workspace-card/activity/worktree-card-agents-expansion-state'
+
+/** Store seeding for the 2.0 card fixtures: workspaces, their agents, and which results are read. */
+export const MINUTE = 60_000
+
+type AgentSeed = {
+ agentType: string
+ state: AgentStatusEntry['state']
+ minutesAgo: number
+ toolName?: string
+ toolInput?: string
+ message?: string
+ failed?: boolean
+ unread?: boolean
+ prompt?: string
+}
+
+export type WorkspaceSeed = {
+ id: string
+ name: string
+ agents: AgentSeed[]
+ manualUnread?: boolean
+ lastActivityMinutesAgo?: number
+}
+
+export function makeWorktree(seed: WorkspaceSeed, now: number): Worktree {
+ return {
+ id: seed.id,
+ repoId: 'repo-orca',
+ path: `/repo/orca/${seed.name}`,
+ displayName: seed.name,
+ branch: `jinjing/${seed.name}`,
+ head: 'abc123',
+ isBare: false,
+ isMainWorktree: false,
+ comment: '',
+ linkedIssue: null,
+ linkedPR: null,
+ linkedLinearIssue: null,
+ isArchived: false,
+ isUnread: seed.manualUnread === true,
+ isPinned: false,
+ sortOrder: 0,
+ lastActivityAt: now - (seed.lastActivityMinutesAgo ?? 30) * MINUTE
+ }
+}
+
+export function seedWorkspaces(seeds: readonly WorkspaceSeed[], now: number): void {
+ agentExpansion.clearWorktreeAgentExpansionStateForTests()
+ const tabsByWorktree: Record = {}
+ const agentStatusByPaneKey: Record = {}
+ const unreadAgentCompletionPanes: Record = {}
+ const acknowledged: Record = {}
+ const ptyIdsByTabId: Record = {}
+ for (const seed of seeds) {
+ tabsByWorktree[seed.id] = seed.agents.map((agent, index) => {
+ const tabId = `${seed.id}-tab-${index}`
+ const paneKey = `${tabId}:00000000-0000-4000-8000-00000000000${index}`
+ agentStatusByPaneKey[paneKey] = {
+ state: agent.state,
+ prompt: agent.prompt ?? '',
+ updatedAt: now,
+ stateStartedAt: now - agent.minutesAgo * MINUTE,
+ agentType: agent.agentType,
+ paneKey,
+ tabId,
+ worktreeId: seed.id,
+ stateHistory: [],
+ ...(agent.toolName ? { toolName: agent.toolName, toolInput: agent.toolInput } : {}),
+ ...(agent.message ? { lastAssistantMessage: agent.message } : {}),
+ ...(agent.failed
+ ? { mainAgent: { state: 'done', outcome: 'failure', stateStartedAt: now } }
+ : {})
+ }
+ ptyIdsByTabId[tabId] = [`pty-${tabId}`]
+ if (agent.unread) {
+ unreadAgentCompletionPanes[paneKey] = 'agent-completion'
+ } else {
+ acknowledged[paneKey] = now - agent.minutesAgo * MINUTE
+ }
+ return {
+ id: tabId,
+ ptyId: `pty-${tabId}`,
+ worktreeId: seed.id,
+ title: agent.agentType,
+ customTitle: null,
+ color: null,
+ sortOrder: index,
+ createdAt: now - 60 * MINUTE
+ }
+ })
+ }
+ useAppStore.setState((state) => ({
+ settings: state.settings
+ ? {
+ ...state.settings,
+ ...worktreeCardStyleSettingsUpdate('experimental-2')
+ }
+ : state.settings,
+ worktreeCardProperties: ['status', 'inline-agents'],
+ tabsByWorktree: { ...state.tabsByWorktree, ...tabsByWorktree },
+ ptyIdsByTabId: { ...state.ptyIdsByTabId, ...ptyIdsByTabId },
+ agentStatusByPaneKey: { ...state.agentStatusByPaneKey, ...agentStatusByPaneKey },
+ acknowledgedAgentsByPaneKey: { ...state.acknowledgedAgentsByPaneKey, ...acknowledged },
+ agentStatusEpoch: state.agentStatusEpoch + 1,
+ unreadAgentCompletionPanes: {
+ ...state.unreadAgentCompletionPanes,
+ ...unreadAgentCompletionPanes
+ }
+ }))
+}
diff --git a/src/renderer/src/components/sidebar/worktree-card-slot-rollup.ts b/src/renderer/src/components/sidebar/worktree-card-slot-rollup.ts
index 27b36592077..ac36252a887 100644
--- a/src/renderer/src/components/sidebar/worktree-card-slot-rollup.ts
+++ b/src/renderer/src/components/sidebar/worktree-card-slot-rollup.ts
@@ -37,13 +37,15 @@ export function selectCollapsedLineageSlot(
if (!isExplicitAgentStatusFresh(entry, now, AGENT_STATUS_STALE_AFTER_MS)) {
continue
}
+ const dotState = agentRowDisplayDotState({ state: entry.state, entry })
lines.push({
kind: classifySlotLine({
- dotState: agentRowDisplayDotState({ state: entry.state, entry }),
+ dotState,
needsAttention: false,
unreadCompletion: isAgentSubjectUnread(turnRecords, entry.paneKey)
}),
- at: entry.stateStartedAt > 0 ? entry.stateStartedAt : null
+ at: entry.stateStartedAt > 0 ? entry.stateStartedAt : null,
+ monitoring: dotState === 'monitoring'
})
}
for (const retained of selectRetainedAgentEntriesForWorktree(state, worktreeId)) {
@@ -107,3 +109,29 @@ export function useLineageLatestAt(descendantIds: readonly string[]): number | n
)
)
}
+
+export type LineageChildCounts = { needs: number; failed: number; unread: number; working: number }
+
+/** Direct children counted once each by their most urgent exception; quiet children aren't counted. */
+export function selectLineageChildCounts(
+ state: AppState,
+ childIds: readonly string[]
+): LineageChildCounts {
+ const counts: LineageChildCounts = { needs: 0, failed: 0, unread: 0, working: 0 }
+ for (const childId of childIds) {
+ const signal = selectCollapsedLineageSlot(state, [childId])
+ if (signal && signal.kind in counts) {
+ counts[signal.kind as keyof LineageChildCounts] += 1
+ }
+ }
+ return counts
+}
+
+export function useLineageChildCounts(childIds: readonly string[]): LineageChildCounts {
+ const childKey = childIds.join('\n')
+ const selector = useCallback(
+ (state: AppState) => selectLineageChildCounts(state, childKey ? childKey.split('\n') : []),
+ [childKey]
+ )
+ return useAppStore(useShallow(selector))
+}
diff --git a/src/renderer/src/components/sidebar/worktree-card-slot-signal.ts b/src/renderer/src/components/sidebar/worktree-card-slot-signal.ts
index 8b736f55157..77e31ed154b 100644
--- a/src/renderer/src/components/sidebar/worktree-card-slot-signal.ts
+++ b/src/renderer/src/components/sidebar/worktree-card-slot-signal.ts
@@ -5,14 +5,15 @@ import { lastEnteredDoneAt } from '@/components/dashboard/agent-finished-timesta
/** What one agent line means for the Experimental 2.0 card, in slot priority order. */
export type SlotLineKind = 'needs' | 'failed' | 'unread' | 'working' | 'seen' | 'idle'
-export type SlotLineSignal = { kind: SlotLineKind; at: number | null }
+/** `monitoring`: working, but watching rather than taking turns; drawn with its own glyph. */
+export type SlotLineSignal = { kind: SlotLineKind; at: number | null; monitoring?: boolean }
/** One right-hand slot: the first exception that applies, otherwise a time, or quiet. */
export type SlotSignal =
| { kind: 'needs'; count: number }
| { kind: 'failed' }
| { kind: 'unread'; at: number | null; manual: boolean }
- | { kind: 'working'; at: number | null }
+ | { kind: 'working'; at: number | null; monitoring?: boolean }
| { kind: 'offline' }
| { kind: 'time'; at: number | null }
| { kind: 'quiet' }
@@ -93,7 +94,11 @@ export function slotExceptionFor(lines: readonly SlotLineSignal[]): SlotSignal |
}
const working = lines.filter((line) => line.kind === 'working')
if (working.length > 0) {
- return { kind: 'working', at: latest(working) }
+ return {
+ kind: 'working',
+ at: latest(working),
+ monitoring: working.every((line) => line.monitoring === true)
+ }
}
return null
}
diff --git a/src/renderer/src/i18n/en-runtime-required.json b/src/renderer/src/i18n/en-runtime-required.json
index d2030d20df8..805610d2680 100644
--- a/src/renderer/src/i18n/en-runtime-required.json
+++ b/src/renderer/src/i18n/en-runtime-required.json
@@ -2531,6 +2531,10 @@
"working_other": "{{count}} working"
}
},
+ "cardSlot": {
+ "childNeeds_one": "{{count}} needs you",
+ "childNeeds_other": "{{count}} need you"
+ },
"createProject": {
"errors": {
"createDirectoryFailed": "Failed to create directory: {{detail}}",
diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json
index 2b60844b3de..62cfc995edb 100644
--- a/src/renderer/src/i18n/locales/en.json
+++ b/src/renderer/src/i18n/locales/en.json
@@ -259,6 +259,7 @@
"markedUnread": "Marked unread by you",
"unread": "Unread result",
"working": "Working",
+ "monitoring": "Monitoring",
"offlineLabel": "Host offline",
"offline": "offline",
"needYouCount": "{{count}} need you",
@@ -268,7 +269,12 @@
"detailsRow": "Details row",
"hostRemoved": "host removed",
"disconnected": "disconnected",
- "orcaServer": "Orca server"
+ "orcaServer": "Orca server",
+ "childNeeds_one": "{{count}} needs you",
+ "childNeeds_other": "{{count}} need you",
+ "childFailed": "{{count}} failed",
+ "childUnread": "{{count}} unread",
+ "childWorking": "{{count}} working"
}
},
"app": {