mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 10:27:33 +00:00
Improve experimental worktree cards with compact agent summaries
- Add an optional details row for workspace metadata and indicators - Roll up descendant activity timestamps to parent cards
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import type { DashboardAgentRow } from '@/components/dashboard/useDashboardData'
|
||||
import { useAgentRowConversationName } from '@/components/dashboard/use-agent-row-conversation-name'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
@@ -103,6 +104,46 @@ const SlotAgentLineRow = React.memo(function SlotAgentLineRow({
|
||||
)
|
||||
})
|
||||
|
||||
/** Expanded agents' header: the label sits in the icon column and the chevron in the slot column. */
|
||||
export function SlotAgentLinesHeader({
|
||||
label,
|
||||
onCollapse
|
||||
}: {
|
||||
label: string
|
||||
onCollapse: () => void
|
||||
}): React.JSX.Element {
|
||||
const handleClick = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
onCollapse()
|
||||
},
|
||||
[onCollapse]
|
||||
)
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
draggable={false}
|
||||
className="worktree-agent-row-hover mt-[3px] flex h-[19px] w-full min-w-0 items-center gap-1.5 rounded-sm pr-2 text-left text-2xs leading-none font-medium text-muted-foreground outline-none focus-visible:ring-1 focus-visible:ring-worktree-sidebar-ring"
|
||||
aria-expanded={true}
|
||||
aria-label={translate(
|
||||
'auto.components.sidebar.worktree.card.compact.agents.0c1debfe84',
|
||||
'Collapse {{value0}}',
|
||||
{ value0: label }
|
||||
)}
|
||||
onClick={handleClick}
|
||||
onMouseDown={stopPropagation}
|
||||
onPointerDown={stopPropagation}
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate">{label}</span>
|
||||
<ChevronDown className="size-3 shrink-0" aria-hidden />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function stopPropagation(event: React.SyntheticEvent): void {
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
/** Experimental 2.0 agent lines: icon, what it's doing or said, and the line's own slot. */
|
||||
export function WorktreeCardSlotAgentLines({
|
||||
worktreeId,
|
||||
@@ -138,7 +179,7 @@ export function WorktreeCardSlotAgentLines({
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="mt-px flex flex-col gap-px pl-3.5"
|
||||
className="mt-px flex flex-col gap-px"
|
||||
role="group"
|
||||
aria-label={translate('auto.components.sidebar.WorktreeCardAgents.1b0a156717', 'Agents')}
|
||||
onDoubleClick={stopBubble}
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { Worktree } from '../../../../shared/worktree/types'
|
||||
import { folderWorkspaceToWorktree } from '../../../../shared/folder-workspaces/folder-workspace-worktree'
|
||||
import { worktreeCardStyleSettingsUpdate } from '../../../../shared/worktree/card-style'
|
||||
import WorktreeCard from './workspace-card/surface/WorktreeCard'
|
||||
import * as agentExpansion from './workspace-card/activity/worktree-card-agents-expansion-state'
|
||||
|
||||
const MINUTE = 60_000
|
||||
const SIDEBAR_WIDTH = 320
|
||||
@@ -56,6 +57,7 @@ function makeWorktree(seed: WorkspaceSeed, now: number): Worktree {
|
||||
}
|
||||
|
||||
function seedWorkspaces(seeds: readonly WorkspaceSeed[], now: number): void {
|
||||
agentExpansion.clearWorktreeAgentExpansionStateForTests()
|
||||
const tabsByWorktree: Record<string, TerminalTab[]> = {}
|
||||
const agentStatusByPaneKey: Record<string, AgentStatusEntry> = {}
|
||||
const unreadAgentCompletionPanes: Record<string, 'agent-completion'> = {}
|
||||
@@ -76,7 +78,9 @@ function seedWorkspaces(seeds: readonly WorkspaceSeed[], now: number): void {
|
||||
stateHistory: [],
|
||||
...(agent.toolName ? { toolName: agent.toolName, toolInput: agent.toolInput } : {}),
|
||||
...(agent.message ? { lastAssistantMessage: agent.message } : {}),
|
||||
...(agent.failed ? { mainAgent: { state: 'done', outcome: 'failure', stateStartedAt: now } } : {})
|
||||
...(agent.failed
|
||||
? { mainAgent: { state: 'done', outcome: 'failure', stateStartedAt: now } }
|
||||
: {})
|
||||
}
|
||||
ptyIdsByTabId[tabId] = [`pty-${tabId}`]
|
||||
if (agent.unread) {
|
||||
@@ -309,7 +313,7 @@ function Card({
|
||||
children,
|
||||
childCount = 0,
|
||||
collapsed = false,
|
||||
hiddenDescendantIds
|
||||
descendantIds
|
||||
}: {
|
||||
worktree: Worktree
|
||||
nested?: boolean
|
||||
@@ -317,7 +321,7 @@ function Card({
|
||||
children?: React.ReactNode
|
||||
childCount?: number
|
||||
collapsed?: boolean
|
||||
hiddenDescendantIds?: readonly string[]
|
||||
descendantIds?: readonly string[]
|
||||
}): React.JSX.Element {
|
||||
return (
|
||||
<div className="relative" data-worktree-id={worktree.id}>
|
||||
@@ -330,7 +334,7 @@ function Card({
|
||||
nativeDragEnabled={false}
|
||||
lineageChildCount={childCount}
|
||||
lineageCollapsed={collapsed}
|
||||
lineageHiddenDescendantIds={hiddenDescendantIds}
|
||||
lineageDescendantIds={descendantIds}
|
||||
lineageChildren={children}
|
||||
onLineageToggle={childCount > 0 ? noop : undefined}
|
||||
/>
|
||||
@@ -348,10 +352,8 @@ const nestedWorktrees = worktreesFor(NESTED, now)
|
||||
FOLDER_WORKSPACE.isUnread = false
|
||||
FOLDER_WORKSPACE.lastActivityAt = now - 9 * MINUTE
|
||||
|
||||
export const exceptions = defineFixture({
|
||||
width: SIDEBAR_WIDTH,
|
||||
setup: () => seedWorkspaces(FLAT, now),
|
||||
render: () => (
|
||||
function FlatList(): React.JSX.Element {
|
||||
return (
|
||||
<SidebarFrame>
|
||||
{FLAT.map((seed) => (
|
||||
<Card
|
||||
@@ -363,6 +365,12 @@ export const exceptions = defineFixture({
|
||||
<Card worktree={FOLDER_WORKSPACE} />
|
||||
</SidebarFrame>
|
||||
)
|
||||
}
|
||||
|
||||
export const exceptions = defineFixture({
|
||||
width: SIDEBAR_WIDTH,
|
||||
setup: () => seedWorkspaces(FLAT, now),
|
||||
render: () => <FlatList />
|
||||
})
|
||||
|
||||
export const nesting = defineFixture({
|
||||
@@ -370,8 +378,17 @@ export const nesting = defineFixture({
|
||||
setup: () => seedWorkspaces(NESTED, now),
|
||||
render: () => (
|
||||
<SidebarFrame>
|
||||
<Card worktree={nestedWorktrees.get('wt-parent')!} childCount={1}>
|
||||
<Card nested worktree={nestedWorktrees.get('wt-child')!} childCount={1}>
|
||||
<Card
|
||||
worktree={nestedWorktrees.get('wt-parent')!}
|
||||
childCount={1}
|
||||
descendantIds={['wt-child', 'wt-grandchild']}
|
||||
>
|
||||
<Card
|
||||
nested
|
||||
worktree={nestedWorktrees.get('wt-child')!}
|
||||
childCount={1}
|
||||
descendantIds={['wt-grandchild']}
|
||||
>
|
||||
<Card nested worktree={nestedWorktrees.get('wt-grandchild')!} />
|
||||
</Card>
|
||||
</Card>
|
||||
@@ -379,8 +396,23 @@ export const nesting = defineFixture({
|
||||
worktree={nestedWorktrees.get('wt-collapsed-parent')!}
|
||||
childCount={1}
|
||||
collapsed
|
||||
hiddenDescendantIds={['wt-collapsed-child']}
|
||||
descendantIds={['wt-collapsed-child']}
|
||||
/>
|
||||
</SidebarFrame>
|
||||
)
|
||||
})
|
||||
|
||||
export const agentsExpanded = defineFixture({
|
||||
width: SIDEBAR_WIDTH,
|
||||
setup: () => {
|
||||
seedWorkspaces(FLAT, now)
|
||||
const none = new Set<string>()
|
||||
const state = {
|
||||
expandedLineageParents: none,
|
||||
expandedIdleChildGroups: none,
|
||||
compactRootListExpanded: true
|
||||
}
|
||||
FLAT.forEach((seed) => agentExpansion.seedWorktreeAgentExpansionStateForTests(seed.id, state))
|
||||
},
|
||||
render: () => <FlatList />
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
|
||||
@@ -14,7 +14,7 @@ import { agentRowDisplayDotState as getAgentDotState } from '@/lib/agent-row-dot
|
||||
import { WorktreeCardIdentityHover } from './worktree-card-identity-hover'
|
||||
import type { WorktreeCardPresentation } from './workspace-card/surface/worktree-card-presentation'
|
||||
import { WorktreeCardSecondaryRows } from './workspace-card/surface/worktree-card-secondary-rows'
|
||||
import { useCollapsedLineageSlot } from './worktree-card-slot-rollup'
|
||||
import { useCollapsedLineageSlot, useLineageLatestAt } from './worktree-card-slot-rollup'
|
||||
import {
|
||||
classifySlotLine,
|
||||
compareSlotLines,
|
||||
@@ -26,9 +26,21 @@ import {
|
||||
WorktreeCardRenameFailedButton
|
||||
} from './worktree-card-title-actions'
|
||||
import { WorktreeCardSlot } from './WorktreeCardSlot'
|
||||
import { WorktreeCardSlotAgentLines, type SlotAgentLine } from './WorktreeCardSlotAgentLines'
|
||||
import { useWorktreeAgentExpansionState } from './workspace-card/activity/worktree-card-agents-expansion-state'
|
||||
import {
|
||||
CompactAgentExpansion,
|
||||
CompactAgentSummaryButton
|
||||
} from './workspace-card/activity/worktree-card-compact-agents'
|
||||
import { dispatchSuppressScrollAdjustment } from './workspace-card/activity/WorktreeCardAgents'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import {
|
||||
SlotAgentLinesHeader,
|
||||
WorktreeCardSlotAgentLines,
|
||||
type SlotAgentLine
|
||||
} from './WorktreeCardSlotAgentLines'
|
||||
import { shouldBeginWorktreeRename } from './workspace-card/controller/worktree-card-model'
|
||||
import { WorktreeTitleInlineRename } from './workspace-card/identity/WorktreeTitleInlineRename'
|
||||
import { WorktreeCardMetaRow } from './workspace-card/metadata/worktree-card-meta-row'
|
||||
|
||||
const NO_DESCENDANTS: readonly string[] = []
|
||||
|
||||
@@ -79,42 +91,82 @@ export function WorktreeCardSlotContent({
|
||||
showDeleteQuickAction,
|
||||
showLineageChildChip,
|
||||
lineageCollapsed,
|
||||
lineageHiddenDescendantIds,
|
||||
lineageDescendantIds,
|
||||
onLineageToggle,
|
||||
lineageChildAriaLabel,
|
||||
childWorkspaceShortLabel
|
||||
childWorkspaceShortLabel,
|
||||
settings
|
||||
} = card
|
||||
const lines = useSlotAgentLines(worktree.id)
|
||||
const now = useNow(30_000)
|
||||
const hasLiveMarker = useAppStore((s) => workspaceHasLiveMarker(s, worktree.id))
|
||||
// A flag with no tab or agent marker behind it was set by the user.
|
||||
const manualUnread = worktree.isUnread && !hasLiveMarker
|
||||
const descendantIds = lineageDescendantIds ?? NO_DESCENDANTS
|
||||
const collapsedLineage = useCollapsedLineageSlot(
|
||||
lineageCollapsed ? (lineageHiddenDescendantIds ?? NO_DESCENDANTS) : NO_DESCENDANTS
|
||||
lineageCollapsed ? descendantIds : NO_DESCENDANTS
|
||||
)
|
||||
const ownLinesShown = showInlineAgentList && lines.length > 0
|
||||
const lineageLatestAt = useLineageLatestAt(descendantIds)
|
||||
// Several agents fold into the 1.0 summary row; the title slot speaks for them until expanded.
|
||||
const { compactRootListExpanded: agentsExpanded, toggleCompactRootList } =
|
||||
useWorktreeAgentExpansionState(worktree.id)
|
||||
const toggleAgents = useCallback(() => {
|
||||
dispatchSuppressScrollAdjustment()
|
||||
toggleCompactRootList()
|
||||
}, [toggleCompactRootList])
|
||||
const showLines = showInlineAgentList && lines.length > 0
|
||||
const foldsAgents = showLines && lines.length > 1
|
||||
const ownLinesShown = showLines && (!foldsAgents || agentsExpanded)
|
||||
const titleSlot = titleRowSlot({
|
||||
offline: isRuntimeDisconnected,
|
||||
hiddenLines: ownLinesShown ? [] : lines.map((line) => line.signal),
|
||||
collapsedLineage,
|
||||
manualUnread,
|
||||
ownLinesShown,
|
||||
lastActivityAt: worktree.lastActivityAt > 0 ? worktree.lastActivityAt : null
|
||||
lastActivityAt: worktree.lastActivityAt > 0 ? worktree.lastActivityAt : null,
|
||||
lineageLatestAt
|
||||
})
|
||||
const showDetailsRow =
|
||||
settings?.worktreeCardDetailsRow === true &&
|
||||
(presentation.hasMetaRow || presentation.detailsAndPorts !== null)
|
||||
const titleBold = manualUnread || lines.some((line) => line.signal.kind === 'unread')
|
||||
|
||||
const agentLines = showLines ? (
|
||||
<WorktreeCardSlotAgentLines
|
||||
worktreeId={worktree.id}
|
||||
lines={lines}
|
||||
now={now}
|
||||
offline={isRuntimeDisconnected}
|
||||
/>
|
||||
) : null
|
||||
|
||||
const agentCountLabel = translate('sidebar.agentSummary.agentCount', '{{count}} agents', {
|
||||
count: lines.length,
|
||||
defaultValue_one: '{{count}} agent'
|
||||
})
|
||||
const slotRows = (
|
||||
<>
|
||||
{ownLinesShown ? (
|
||||
<WorktreeCardSlotAgentLines
|
||||
worktreeId={worktree.id}
|
||||
lines={lines}
|
||||
now={now}
|
||||
offline={isRuntimeDisconnected}
|
||||
/>
|
||||
) : null}
|
||||
{foldsAgents ? (
|
||||
<>
|
||||
{agentsExpanded ? (
|
||||
<SlotAgentLinesHeader label={agentCountLabel} onCollapse={toggleAgents} />
|
||||
) : (
|
||||
<div className="mt-[3px] min-w-0 pr-2">
|
||||
<CompactAgentSummaryButton
|
||||
agents={lines.map((line) => line.agent)}
|
||||
subjectLabel={agentCountLabel}
|
||||
expanded={false}
|
||||
onToggle={toggleAgents}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<CompactAgentExpansion expanded={agentsExpanded}>{agentLines}</CompactAgentExpansion>
|
||||
</>
|
||||
) : (
|
||||
agentLines
|
||||
)}
|
||||
{showLineageChildChip ? (
|
||||
<div className="mt-[3px] flex min-w-0 pl-3.5">
|
||||
<div className="mt-[3px] flex min-w-0">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
@@ -144,32 +196,51 @@ export function WorktreeCardSlotContent({
|
||||
>
|
||||
<WorktreeCardIdentityHover card={card} presentation={presentation}>
|
||||
<div
|
||||
className="group/worktree-card flex h-5 min-w-0 items-center gap-1.5 pr-2"
|
||||
className="group/worktree-card flex min-w-0 flex-col"
|
||||
data-worktree-card-hover-trigger=""
|
||||
>
|
||||
<WorktreeTitleInlineRename
|
||||
displayName={visibleCardTitle}
|
||||
disabled={isDeleting || affiliateListMode}
|
||||
showUnreadEmphasis={titleBold}
|
||||
dimReadTitle
|
||||
className="text-[13px] leading-5"
|
||||
editingClassName="flex-1"
|
||||
titleWrapper={presentation.titleWrapper}
|
||||
onEditingChange={affiliateListMode ? undefined : setTitleRenaming}
|
||||
onRename={handleRenameTitle}
|
||||
beginEditing={
|
||||
!affiliateListMode &&
|
||||
shouldBeginWorktreeRename(renamingWorktreeId, worktree.id, renameRowKey)
|
||||
}
|
||||
onBeginEditingConsumed={
|
||||
affiliateListMode ? undefined : () => setRenamingWorktreeId(null)
|
||||
}
|
||||
/>
|
||||
<WorktreeCardRenameFailedButton card={card} />
|
||||
<span className="ml-auto flex shrink-0 items-center gap-1">
|
||||
{showDeleteQuickAction ? <WorktreeCardDeleteQuickAction card={card} /> : null}
|
||||
<WorktreeCardSlot signal={titleSlot} now={now} />
|
||||
</span>
|
||||
<div className="flex h-5 min-w-0 items-center gap-1.5 pr-2">
|
||||
<WorktreeTitleInlineRename
|
||||
displayName={visibleCardTitle}
|
||||
disabled={isDeleting || affiliateListMode}
|
||||
showUnreadEmphasis={titleBold}
|
||||
dimReadTitle
|
||||
className="text-[13px] leading-5"
|
||||
editingClassName="flex-1"
|
||||
titleWrapper={presentation.titleWrapper}
|
||||
onEditingChange={affiliateListMode ? undefined : setTitleRenaming}
|
||||
onRename={handleRenameTitle}
|
||||
beginEditing={
|
||||
!affiliateListMode &&
|
||||
shouldBeginWorktreeRename(renamingWorktreeId, worktree.id, renameRowKey)
|
||||
}
|
||||
onBeginEditingConsumed={
|
||||
affiliateListMode ? undefined : () => setRenamingWorktreeId(null)
|
||||
}
|
||||
/>
|
||||
<WorktreeCardRenameFailedButton card={card} />
|
||||
<span className="ml-auto flex shrink-0 items-center gap-1">
|
||||
{showDeleteQuickAction ? <WorktreeCardDeleteQuickAction card={card} /> : null}
|
||||
<WorktreeCardSlot signal={titleSlot} now={now} />
|
||||
</span>
|
||||
</div>
|
||||
{showDetailsRow ? (
|
||||
<div
|
||||
className="mt-1 mb-1 flex h-4 min-w-0 items-center gap-1.5 pr-2"
|
||||
data-worktree-card-details-row=""
|
||||
>
|
||||
{presentation.hasMetaRow ? (
|
||||
<div className="min-w-0 flex-1 pl-2">
|
||||
<WorktreeCardMetaRow card={card} presentation={presentation} />
|
||||
</div>
|
||||
) : null}
|
||||
{presentation.detailsAndPorts ? (
|
||||
<div className="ml-auto flex shrink-0 items-center gap-1">
|
||||
{presentation.detailsAndPorts}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</WorktreeCardIdentityHover>
|
||||
<WorktreeCardSecondaryRows card={card} presentation={presentation} slotRows={slotRows} />
|
||||
|
||||
@@ -32,6 +32,13 @@ import {
|
||||
} from './worktree-card-idle-child-agents'
|
||||
import { recomputeOn } from '@/lib/recompute-on'
|
||||
|
||||
export const SUPPRESS_WORKTREE_LIST_SCROLL_ADJUSTMENT_EVENT =
|
||||
'orca-suppress-worktree-list-scroll-adjustment'
|
||||
|
||||
export const dispatchSuppressScrollAdjustment = () => {
|
||||
window.dispatchEvent(new CustomEvent(SUPPRESS_WORKTREE_LIST_SCROLL_ADJUSTMENT_EVENT))
|
||||
}
|
||||
|
||||
type Props = {
|
||||
worktreeId: string
|
||||
agents?: DashboardAgentRowData[]
|
||||
|
||||
+2
-2
@@ -37,8 +37,8 @@ export type WorktreeCardProps = {
|
||||
lineageChildCount?: number
|
||||
lineageChildWorktreeIds?: readonly string[]
|
||||
lineageCollapsed?: boolean
|
||||
/** Workspaces folded under a collapsed parent; the 2.0 card reports their exceptions. */
|
||||
lineageHiddenDescendantIds?: readonly string[]
|
||||
/** Workspaces nested under this one; the 2.0 card rolls up their latest time and, when collapsed, their exceptions. */
|
||||
lineageDescendantIds?: readonly string[]
|
||||
lineageChildren?: React.ReactNode
|
||||
lineageChildrenStyle?: React.CSSProperties
|
||||
onLineageToggle?: (event: React.MouseEvent<HTMLButtonElement>) => void
|
||||
|
||||
+3
-1
@@ -11,7 +11,9 @@ import type { WorktreeCardController } from '../controller/use-worktree-card-con
|
||||
export function renderWorktreeCardIndicators(
|
||||
card: WorktreeCardController
|
||||
): React.ReactElement | null {
|
||||
const { worktree, hasDetails, hasPorts, newCardStyle, showPR, showIssue } = card
|
||||
const { worktree, hasDetails, hasPorts, showPR, showIssue } = card
|
||||
// 1.0 draws the PR in its status lane; 2.0 has no lane, so it lists the PR like Classic.
|
||||
const newCardStyle = card.newCardStyle && card.worktreeCardStyle !== 'experimental-2'
|
||||
if (!hasDetails && !hasPorts) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ const WorktreeCard = React.memo(function WorktreeCard({
|
||||
lineageChildCount = 0,
|
||||
lineageChildWorktreeIds,
|
||||
lineageCollapsed = false,
|
||||
lineageHiddenDescendantIds,
|
||||
lineageDescendantIds,
|
||||
lineageChildren,
|
||||
lineageChildrenStyle,
|
||||
onLineageToggle,
|
||||
@@ -74,7 +74,7 @@ const WorktreeCard = React.memo(function WorktreeCard({
|
||||
lineageChildCount,
|
||||
lineageChildWorktreeIds,
|
||||
lineageCollapsed,
|
||||
lineageHiddenDescendantIds,
|
||||
lineageDescendantIds,
|
||||
lineageChildren,
|
||||
lineageChildrenStyle,
|
||||
onLineageToggle,
|
||||
|
||||
+18
@@ -19,6 +19,7 @@ import {
|
||||
getWorktreeCardDisplayOptions
|
||||
} from '../../worktree-list/controls/sidebar-workspace-option-items'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { getWorktreeCardStyle } from '../../../../../../shared/worktree/card-style'
|
||||
|
||||
type CardMenuItemProps = {
|
||||
preserveWorkspaceBoardOpen: boolean
|
||||
@@ -68,6 +69,9 @@ export function WorktreeCardDisplayMenuItem({
|
||||
const hasProjectGroups = useAppStore((s) => s.projectGroups.length > 0)
|
||||
const toggleProperty = useToggleCardProperty()
|
||||
const options = useMemo(() => getWorktreeCardDisplayOptions(hasProjectGroups), [hasProjectGroups])
|
||||
const slotLayout = useAppStore((s) => getWorktreeCardStyle(s.settings) === 'experimental-2')
|
||||
const detailsRow = useAppStore((s) => s.settings?.worktreeCardDetailsRow === true)
|
||||
const updateSettings = useAppStore((s) => s.updateSettings)
|
||||
return (
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
@@ -82,6 +86,20 @@ export function WorktreeCardDisplayMenuItem({
|
||||
className="w-56"
|
||||
data-workspace-board-preserve-open={boardAttr(preserveWorkspaceBoardOpen)}
|
||||
>
|
||||
{slotLayout ? (
|
||||
<>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={detailsRow}
|
||||
onCheckedChange={(checked) =>
|
||||
void updateSettings({ worktreeCardDetailsRow: checked === true })
|
||||
}
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
{t('sidebar.cardSlot.detailsRow', 'Details row')}
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
) : null}
|
||||
{options.map((opt) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={opt.id}
|
||||
|
||||
@@ -73,3 +73,33 @@ export function useCollapsedLineageSlot(descendantIds: readonly string[]): SlotS
|
||||
)
|
||||
return useAppStore(useShallow(selector))
|
||||
}
|
||||
|
||||
/** The most recent activity among child workspaces, so a parent never looks older than its children. */
|
||||
export function selectLineageLatestAt(
|
||||
state: AppState,
|
||||
descendantIds: readonly string[]
|
||||
): number | null {
|
||||
let at = 0
|
||||
const worktreeMap = getWorktreeMapFromState(state)
|
||||
for (const worktreeId of descendantIds) {
|
||||
at = Math.max(at, worktreeMap.get(worktreeId)?.lastActivityAt ?? 0)
|
||||
for (const entry of selectLiveAgentStatusEntriesForWorktree(state, worktreeId)) {
|
||||
at = Math.max(at, entry.stateStartedAt)
|
||||
}
|
||||
for (const retained of selectRetainedAgentEntriesForWorktree(state, worktreeId)) {
|
||||
at = Math.max(at, retained.entry.stateStartedAt)
|
||||
}
|
||||
}
|
||||
return at > 0 ? at : null
|
||||
}
|
||||
|
||||
export function useLineageLatestAt(descendantIds: readonly string[]): number | null {
|
||||
const descendantKey = descendantIds.join('\n')
|
||||
return useAppStore(
|
||||
useCallback(
|
||||
(state: AppState) =>
|
||||
selectLineageLatestAt(state, descendantKey ? descendantKey.split('\n') : []),
|
||||
[descendantKey]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -136,6 +136,7 @@ export function titleRowSlot(args: {
|
||||
manualUnread: boolean
|
||||
ownLinesShown: boolean
|
||||
lastActivityAt: number | null
|
||||
lineageLatestAt?: number | null
|
||||
}): SlotSignal {
|
||||
if (args.offline) {
|
||||
return { kind: 'offline' }
|
||||
@@ -150,5 +151,10 @@ export function titleRowSlot(args: {
|
||||
if (args.ownLinesShown) {
|
||||
return { kind: 'quiet' }
|
||||
}
|
||||
return { kind: 'time', at: latest(args.hiddenLines) ?? args.lastActivityAt }
|
||||
const ownAt = latest(args.hiddenLines) ?? args.lastActivityAt
|
||||
const lineageAt = args.lineageLatestAt ?? null
|
||||
return {
|
||||
kind: 'time',
|
||||
at: ownAt === null || (lineageAt !== null && lineageAt > ownAt) ? lineageAt : ownAt
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ export function renderWorktreeItemRow(
|
||||
lineageChildCount={itemRow.lineageChildCount}
|
||||
lineageChildWorktreeIds={itemRow.lineageChildWorktreeIds}
|
||||
lineageCollapsed={itemRow.lineageCollapsed}
|
||||
lineageHiddenDescendantIds={itemRow.lineageHiddenDescendantIds}
|
||||
lineageDescendantIds={itemRow.lineageDescendantIds}
|
||||
onLineageToggle={
|
||||
itemRow.lineageGroupKey ? ctx.getLineageToggleHandler(itemRow.lineageGroupKey) : undefined
|
||||
}
|
||||
|
||||
@@ -264,7 +264,8 @@
|
||||
"needYouCount": "{{count}} need you",
|
||||
"needsYou": "Needs you",
|
||||
"failed": "Failed",
|
||||
"lastSeen": "Last seen {{state}}"
|
||||
"lastSeen": "Last seen {{state}}",
|
||||
"detailsRow": "Details row"
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { AntigravityChatAuthSettings } from './antigravity-chat-auth-config
|
||||
import type { EmulatorOpeningSettings } from './emulator-opening-preferences'
|
||||
import type { BrowserExtensionGlobalSettings } from './browser/extensions/browser-extension-settings'
|
||||
import type { ClaudeAgentViewSettings } from './claude-agent-view'
|
||||
import type { WorktreeCardDisplaySettings } from './worktree/card-display-settings-types'
|
||||
import type { BrowserLinkOpeningSettings } from './browser-link-opening-settings'
|
||||
import type { BrowserPasswordsGlobalSettings } from './browser-passwords-types'
|
||||
import type { ClaudeAccountSwitchSettings } from './claude-account-switch-settings'
|
||||
@@ -33,6 +34,7 @@ export type GlobalSettingsBase = AntigravityChatAuthSettings &
|
||||
NativeChatGlobalSettings &
|
||||
NativeChatIdleStopSettings &
|
||||
WorktreeDeletionPreferences &
|
||||
WorktreeCardDisplaySettings &
|
||||
ClaudeAccountSwitchSettings &
|
||||
AgentPermissionGlobalSettings &
|
||||
ExternalConversationSettings &
|
||||
|
||||
@@ -473,8 +473,6 @@ export type GlobalSettings = GlobalSettingsBase & {
|
||||
experimentalNewWorktreeCardStyle?: boolean
|
||||
/** Plugin frames as isolated guests (default on); main-only kill switch, see plugin-frame-isolation.ts. */
|
||||
experimentalPluginIsolatedFrames?: boolean
|
||||
/** Which worktree card the sidebar draws; `experimentalNewWorktreeCardStyle` stays in step for older clients. */
|
||||
worktreeCardStyle?: 'classic' | 'experimental-1' | 'experimental-2'
|
||||
/** Experimental: per-workspace on-demand environment recipes and setup surface. */
|
||||
experimentalEphemeralVms?: boolean
|
||||
/** Experimental (Windows): add WSL distros as hosts, each running its own Orca server. */
|
||||
|
||||
@@ -65,8 +65,8 @@ export type WorktreeRow = {
|
||||
lineageChildWorktreeIds?: readonly string[]
|
||||
lineageGroupKey?: string
|
||||
lineageCollapsed?: boolean
|
||||
/** Set only while collapsed: every workspace folded under this one. */
|
||||
lineageHiddenDescendantIds?: readonly string[]
|
||||
/** Every workspace nested under this one, collapsed or not. */
|
||||
lineageDescendantIds?: readonly string[]
|
||||
hostContextLabel?: string
|
||||
showProjectName?: boolean
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ export function buildNewExternalWorktreesInboxRow(
|
||||
}
|
||||
}
|
||||
|
||||
function hiddenDescendantIds(
|
||||
function descendantIds(
|
||||
occurrences: readonly WorktreeOccurrence<Worktree>[],
|
||||
parent: WorktreeOccurrence<Worktree>
|
||||
): string[] {
|
||||
@@ -142,11 +142,7 @@ export function appendWorktreeRows(
|
||||
? {
|
||||
lineageGroupKey: getWorktreeLineageGroupKey(worktree),
|
||||
lineageCollapsed: options.collapsedGroups.has(getWorktreeLineageGroupKey(worktree)),
|
||||
...(options.collapsedGroups.has(getWorktreeLineageGroupKey(worktree))
|
||||
? {
|
||||
lineageHiddenDescendantIds: hiddenDescendantIds(sectionOccurrences, occurrence)
|
||||
}
|
||||
: {})
|
||||
lineageDescendantIds: descendantIds(sectionOccurrences, occurrence)
|
||||
}
|
||||
: {})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export type WorktreeCardDisplaySettings = {
|
||||
/** Which worktree card the sidebar draws; `experimentalNewWorktreeCardStyle` stays in step for older clients. */
|
||||
worktreeCardStyle?: 'classic' | 'experimental-1' | 'experimental-2'
|
||||
/** 2.0 card: draw the details row (Card display items) under each name; off by default. */
|
||||
worktreeCardDetailsRow?: boolean
|
||||
}
|
||||
Reference in New Issue
Block a user