mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 02:17:31 +00:00
improve perf
This commit is contained in:
@@ -401,7 +401,6 @@
|
||||
1 src/renderer/src/components/settings/browser/extensions/BrowserExtensionsSection.tsx
|
||||
1 src/renderer/src/components/settings/browser/extensions/BrowserExtensionsWebStoreInstall.tsx
|
||||
1 src/renderer/src/components/settings/browser/profiles/BrowserNewProfileDialog.tsx
|
||||
1 src/renderer/src/components/settings/chat/ChatNamingSetting.tsx
|
||||
1 src/renderer/src/components/settings/cli/install/CliSection.tsx
|
||||
3 src/renderer/src/components/settings/cli/install/WslCliRegistration.tsx
|
||||
2 src/renderer/src/components/settings/cli/install/use-cli-registration-actions.ts
|
||||
@@ -509,8 +508,6 @@
|
||||
1 src/renderer/src/components/sidebar/workspace-board/viewport/WorkspaceKanbanLaneCardList.tsx
|
||||
1 src/renderer/src/components/sidebar/workspace-board/viewport/WorkspaceKanbanLaneGrid.tsx
|
||||
3 src/renderer/src/components/sidebar/workspace-board/viewport/use-workspace-kanban-column-resize.ts
|
||||
1 src/renderer/src/components/sidebar/workspace-card/activity/WorktreeCardAgents.tsx
|
||||
1 src/renderer/src/components/sidebar/workspace-card/activity/useWorktreeAgentRows.ts
|
||||
3 src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-compact-agent-row.tsx
|
||||
2 src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-compact-agents.tsx
|
||||
1 src/renderer/src/components/sidebar/workspace-card/identity/AutoRenameFailedDialog.tsx
|
||||
|
||||
@@ -1742,12 +1742,6 @@
|
||||
"auto.components.settings.ExperimentalPane.agentHibernation.toggleLabel": {
|
||||
"ko": "에이전트 최대 절전 전환"
|
||||
},
|
||||
"auto.components.settings.ExperimentalPane.newWorktreeCardStyle.copy": {
|
||||
"ko": "업데이트된 워크트리 카드 레이아웃, 메타데이터 배치, 카드 표시 메뉴 옵션 및 상태 표시를 미리 봅니다."
|
||||
},
|
||||
"auto.components.settings.ExperimentalPane.newWorktreeCardStyle.description": {
|
||||
"ko": "업데이트된 워크트리 카드 레이아웃, 메타데이터 배치, 카드 표시 메뉴 옵션 및 상태 표시를 미리 봅니다."
|
||||
},
|
||||
"auto.components.settings.EphemeralVmsPane.recipes": {
|
||||
"ko": "레시피"
|
||||
},
|
||||
@@ -2585,9 +2579,6 @@
|
||||
"auto.components.settings.experimental.search.agentHibernation.title": {
|
||||
"ko": "에이전트 최대 절전"
|
||||
},
|
||||
"auto.components.settings.experimental.search.newWorktreeCardStyle.description": {
|
||||
"ko": "업데이트된 워크트리 카드 레이아웃, 메타데이터 배치, 카드 표시 메뉴 옵션 및 상태 표시를 미리 봅니다."
|
||||
},
|
||||
"auto.components.settings.general.search.12ecc640a8": {
|
||||
"ko": "mru"
|
||||
},
|
||||
|
||||
@@ -25,7 +25,6 @@ export type RuntimeClientSettings = Pick<
|
||||
| 'defaultLinearTeamSelection'
|
||||
| 'githubProjects'
|
||||
| 'experimentalNewWorktreeCardStyle'
|
||||
| 'worktreeCardStyle'
|
||||
| 'worktreeCardDetailsRow'
|
||||
| 'compactWorktreeCards'
|
||||
| 'minimaxGroupId'
|
||||
@@ -77,7 +76,6 @@ export type RuntimeClientSettingsUpdate = Pick<
|
||||
| 'defaultLinearTeamSelection'
|
||||
| 'githubProjects'
|
||||
| 'experimentalNewWorktreeCardStyle'
|
||||
| 'worktreeCardStyle'
|
||||
| 'worktreeCardDetailsRow'
|
||||
| 'compactWorktreeCards'
|
||||
| 'minimaxGroupId'
|
||||
|
||||
@@ -13,7 +13,6 @@ import { haveSameDisabledTuiAgents } from '../../../../shared/agent-launch/catal
|
||||
import { normalizeSourceControlAiSettings } from '../../../../shared/source-control/ai/settings/source-control-ai-settings'
|
||||
import { normalizeSourceControlGroupOrder } from '../../../../shared/source-control/listing/source-control-group-order'
|
||||
import { SOURCE_CONTROL_LAUNCH_ACTION_IDS } from '../../../../shared/source-control/ai/recipes/source-control-ai-actions'
|
||||
import { getWorktreeCardStyle } from '../../../../shared/worktree/card-style'
|
||||
import type { GlobalSettings } from '../../../../shared/global-settings-types'
|
||||
import { applyNativeChatSessionOptionSettingsMutation } from '../../../../shared/native-chat/input/native-chat-session-option-defaults'
|
||||
import type { NativeChatSessionOptionSettingsMutation } from '../../../../shared/native-chat/input/native-chat-session-options'
|
||||
@@ -92,8 +91,6 @@ export class RuntimeClientSettingsController {
|
||||
: null,
|
||||
githubProjects: settings.githubProjects,
|
||||
experimentalNewWorktreeCardStyle: settings.experimentalNewWorktreeCardStyle === true,
|
||||
// Always present, so a client can tell this host accepts the three-way choice.
|
||||
worktreeCardStyle: getWorktreeCardStyle(settings),
|
||||
worktreeCardDetailsRow: settings.worktreeCardDetailsRow !== false,
|
||||
compactWorktreeCards: settings.compactWorktreeCards === true,
|
||||
minimaxGroupId: settings.minimaxGroupId ?? '',
|
||||
|
||||
@@ -9,7 +9,6 @@ import type { RuntimeStore } from '../../construction/runtime-store-contract'
|
||||
const WORKSPACE_VIEW_SETTINGS: readonly (keyof GlobalSettings)[] = [
|
||||
'compactWorktreeCards',
|
||||
'experimentalNewWorktreeCardStyle',
|
||||
'worktreeCardStyle',
|
||||
'worktreeCardDetailsRow',
|
||||
'showPinnedWorktreesInGroups'
|
||||
]
|
||||
|
||||
@@ -168,7 +168,6 @@ export type RuntimeStore = {
|
||||
defaultLinearTeamSelection?: GlobalSettings['defaultLinearTeamSelection']
|
||||
githubProjects?: GlobalSettings['githubProjects']
|
||||
experimentalNewWorktreeCardStyle?: GlobalSettings['experimentalNewWorktreeCardStyle']
|
||||
worktreeCardStyle?: GlobalSettings['worktreeCardStyle']
|
||||
worktreeCardDetailsRow?: GlobalSettings['worktreeCardDetailsRow']
|
||||
compactWorktreeCards?: GlobalSettings['compactWorktreeCards']
|
||||
minimaxGroupId?: GlobalSettings['minimaxGroupId']
|
||||
|
||||
@@ -174,7 +174,6 @@ function ActivityThreadHoverCardContent({
|
||||
linkedAzureDevOpsPR: review.linkedAzureDevOpsPR,
|
||||
linkedGiteaPR: review.linkedGiteaPR,
|
||||
newCardStyle: foundation.newCardStyle,
|
||||
worktreeCardStyle: foundation.worktreeCardStyle,
|
||||
compactCards: foundation.compactCards,
|
||||
cardDetails: foundation.cardDetails,
|
||||
agentActivityDisplayMode: foundation.agentActivityDisplayMode,
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
import {
|
||||
getWorktreeCardStyle,
|
||||
worktreeCardStyleSettingsUpdate,
|
||||
type WorktreeCardStyle
|
||||
} from '../../../../../shared/worktree/card-style'
|
||||
import type { GlobalSettings } from '../../../../../shared/global-settings-types'
|
||||
import { Label } from '../../ui/label'
|
||||
import { Switch } from '../../ui/switch'
|
||||
@@ -54,7 +49,6 @@ export function ExperimentalPane({
|
||||
getExperimentalSearchEntry().newWorktreeCardStyle
|
||||
])
|
||||
const agentHibernationEnabled = settings.experimentalAgentHibernation === true
|
||||
const worktreeCardStyle = getWorktreeCardStyle(settings)
|
||||
// Use the planner’s clamped millisecond value/default while editing minutes.
|
||||
const agentHibernationIdleMinutes = Math.round(
|
||||
getEffectiveAgentHibernationIdleMs(settings.agentHibernationIdleMs) / MS_PER_MINUTE
|
||||
@@ -239,10 +233,14 @@ export function ExperimentalPane({
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<SettingsSegmentedControl<WorktreeCardStyle>
|
||||
<SettingsSegmentedControl<'classic' | 'experimental'>
|
||||
size="sm"
|
||||
value={worktreeCardStyle}
|
||||
onChange={(style) => updateSettings(worktreeCardStyleSettingsUpdate(style))}
|
||||
value={
|
||||
settings.experimentalNewWorktreeCardStyle === true ? 'experimental' : 'classic'
|
||||
}
|
||||
onChange={(style) =>
|
||||
updateSettings({ experimentalNewWorktreeCardStyle: style === 'experimental' })
|
||||
}
|
||||
ariaLabel={t(
|
||||
'auto.components.settings.ExperimentalPane.worktreeCardStyle.ariaLabel',
|
||||
'Card style'
|
||||
@@ -256,7 +254,7 @@ export function ExperimentalPane({
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'experimental-1',
|
||||
value: 'experimental',
|
||||
label: t(
|
||||
'auto.components.settings.ExperimentalPane.worktreeCardStyle.experimental',
|
||||
'Experimental'
|
||||
|
||||
+2
-2
@@ -110,6 +110,7 @@ export function useGlobalTextActionRecipeDraft({
|
||||
: {}),
|
||||
...(customCommandDirty ? { customAgentCommand: customCommand } : {})
|
||||
}))
|
||||
setIsSaving(false)
|
||||
if (!isMountedRef.current) {
|
||||
return
|
||||
}
|
||||
@@ -121,9 +122,8 @@ export function useGlobalTextActionRecipeDraft({
|
||||
setCustomCommandDraft(hasNewCommandEdits ? latestValue.customCommand : null)
|
||||
onDirtyChangeRef.current?.(hasNewEdits || hasNewCommandEdits)
|
||||
} catch (error) {
|
||||
onSaveError(error)
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
onSaveError(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -238,7 +238,6 @@ function Card({
|
||||
lineageCollapsed={collapsed}
|
||||
lineageDescendantIdentities={descendantIdentities}
|
||||
lineageChildWorktreeIds={descendantIds}
|
||||
lineageChildIdentities={descendantIdentities}
|
||||
lineageChildren={children}
|
||||
lineageChildrenStyle={rows.getLineageChildrenInlineStyle(
|
||||
rows.LINEAGE_CHILDREN_INLINE_OFFSET
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from 'react'
|
||||
import { Activity, ChevronRight, CircleX } from 'lucide-react'
|
||||
import { CircleX } from 'lucide-react'
|
||||
import { AgentQuestionIcon } from '@/components/AgentQuestionIcon'
|
||||
import { AgentWorkingSpinner } from '@/components/AgentWorkingSpinner'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate, type Translate } from '@/i18n/use-translate'
|
||||
import { formatShortTimeAgo } from '@/lib/time-format/short-time-ago'
|
||||
import { cn } from '@/lib/class-names'
|
||||
import type { SlotException, SlotSignal } from './worktree-card-slot-signal'
|
||||
@@ -13,108 +12,59 @@ function formatSlotTime(at: number | null, now: number): string {
|
||||
return at === null ? '' : formatShortTimeAgo(at, now)
|
||||
}
|
||||
|
||||
export function slotLabel(signal: SlotSignal): string | undefined {
|
||||
function slotLabel(signal: SlotSignal, t: Translate): string | undefined {
|
||||
switch (signal.kind) {
|
||||
case 'needs':
|
||||
return translate('sidebar.cardSlot.needsYouLabel', 'Waiting on you')
|
||||
return t('sidebar.cardSlot.needsYouLabel', 'Waiting on you')
|
||||
case 'failed':
|
||||
return translate('sidebar.cardSlot.failedLabel', 'Last turn failed')
|
||||
return t('sidebar.cardSlot.failedLabel', 'Last turn failed')
|
||||
case 'unread':
|
||||
return signal.manual
|
||||
? translate('sidebar.cardSlot.markedUnread', 'Marked unread by you')
|
||||
: translate('sidebar.cardSlot.unread', 'Unread result')
|
||||
? t('sidebar.cardSlot.markedUnread', 'Marked unread by you')
|
||||
: t('sidebar.cardSlot.unread', 'Unread result')
|
||||
case 'working':
|
||||
return signal.monitoring
|
||||
? translate('sidebar.cardSlot.monitoring', 'Monitoring')
|
||||
: translate('sidebar.cardSlot.working', 'Working')
|
||||
? t('sidebar.cardSlot.monitoring', 'Monitoring')
|
||||
: t('sidebar.cardSlot.working', 'Working')
|
||||
case 'offline':
|
||||
return translate('sidebar.cardSlot.offlineLabel', 'Host offline')
|
||||
return t('sidebar.cardSlot.offlineLabel', 'Host offline')
|
||||
case 'time':
|
||||
case 'quiet':
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/** The one status slot at a row's right edge: the first exception, otherwise the time. */
|
||||
/** The title's time, or its host's offline state. */
|
||||
export function WorktreeCardSlot({
|
||||
signal,
|
||||
now,
|
||||
stale = false,
|
||||
timeTone
|
||||
}: {
|
||||
signal: SlotSignal
|
||||
signal: Extract<SlotSignal, { kind: 'time' } | { kind: 'offline' }>
|
||||
now: number
|
||||
/** The host is offline: show what was last seen, never a live state. */
|
||||
stale?: boolean
|
||||
/** A plain time beside an unread or working glyph reads like that state's own time. */
|
||||
timeTone?: 'unread' | 'working'
|
||||
}): React.JSX.Element | null {
|
||||
if (signal.kind === 'quiet') {
|
||||
return null
|
||||
}
|
||||
const base =
|
||||
'inline-flex min-w-[22px] shrink-0 items-center justify-end gap-1 whitespace-nowrap text-3xs leading-none tabular-nums'
|
||||
const label = slotLabel(signal)
|
||||
const slotProps = { 'data-worktree-card-slot': signal.kind, title: label }
|
||||
|
||||
if (stale || signal.kind === 'offline') {
|
||||
return (
|
||||
<span {...slotProps} className={cn(base, 'italic text-muted-foreground/70')}>
|
||||
{signal.kind === 'offline'
|
||||
? translate('sidebar.cardSlot.offline', 'offline')
|
||||
: 'at' in signal
|
||||
? formatSlotTime(signal.at, now)
|
||||
: null}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
switch (signal.kind) {
|
||||
case 'needs':
|
||||
return <WorktreeCardStatusPill signal={signal} />
|
||||
case 'failed':
|
||||
return (
|
||||
<span {...slotProps} className={cn(base, 'font-semibold text-destructive')}>
|
||||
{/* A crossed circle, not a red dot, so failed never reads like the blue unread dot. */}
|
||||
<CircleX className="size-2.5 shrink-0" aria-hidden="true" />
|
||||
{translate('sidebar.cardSlot.failed', 'Failed')}
|
||||
</span>
|
||||
)
|
||||
case 'unread':
|
||||
return (
|
||||
<span {...slotProps} className={cn(base, 'font-medium text-foreground')}>
|
||||
<span aria-hidden="true" className="size-1.5 shrink-0 rounded-full bg-unread" />
|
||||
{formatSlotTime(signal.at, now)}
|
||||
</span>
|
||||
)
|
||||
case 'working':
|
||||
return (
|
||||
<span {...slotProps} className={cn(base, 'text-muted-foreground')}>
|
||||
{signal.monitoring ? (
|
||||
<Activity className="size-2.5 shrink-0 text-agent-working" aria-hidden="true" />
|
||||
) : (
|
||||
<AgentWorkingSpinner className="size-[9px] border-[1.5px]" />
|
||||
)}
|
||||
{formatSlotTime(signal.at, now)}
|
||||
</span>
|
||||
)
|
||||
case 'time':
|
||||
return (
|
||||
<span
|
||||
{...slotProps}
|
||||
className={cn(
|
||||
base,
|
||||
timeTone === 'unread'
|
||||
? 'font-medium text-foreground'
|
||||
: timeTone === 'working'
|
||||
? 'text-muted-foreground'
|
||||
: 'text-muted-foreground/70'
|
||||
)}
|
||||
>
|
||||
{formatSlotTime(signal.at, now)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
return (
|
||||
<span
|
||||
data-worktree-card-slot={signal.kind}
|
||||
title={slotLabel(signal, t)}
|
||||
className={cn(
|
||||
'inline-flex min-w-[22px] shrink-0 items-center justify-end whitespace-nowrap text-3xs leading-none tabular-nums',
|
||||
signal.kind === 'offline'
|
||||
? 'italic text-muted-foreground/70'
|
||||
: timeTone === 'unread'
|
||||
? 'font-medium text-foreground'
|
||||
: timeTone === 'working'
|
||||
? 'text-muted-foreground'
|
||||
: 'text-muted-foreground/70'
|
||||
)}
|
||||
>
|
||||
{signal.kind === 'offline'
|
||||
? t('sidebar.cardSlot.offline', 'offline')
|
||||
: formatSlotTime(signal.at, now)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function titleTimeTone(exceptions: readonly SlotException[]): 'unread' | 'working' | undefined {
|
||||
@@ -127,11 +77,12 @@ function titleTimeTone(exceptions: readonly SlotException[]): 'unread' | 'workin
|
||||
type PillSignal = Extract<SlotSignal, { kind: 'needs' } | { kind: 'failed' }>
|
||||
|
||||
/** The loud statuses get a labelled pill, so a card that needs you is hard to miss. */
|
||||
export function WorktreeCardStatusPill({ signal }: { signal: PillSignal }): React.JSX.Element {
|
||||
function WorktreeCardStatusPill({ signal }: { signal: PillSignal }): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
return (
|
||||
<span
|
||||
data-worktree-card-slot={signal.kind}
|
||||
title={slotLabel(signal)}
|
||||
title={slotLabel(signal, t)}
|
||||
className={cn(
|
||||
'inline-flex h-4 shrink-0 items-center gap-1 whitespace-nowrap rounded-[5px] px-1.5 text-3xs leading-none font-semibold',
|
||||
signal.kind === 'needs'
|
||||
@@ -147,12 +98,12 @@ export function WorktreeCardStatusPill({ signal }: { signal: PillSignal }): Reac
|
||||
<CircleX className="size-2.5 shrink-0" aria-hidden="true" />
|
||||
)}
|
||||
{signal.kind === 'failed'
|
||||
? translate('sidebar.cardSlot.failed', 'Failed')
|
||||
? t('sidebar.cardSlot.failed', 'Failed')
|
||||
: signal.count > 1
|
||||
? translate('sidebar.cardSlot.needYouCount', '{{count}} need you', {
|
||||
? t('sidebar.cardSlot.needYouCount', '{{count}} need you', {
|
||||
count: signal.count
|
||||
})
|
||||
: translate('sidebar.cardSlot.needsYou', 'Needs you')}
|
||||
: t('sidebar.cardSlot.needsYou', 'Needs you')}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -165,6 +116,7 @@ export function WorktreeCardTitlePills({
|
||||
exceptions: readonly SlotException[]
|
||||
resolveTargets: (kind: SlotException['kind']) => SlotTarget[]
|
||||
}): React.JSX.Element | null {
|
||||
const t = useTranslate()
|
||||
const pills = exceptions.filter(
|
||||
(exception): exception is PillSignal =>
|
||||
exception.kind === 'needs' || exception.kind === 'failed'
|
||||
@@ -177,7 +129,7 @@ export function WorktreeCardTitlePills({
|
||||
{pills.map((pill) => (
|
||||
<SlotJumpTrigger
|
||||
key={pill.kind}
|
||||
label={slotLabel(pill) ?? ''}
|
||||
label={slotLabel(pill, t) ?? ''}
|
||||
resolveTargets={() => resolveTargets(pill.kind)}
|
||||
className="-my-0.5 inline-flex min-w-0 items-center"
|
||||
>
|
||||
@@ -188,76 +140,20 @@ export function WorktreeCardTitlePills({
|
||||
)
|
||||
}
|
||||
|
||||
export type TitleTimeToggle = {
|
||||
open: boolean
|
||||
label: string
|
||||
disabled: boolean
|
||||
onPointerDown: React.PointerEventHandler<HTMLButtonElement>
|
||||
onToggle: (event: React.MouseEvent<HTMLButtonElement>) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* The time. On a card that opens it is also the open/close button: hover or keyboard focus
|
||||
* shows the chevron in its place, and a tap on the time works where there is no hover.
|
||||
*/
|
||||
export function WorktreeCardTitleTime({
|
||||
exceptions,
|
||||
at,
|
||||
now,
|
||||
toggle,
|
||||
disclosureOnly = false
|
||||
now
|
||||
}: {
|
||||
exceptions: readonly SlotException[]
|
||||
at: number | null
|
||||
now: number
|
||||
toggle: TitleTimeToggle | null
|
||||
disclosureOnly?: boolean
|
||||
}): React.JSX.Element {
|
||||
const time = (
|
||||
return (
|
||||
<WorktreeCardSlot
|
||||
signal={{ kind: 'time', at }}
|
||||
now={now}
|
||||
timeTone={titleTimeTone(exceptions)}
|
||||
/>
|
||||
)
|
||||
if (toggle === null) {
|
||||
return time
|
||||
}
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="group/title-time -my-0.5 inline-grid h-4 min-w-4 shrink-0 cursor-pointer items-center justify-items-end rounded-sm leading-none text-muted-foreground outline-none hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none"
|
||||
aria-label={toggle.label}
|
||||
aria-expanded={toggle.open}
|
||||
disabled={toggle.disabled}
|
||||
onPointerDown={toggle.onPointerDown}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
toggle.onToggle(event)
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'[grid-area:1/1] inline-flex items-center can-hover:group-hover/worktree-card:invisible group-focus-visible/title-time:invisible',
|
||||
disclosureOnly && 'invisible'
|
||||
)}
|
||||
>
|
||||
{time}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'[grid-area:1/1] inline-flex size-4 items-center justify-center can-hover:group-hover/worktree-card:visible group-focus-visible/title-time:visible',
|
||||
!disclosureOnly && 'invisible'
|
||||
)}
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
'size-3 transition-transform motion-reduce:transition-none',
|
||||
toggle.open && 'rotate-90'
|
||||
)}
|
||||
aria-hidden
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
+7
-7
@@ -6,7 +6,7 @@ import { useAppStore } from '@/store'
|
||||
import { deriveRunningAgentSendTargets } from '@/lib/agent-session/messaging/running-agent-targets'
|
||||
import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/useDashboardData'
|
||||
import { buildAgentRowLineageTree } from '@/components/dashboard/agent-row-lineage-model'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { agentSendDisabledReasonLabel } from '@/lib/agent-session/messaging/agent-send-disabled-reason-label'
|
||||
import { useUiLanguageRevision } from '@/i18n/use-ui-language-revision'
|
||||
import {
|
||||
@@ -40,6 +40,7 @@ export function useWorktreeAgentListModel(
|
||||
agents: DashboardAgentRowData[],
|
||||
executionHostId?: ExecutionHostId
|
||||
) {
|
||||
const t = useTranslate()
|
||||
const languageRevision = useUiLanguageRevision()
|
||||
const { agentActivityDisplayMode, showAgentModels } = useWorktreeAgentDisplay()
|
||||
const dropAgentStatus = useAppStore((s) => s.dropAgentStatus)
|
||||
@@ -73,7 +74,7 @@ export function useWorktreeAgentListModel(
|
||||
const sendTargetsByPaneKey = useMemo(
|
||||
() =>
|
||||
recomputeOn([agentStatusEpoch, languageRevision], () => {
|
||||
if (!isAgentSendTargetModeActive) {
|
||||
if (!agentSendPopoverTargetMode) {
|
||||
return new Map<
|
||||
string,
|
||||
{ status: 'eligible' | 'disabled' | 'sending'; disabledReason?: string }
|
||||
@@ -87,7 +88,7 @@ export function useWorktreeAgentListModel(
|
||||
agentSendPopoverTargetMode.sendingPaneKey === target.paneKey
|
||||
? {
|
||||
status: 'sending' as const,
|
||||
disabledReason: translate(
|
||||
disabledReason: t(
|
||||
'auto.components.sidebar.WorktreeCardAgents.sendingTarget',
|
||||
'Sending...'
|
||||
)
|
||||
@@ -105,9 +106,8 @@ export function useWorktreeAgentListModel(
|
||||
// Freshness timers advance this epoch without replacing the status map.
|
||||
agentStatusEpoch,
|
||||
languageRevision,
|
||||
agentSendPopoverTargetMode?.sendingPaneKey,
|
||||
agentSendPopoverTargetMode?.status,
|
||||
isAgentSendTargetModeActive,
|
||||
t,
|
||||
agentSendPopoverTargetMode,
|
||||
sendTargetInputs,
|
||||
worktreeId
|
||||
]
|
||||
@@ -162,7 +162,7 @@ export function useWorktreeAgentListModel(
|
||||
isAgentSendTargetModeActive
|
||||
? (sendTargetsByPaneKey.get(paneKey) ?? {
|
||||
status: 'disabled' as const,
|
||||
disabledReason: translate(
|
||||
disabledReason: t(
|
||||
'auto.components.sidebar.WorktreeCardAgents.agentUnavailable',
|
||||
'Agent is not available'
|
||||
)
|
||||
|
||||
+8
-24
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useSyncExternalStore } from 'react'
|
||||
|
||||
export type WorktreeAgentExpansionState = {
|
||||
// Child lists start collapsed so spawning subagents cannot grow the card and shift the sidebar.
|
||||
@@ -26,6 +26,7 @@ function trimPersistedExpansions(): void {
|
||||
break
|
||||
}
|
||||
expansionByWorktreeId.delete(oldest)
|
||||
notifyExpansion(oldest)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,34 +99,21 @@ export type WorktreeAgentExpansionControls = {
|
||||
|
||||
/** Restore each worktree’s disclosures across virtualizer and sibling-lineage remounts. */
|
||||
export function useWorktreeAgentExpansionState(worktreeId: string): WorktreeAgentExpansionControls {
|
||||
const [rendered, setRendered] = useState<{
|
||||
worktreeId: string
|
||||
state: WorktreeAgentExpansionState
|
||||
}>(() => ({ worktreeId, state: readExpansionState(worktreeId) }))
|
||||
|
||||
// Memoized bodies can change worktree without remounting; avoid displaying the previous card’s disclosure.
|
||||
const current =
|
||||
rendered.worktreeId === worktreeId ? rendered.state : readExpansionState(worktreeId)
|
||||
const subscribe = useCallback(
|
||||
(listener: () => void) => subscribeExpansion(worktreeId, listener),
|
||||
[worktreeId]
|
||||
)
|
||||
const getSnapshot = useCallback(() => readExpansionState(worktreeId), [worktreeId])
|
||||
const current = useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
|
||||
|
||||
const commit = useCallback(
|
||||
(next: WorktreeAgentExpansionState) => {
|
||||
persistExpansionState(worktreeId, next)
|
||||
setRendered({ worktreeId, state: next })
|
||||
// The agent list and the title's roll-up both read this state; keep every mounted reader in step.
|
||||
notifyExpansion(worktreeId)
|
||||
},
|
||||
[worktreeId]
|
||||
)
|
||||
|
||||
// A mounted card picks up an outside unfold; an unmounted one reads it from the cache on mount.
|
||||
useEffect(
|
||||
() =>
|
||||
subscribeExpansion(worktreeId, () =>
|
||||
setRendered({ worktreeId, state: readExpansionState(worktreeId) })
|
||||
),
|
||||
[worktreeId]
|
||||
)
|
||||
|
||||
const toggleLineageParent = useCallback(
|
||||
(paneKey: string) => {
|
||||
const base = readExpansionState(worktreeId)
|
||||
@@ -164,10 +152,6 @@ export function clearWorktreeAgentExpansionStateForTests(): void {
|
||||
expansionByWorktreeId.clear()
|
||||
}
|
||||
|
||||
export function getWorktreeAgentExpansionCountForTests(): number {
|
||||
return expansionByWorktreeId.size
|
||||
}
|
||||
|
||||
export function seedWorktreeAgentExpansionStateForTests(
|
||||
worktreeId: string,
|
||||
state: WorktreeAgentExpansionState
|
||||
|
||||
+6
-9
@@ -70,8 +70,8 @@ type IdleChildAgentGroupRowProps = {
|
||||
count: number
|
||||
expanded: boolean
|
||||
onToggle: () => void
|
||||
// Full-mode child rows use a wider inset; match it so the dots share one column. Slot = the 2.0 card's lines.
|
||||
layout: 'compact' | 'full' | 'slot'
|
||||
// Full-mode child rows use a wider inset; match it so the dots share one column.
|
||||
layout: 'compact' | 'full'
|
||||
// Sits among treeitem siblings, so it joins the tree at their level.
|
||||
ariaLevel: number
|
||||
}
|
||||
@@ -107,11 +107,8 @@ export function IdleChildAgentGroupRow({
|
||||
draggable={false}
|
||||
className={cn(
|
||||
'worktree-agent-row-hover',
|
||||
layout === 'slot'
|
||||
? 'flex h-[22px] 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',
|
||||
'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' : 'w-full px-1',
|
||||
// Group header is meta, not an agent: use the trail tone so it doesn't read as a row name.
|
||||
'text-muted-foreground/65 tabular-nums focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-worktree-sidebar-ring'
|
||||
)}
|
||||
@@ -154,7 +151,7 @@ type ChildAgentsWithIdleGroupProps = {
|
||||
flat: boolean
|
||||
idleExpanded: boolean
|
||||
onToggleIdle: () => void
|
||||
layout: 'compact' | 'full' | 'slot'
|
||||
layout: 'compact' | 'full'
|
||||
ariaLevel: number
|
||||
renderChild: (childAgent: DashboardAgentRowData, idleGroupExpanded: boolean) => React.ReactNode
|
||||
}
|
||||
@@ -184,7 +181,7 @@ export function ChildAgentsWithIdleGroup({
|
||||
layout={layout}
|
||||
ariaLevel={ariaLevel}
|
||||
/>
|
||||
{layout !== 'full' ? (
|
||||
{layout === 'compact' ? (
|
||||
<CompactAgentExpansion expanded={idleExpanded}>
|
||||
{idleChildren.map((childAgent) => renderChild(childAgent, idleExpanded))}
|
||||
</CompactAgentExpansion>
|
||||
|
||||
-1
@@ -147,7 +147,6 @@ export function useWorktreeCardController(props: ResolvedWorktreeCardProps) {
|
||||
linkedAzureDevOpsPR: review.linkedAzureDevOpsPR,
|
||||
linkedGiteaPR: review.linkedGiteaPR,
|
||||
newCardStyle: foundation.newCardStyle,
|
||||
worktreeCardStyle: foundation.worktreeCardStyle,
|
||||
compactCards: foundation.compactCards,
|
||||
cardDetails: foundation.cardDetails,
|
||||
agentActivityDisplayMode: foundation.agentActivityDisplayMode,
|
||||
|
||||
+1
-7
@@ -2,10 +2,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { DEFAULT_AGENT_ACTIVITY_DISPLAY_MODE } from '../../../../../../shared/constants'
|
||||
import { getWorktreeCardDetails } from '../../../../../../shared/worktree/row-display/card-properties'
|
||||
import {
|
||||
getWorktreeCardStyle,
|
||||
isExperimentalWorktreeCardStyle
|
||||
} from '../../../../../../shared/worktree/card-style'
|
||||
import {
|
||||
isRuntimeOwnedSshTargetId,
|
||||
parseExecutionHostId,
|
||||
@@ -47,8 +43,7 @@ export function useWorktreeCardFoundation({
|
||||
const agentActivityDisplayMode =
|
||||
useAppStore((s) => s.agentActivityDisplayMode) ?? DEFAULT_AGENT_ACTIVITY_DISPLAY_MODE
|
||||
const projectGroups = useAppStore((s) => s.projectGroups)
|
||||
const worktreeCardStyle = getWorktreeCardStyle(settings)
|
||||
const newCardStyle = isExperimentalWorktreeCardStyle(worktreeCardStyle)
|
||||
const newCardStyle = settings?.experimentalNewWorktreeCardStyle === true
|
||||
const compactWorktreeCards = settings?.compactWorktreeCards === true
|
||||
const cardDetails = useMemo(
|
||||
() => getWorktreeCardDetails(cardProps, { compactWorktreeCards, newCardStyle }),
|
||||
@@ -230,7 +225,6 @@ export function useWorktreeCardFoundation({
|
||||
cardProps,
|
||||
agentActivityDisplayMode,
|
||||
projectGroups,
|
||||
worktreeCardStyle,
|
||||
newCardStyle,
|
||||
compactCards,
|
||||
cardDetails,
|
||||
|
||||
@@ -4,7 +4,6 @@ import type React from 'react'
|
||||
|
||||
import type { Repo } from '../../../../../../shared/repo-types'
|
||||
import type { WorkspaceStatus, Worktree } from '../../../../../../shared/worktree/types'
|
||||
import type { HostScopedWorktreeKey } from '../../../../../../shared/worktree/host-qualified-identity'
|
||||
import type { WorktreeCardPrDisplay } from '../review/worktree-card-pr-display'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import type { Translate } from '@/i18n/use-translate'
|
||||
@@ -38,7 +37,6 @@ export type WorktreeCardProps = {
|
||||
flushSurface?: boolean
|
||||
lineageChildCount?: number
|
||||
lineageChildWorktreeIds?: readonly string[]
|
||||
lineageChildIdentities?: readonly HostScopedWorktreeKey[]
|
||||
lineageCollapsed?: boolean
|
||||
/** Workspaces nested under this one; the 2.0 card rolls up their latest time and, when collapsed, their exceptions. */
|
||||
lineageDescendantIdentities?: WorktreeLineageSource
|
||||
|
||||
-1
@@ -14,7 +14,6 @@ export type WorktreeCardSecondaryDetailsInputs = Pick<
|
||||
Pick<
|
||||
Foundation,
|
||||
| 'newCardStyle'
|
||||
| 'worktreeCardStyle'
|
||||
| 'compactCards'
|
||||
| 'cardDetails'
|
||||
| 'agentActivityDisplayMode'
|
||||
|
||||
-18
@@ -137,21 +137,3 @@ export function deriveWorktreeCardPrDisplay(
|
||||
)
|
||||
return { prDisplay, cachedBranchFallbackGitHubPRNumber }
|
||||
}
|
||||
|
||||
/** A worktree's review read straight from the store, for cards that aren't mounted (hidden children). */
|
||||
export function selectWorktreeCardPrDisplay(
|
||||
state: AppState,
|
||||
worktree: Worktree,
|
||||
repo: Repo | undefined
|
||||
): WorktreeCardPrDisplay | null {
|
||||
const { hostedReviewCacheKey, prCacheKey } = getWorktreeReviewCacheKeys(
|
||||
worktree,
|
||||
repo,
|
||||
state.settings
|
||||
)
|
||||
return deriveWorktreeCardPrDisplay(
|
||||
worktree,
|
||||
hostedReviewCacheKey ? state.hostedReviewCache[hostedReviewCacheKey] : undefined,
|
||||
prCacheKey ? state.prCache?.[prCacheKey] : undefined
|
||||
).prDisplay
|
||||
}
|
||||
|
||||
+5
-8
@@ -55,7 +55,7 @@ function getCheckTone(
|
||||
)
|
||||
}
|
||||
|
||||
export function getReviewStateTone(state: WorktreeCardPrDisplay['state']): string {
|
||||
function getStateTone(state: WorktreeCardPrDisplay['state']): string {
|
||||
if (state === 'merged') {
|
||||
return 'text-purple-600/70 dark:text-purple-400/70'
|
||||
}
|
||||
@@ -71,12 +71,6 @@ export function getReviewStateTone(state: WorktreeCardPrDisplay['state']): strin
|
||||
return 'text-muted-foreground opacity-70'
|
||||
}
|
||||
|
||||
export function getReviewTone(
|
||||
review: Pick<WorktreeCardPrDisplay, 'state' | 'status' | 'checksSummary'>
|
||||
): string {
|
||||
return getCheckTone(review) ?? getReviewStateTone(review.state)
|
||||
}
|
||||
|
||||
export function ReviewIcon({
|
||||
review,
|
||||
className,
|
||||
@@ -93,6 +87,9 @@ export function ReviewIcon({
|
||||
variant === 'provider' && review.provider === 'gitlab' ? GitMerge : PullRequestIcon
|
||||
const Icon = getReviewStateIcon(review.state) ?? providerIcon
|
||||
return createElement(Icon, {
|
||||
className: cn(className, showChecks ? getReviewTone(review) : getReviewStateTone(review.state))
|
||||
className: cn(
|
||||
className,
|
||||
(showChecks ? getCheckTone(review) : null) ?? getStateTone(review.state)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ const WorktreeCard = React.memo(function WorktreeCard({
|
||||
flushSurface = false,
|
||||
lineageChildCount = 0,
|
||||
lineageChildWorktreeIds,
|
||||
lineageChildIdentities,
|
||||
lineageCollapsed = false,
|
||||
lineageDescendantIdentities,
|
||||
lineageChildren,
|
||||
@@ -74,7 +73,6 @@ const WorktreeCard = React.memo(function WorktreeCard({
|
||||
flushSurface,
|
||||
lineageChildCount,
|
||||
lineageChildWorktreeIds,
|
||||
lineageChildIdentities,
|
||||
lineageCollapsed,
|
||||
lineageDescendantIdentities,
|
||||
lineageChildren,
|
||||
|
||||
+2
-4
@@ -8,16 +8,14 @@ import {
|
||||
export function getWorktreeCardFrameGeometry({
|
||||
flushSurface,
|
||||
contentIndent,
|
||||
statusLane,
|
||||
minContentInset
|
||||
statusLane
|
||||
}: {
|
||||
flushSurface: boolean
|
||||
contentIndent: number
|
||||
statusLane: boolean
|
||||
minContentInset?: number
|
||||
}): { cardStyle: React.CSSProperties | undefined; parentContentMarginLeft: number } {
|
||||
const cardPaddingLeft = flushSurface
|
||||
? getFlushWorktreeCardPaddingLeft(contentIndent, statusLane, minContentInset)
|
||||
? getFlushWorktreeCardPaddingLeft(contentIndent, statusLane)
|
||||
: contentIndent > 0
|
||||
? `calc(0.125rem + ${contentIndent}px)`
|
||||
: null
|
||||
|
||||
+1
-2
@@ -19,7 +19,6 @@ 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
|
||||
@@ -69,7 +68,7 @@ export function WorktreeCardDisplayMenuItem({
|
||||
const hasProjectGroups = useAppStore((s) => s.projectGroups.length > 0)
|
||||
const toggleProperty = useToggleCardProperty()
|
||||
const options = useMemo(() => getWorktreeCardDisplayOptions(hasProjectGroups), [hasProjectGroups])
|
||||
const experimentalCard = useAppStore((s) => getWorktreeCardStyle(s.settings) !== 'classic')
|
||||
const experimentalCard = useAppStore((s) => s.settings?.experimentalNewWorktreeCardStyle === true)
|
||||
const detailsRow = useAppStore((s) => s.settings?.worktreeCardDetailsRow !== false)
|
||||
const updateSettings = useAppStore((s) => s.updateSettings)
|
||||
return (
|
||||
|
||||
+8
-16
@@ -9,7 +9,6 @@ import { selectAgentRowsUnread } from '../activity/worktree-card-agent-ack-input
|
||||
import { selectSendTargetControlInputs } from '../activity/worktree-card-send-target-inputs'
|
||||
import { useWorktreeAgentExpansionState } from '../activity/worktree-card-agents-expansion-state'
|
||||
import type { WorktreeCardController } from '../controller/use-worktree-card-controller'
|
||||
import { getWorktreeCardHost } from '../../worktree-card-host-name'
|
||||
import {
|
||||
collectLineageSlotTargets,
|
||||
useCollapsedLineageSlots,
|
||||
@@ -41,7 +40,6 @@ type TitleStatus = {
|
||||
/** What the title speaks for: hidden agents, collapsed child workspaces, the workspace's own unread. */
|
||||
exceptions: readonly SlotException[]
|
||||
at: number | null
|
||||
now: number
|
||||
resolveTargets: (kind: SlotException['kind']) => SlotTarget[]
|
||||
}
|
||||
|
||||
@@ -67,9 +65,7 @@ export function WorktreeCardTitleStatusProvider({
|
||||
const { worktree, showInlineAgentList, lineageCollapsed, lineageDescendantIdentities } = card
|
||||
const agents = card.inlineAgentRows
|
||||
const signals = useSlotAgentSignals(agents)
|
||||
const now = useNow(30_000)
|
||||
const host = getWorktreeCardHost(card)
|
||||
const offline = card.isRuntimeDisconnected || (host?.kind === 'ssh' && host.down)
|
||||
const offline = card.isRuntimeDisconnected || card.isSshDisconnected
|
||||
const markedByUser = useAppStore((s) => Boolean(s.workspacesMarkedUnreadByUser[worktree.id]))
|
||||
const pickingSendTarget = useAppStore(
|
||||
(s) => selectSendTargetControlInputs(s, worktree.id).targetMode !== null
|
||||
@@ -78,10 +74,10 @@ export function WorktreeCardTitleStatusProvider({
|
||||
// Compact mode folds two or more agents behind a summary row until it is expanded.
|
||||
const foldedBehindSummary =
|
||||
card.agentActivityDisplayMode === 'compact' &&
|
||||
signals.rootLines.length > 1 &&
|
||||
signals.rootCount > 1 &&
|
||||
!compactRootListExpanded &&
|
||||
!pickingSendTarget
|
||||
const linesShown = showInlineAgentList && signals.rootLines.length > 0 && !foldedBehindSummary
|
||||
const linesShown = showInlineAgentList && signals.rootCount > 0 && !foldedBehindSummary
|
||||
const descendants = lineageDescendantIdentities ?? NO_DESCENDANTS
|
||||
const hiddenDescendants = lineageCollapsed ? descendants : NO_DESCENDANTS
|
||||
const collapsedLineage = useCollapsedLineageSlots(hiddenDescendants)
|
||||
@@ -126,8 +122,8 @@ export function WorktreeCardTitleStatusProvider({
|
||||
const exceptions = slot.kind === 'status' ? slot.exceptions : NO_EXCEPTIONS
|
||||
const at = slot.kind === 'status' ? slot.at : null
|
||||
const value = useMemo<TitleStatus>(
|
||||
() => ({ offline, exceptions, at, now, resolveTargets }),
|
||||
[at, exceptions, now, offline, resolveTargets]
|
||||
() => ({ offline, exceptions, at, resolveTargets }),
|
||||
[at, exceptions, offline, resolveTargets]
|
||||
)
|
||||
return <TitleStatusContext.Provider value={value}>{children}</TitleStatusContext.Provider>
|
||||
}
|
||||
@@ -141,11 +137,12 @@ export function useWorktreeCardRolledUpUnread(): boolean {
|
||||
/** The title row's right edge: a pill per loud status the card speaks for, then the time. */
|
||||
export function WorktreeCardTitleStatus(): React.JSX.Element | null {
|
||||
const status = useContext(TitleStatusContext)
|
||||
const now = useNow(30_000)
|
||||
if (!status) {
|
||||
return null
|
||||
}
|
||||
if (status.offline) {
|
||||
return <WorktreeCardSlot signal={{ kind: 'offline' }} now={status.now} />
|
||||
return <WorktreeCardSlot signal={{ kind: 'offline' }} now={now} />
|
||||
}
|
||||
return (
|
||||
<>
|
||||
@@ -153,12 +150,7 @@ export function WorktreeCardTitleStatus(): React.JSX.Element | null {
|
||||
exceptions={status.exceptions}
|
||||
resolveTargets={status.resolveTargets}
|
||||
/>
|
||||
<WorktreeCardTitleTime
|
||||
exceptions={status.exceptions}
|
||||
at={status.at}
|
||||
now={status.now}
|
||||
toggle={null}
|
||||
/>
|
||||
<WorktreeCardTitleTime exceptions={status.exceptions} at={status.at} now={now} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Server, ServerOff } from 'lucide-react'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { cn } from '@/lib/class-names'
|
||||
import type { WorktreeCardController } from './workspace-card/controller/use-worktree-card-controller'
|
||||
|
||||
export type WorktreeCardHost = {
|
||||
label: string
|
||||
kind: 'ssh' | 'runtime'
|
||||
/** The link to the host is down; agent state on the card is the last seen. */
|
||||
down: boolean
|
||||
}
|
||||
|
||||
/** The computer a remote workspace runs on; local workspaces have none. */
|
||||
export function getWorktreeCardHost(card: WorktreeCardController): WorktreeCardHost | null {
|
||||
const {
|
||||
repo,
|
||||
parsedRepoHost,
|
||||
sshTargetLabel,
|
||||
sshStatus,
|
||||
isRuntimeDisconnected,
|
||||
runtimeHostLabel
|
||||
} = card
|
||||
if (repo?.connectionId) {
|
||||
return {
|
||||
label: sshTargetLabel || repo.displayName,
|
||||
kind: 'ssh',
|
||||
down: sshStatus != null && sshStatus !== 'connected'
|
||||
}
|
||||
}
|
||||
if (parsedRepoHost?.kind === 'runtime') {
|
||||
return {
|
||||
label: runtimeHostLabel ?? translate('sidebar.cardSlot.orcaServer', 'Orca server'),
|
||||
kind: 'runtime',
|
||||
down: isRuntimeDisconnected
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function hostTooltip(host: WorktreeCardHost): string {
|
||||
if (host.kind === 'runtime') {
|
||||
return host.down
|
||||
? translate(
|
||||
'auto.components.sidebar.WorktreeCard.runtimeHostDisconnectedNamed',
|
||||
'{{hostName}} disconnected',
|
||||
{ hostName: host.label }
|
||||
)
|
||||
: translate(
|
||||
'auto.components.sidebar.WorktreeCard.runtimeHostProjectNamed',
|
||||
'Project on {{hostName}}',
|
||||
{
|
||||
hostName: host.label
|
||||
}
|
||||
)
|
||||
}
|
||||
return translate(
|
||||
'auto.components.sidebar.WorktreeCardSshHostControl.connectedName',
|
||||
'Project on SSH host {{value0}}',
|
||||
{ value0: host.label }
|
||||
)
|
||||
}
|
||||
|
||||
/** A server glyph after the title marks a remote card; the machine name is in its tooltip so titles keep their width. */
|
||||
export function WorktreeCardHostName({ host }: { host: WorktreeCardHost }): React.JSX.Element {
|
||||
const Icon = host.down ? ServerOff : Server
|
||||
const label = hostTooltip(host)
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex shrink-0 items-center',
|
||||
host.down ? 'text-destructive' : 'text-muted-foreground/70'
|
||||
)}
|
||||
title={label}
|
||||
aria-label={label}
|
||||
role="img"
|
||||
data-worktree-card-host=""
|
||||
>
|
||||
<Icon className="size-2.5 shrink-0" aria-hidden />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -1,50 +1,31 @@
|
||||
import type { AgentDotState } from '@/components/AgentStateDot'
|
||||
import type { AgentRowState } from '../../../../shared/agent-status/presentation/agent-row-decay-state'
|
||||
import type { AgentChildRowModel } from '../../../../shared/agent-status/presentation/agent-child-row-model'
|
||||
import type { DashboardAgentRow } from '@/components/dashboard/useDashboardData'
|
||||
import { buildAgentRowLineageTree } from '@/components/dashboard/agent-row-lineage-model'
|
||||
import { agentRowDisplayDotState } from '@/lib/agent-status/presentation/agent-row-dot-state'
|
||||
import { resolveRowProgramStatus } from '@/lib/program-status/agent-row-presentation'
|
||||
import {
|
||||
classifySlotLine,
|
||||
compareSlotLines,
|
||||
slotLineAt,
|
||||
type SlotLineSignal
|
||||
} from './worktree-card-slot-signal'
|
||||
import { classifySlotLine, slotLineAt, type SlotLineSignal } from './worktree-card-slot-signal'
|
||||
|
||||
export type SlotAgentLine = {
|
||||
agent: DashboardAgentRow
|
||||
dotState: AgentDotState
|
||||
signal: SlotLineSignal
|
||||
}
|
||||
|
||||
export type SlotAgentSignals = {
|
||||
/** Lead agents, most urgent first by their own and their subagents' state. */
|
||||
rootLines: SlotAgentLine[]
|
||||
childrenByParentPaneKey: ReadonlyMap<string, DashboardAgentRow[]>
|
||||
lineByPaneKey: ReadonlyMap<string, SlotAgentLine>
|
||||
/** The most urgent of an agent and its subagents; a folded lead reports this. */
|
||||
rolledByPaneKey: ReadonlyMap<string, SlotLineSignal>
|
||||
/** Every agent's own signal, subagents included. */
|
||||
rootCount: number
|
||||
lines: SlotAgentLine[]
|
||||
allSignals: SlotLineSignal[]
|
||||
}
|
||||
|
||||
const EXCEPTION_KINDS: ReadonlySet<SlotLineSignal['kind']> = new Set([
|
||||
'needs',
|
||||
'failed',
|
||||
'unread',
|
||||
'working'
|
||||
])
|
||||
|
||||
/** One agent's dot and slot kind, program-status blocks included; visible lines and collapsed rollups both read this. */
|
||||
export function projectSlotAgentLine(
|
||||
function projectSlotAgentLine(
|
||||
agent: {
|
||||
state: AgentRowState
|
||||
entry: DashboardAgentRow['entry']
|
||||
childRow?: Pick<AgentChildRowModel, 'displayState'>
|
||||
},
|
||||
unreadCompletion: boolean
|
||||
): { dotState: AgentDotState; kind: SlotLineSignal['kind'] } {
|
||||
) {
|
||||
const { presentation, dotStateOverride } = resolveRowProgramStatus(agent)
|
||||
const dotState = dotStateOverride ?? agentRowDisplayDotState(agent)
|
||||
return {
|
||||
@@ -62,64 +43,16 @@ export function buildSlotAgentSignals(
|
||||
agents: readonly DashboardAgentRow[],
|
||||
unread: readonly boolean[]
|
||||
): SlotAgentSignals {
|
||||
const lineByPaneKey = new Map<string, SlotAgentLine>()
|
||||
agents.forEach((agent, index) => {
|
||||
const lines = agents.map((agent, index): SlotAgentLine => {
|
||||
const { dotState, kind } = projectSlotAgentLine(agent, unread[index] ?? false)
|
||||
lineByPaneKey.set(agent.paneKey, {
|
||||
return {
|
||||
agent,
|
||||
dotState,
|
||||
signal: { kind, at: slotLineAt(agent, kind), monitoring: dotState === 'monitoring' }
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const { rootRows, childrenByParentPaneKey } = buildAgentRowLineageTree([...agents])
|
||||
const rolledByPaneKey = new Map<string, SlotLineSignal>()
|
||||
const visiting = new Set<string>()
|
||||
const rolled = (paneKey: string): SlotLineSignal | null => {
|
||||
const cached = rolledByPaneKey.get(paneKey)
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
const own = lineByPaneKey.get(paneKey)?.signal ?? null
|
||||
// Lineage metadata may cycle; a pane never counts its own subtree twice.
|
||||
if (!own || visiting.has(paneKey)) {
|
||||
return own
|
||||
}
|
||||
visiting.add(paneKey)
|
||||
let best = own
|
||||
for (const child of childrenByParentPaneKey.get(paneKey) ?? []) {
|
||||
const childSignal = rolled(child.paneKey)
|
||||
if (
|
||||
childSignal &&
|
||||
EXCEPTION_KINDS.has(childSignal.kind) &&
|
||||
// Only a more urgent state takes over; the lead keeps its own time for the same state.
|
||||
compareSlotLines({ kind: childSignal.kind, at: null }, { kind: best.kind, at: null }) < 0
|
||||
) {
|
||||
best = childSignal
|
||||
}
|
||||
}
|
||||
visiting.delete(paneKey)
|
||||
rolledByPaneKey.set(paneKey, best)
|
||||
return best
|
||||
}
|
||||
for (const agent of agents) {
|
||||
rolled(agent.paneKey)
|
||||
}
|
||||
|
||||
const rootLines = rootRows
|
||||
.map((agent) => lineByPaneKey.get(agent.paneKey))
|
||||
.filter((line): line is SlotAgentLine => line !== undefined)
|
||||
.sort((a, b) =>
|
||||
compareSlotLines(
|
||||
rolledByPaneKey.get(a.agent.paneKey) ?? a.signal,
|
||||
rolledByPaneKey.get(b.agent.paneKey) ?? b.signal
|
||||
)
|
||||
)
|
||||
return {
|
||||
rootLines,
|
||||
childrenByParentPaneKey,
|
||||
lineByPaneKey,
|
||||
rolledByPaneKey,
|
||||
allSignals: [...lineByPaneKey.values()].map((line) => line.signal)
|
||||
rootCount: buildAgentRowLineageTree([...agents]).rootRows.length,
|
||||
lines,
|
||||
allSignals: lines.map((line) => line.signal)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import type {
|
||||
import type { Tab } from '../../../../shared/tab-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'
|
||||
import { getWorktreeReviewCacheKeys } from './workspace-card/review/worktree-card-pr-derivation'
|
||||
|
||||
@@ -191,7 +190,7 @@ export function seedWorkspaces(seeds: readonly WorkspaceSeed[], now: number): vo
|
||||
settings: state.settings
|
||||
? {
|
||||
...state.settings,
|
||||
...worktreeCardStyleSettingsUpdate('experimental-1')
|
||||
experimentalNewWorktreeCardStyle: true
|
||||
}
|
||||
: state.settings,
|
||||
worktreeCardProperties: ['status', 'inline-agents'],
|
||||
|
||||
@@ -79,10 +79,10 @@ function SlotTargetMenuItem({
|
||||
target: SlotTarget
|
||||
now: number
|
||||
}): React.JSX.Element {
|
||||
// The menu is transient, so a snapshot of the name is enough.
|
||||
const workspaceName =
|
||||
useAppStore.getState().getKnownWorktreeById(target.worktreeId, target.executionHostId)
|
||||
?.displayName ?? ''
|
||||
const workspaceName = useAppStore(
|
||||
(state) =>
|
||||
state.getKnownWorktreeById(target.worktreeId, target.executionHostId)?.displayName ?? ''
|
||||
)
|
||||
return (
|
||||
<DropdownMenuItem onSelect={() => openSlotTarget(target)}>
|
||||
<div className="grid w-full grid-cols-[10px_12px_minmax(0,1fr)_auto] gap-x-1.5 gap-y-0.5 text-xs">
|
||||
|
||||
@@ -16,10 +16,15 @@ import type { AgentStatusEntry } from '../../../../shared/agent-status/contracts
|
||||
import { composeWorktreeHostIdentity } from '../../../../shared/worktree/host-qualified-identity'
|
||||
import {
|
||||
collectLineageSlotTargets,
|
||||
selectCollapsedLineageSlot,
|
||||
selectCollapsedLineageSlots,
|
||||
selectLineageLatestAt
|
||||
} from './worktree-card-slot-rollup'
|
||||
import { slotTargetsOfKind, workspaceUnreadTarget } from './worktree-card-slot-targets'
|
||||
import { fromLineageSlotRecord } from './worktree-card-slot-summary'
|
||||
|
||||
function collapsedSlot(...args: Parameters<typeof selectCollapsedLineageSlots>) {
|
||||
return fromLineageSlotRecord(selectCollapsedLineageSlots(...args))[0] ?? null
|
||||
}
|
||||
|
||||
const original = useAppStore.getState()
|
||||
const LEAF = '11111111-1111-4111-8111-111111111111'
|
||||
@@ -93,7 +98,7 @@ it('includes migration blocks, terminal-derived agents and blocked subagents in
|
||||
})
|
||||
const state = useAppStore.getState()
|
||||
const targets = collectLineageSlotTargets(state, [identity])
|
||||
expect(selectCollapsedLineageSlot(state, [identity])).toEqual({ kind: 'needs', count: 2 })
|
||||
expect(collapsedSlot(state, [identity])).toEqual({ kind: 'needs', count: 2 })
|
||||
expect(
|
||||
targets
|
||||
.filter((target) => target.kind === 'needs')
|
||||
@@ -131,10 +136,10 @@ it('expires hidden working evidence on the same freshness tick as agent rows', (
|
||||
tabsByWorktree: { child: [makeTab({ id: 'parent', worktreeId: 'child', title: '' })] },
|
||||
agentStatusByPaneKey: { [pane('parent')]: entry('parent') }
|
||||
})
|
||||
expect(selectCollapsedLineageSlot(useAppStore.getState(), [identity])?.kind).toBe('working')
|
||||
expect(collapsedSlot(useAppStore.getState(), [identity])?.kind).toBe('working')
|
||||
vi.setSystemTime(20_000_000)
|
||||
useAppStore.setState({ agentStatusEpoch: 2 })
|
||||
expect(selectCollapsedLineageSlot(useAppStore.getState(), [identity])).toBeNull()
|
||||
expect(collapsedSlot(useAppStore.getState(), [identity])).toBeNull()
|
||||
})
|
||||
|
||||
it('updates hidden subtree attention after status changes, acknowledgement, and freshness expiry', () => {
|
||||
@@ -175,14 +180,14 @@ it('updates hidden subtree attention after status changes, acknowledgement, and
|
||||
agentStatusByPaneKey: { [pane('agent')]: entry('agent', { worktreeId: 'grandchild' }) }
|
||||
})
|
||||
expect(selectLineageLatestAt(useAppStore.getState(), source)).toBe(1_950_000)
|
||||
expect(selectCollapsedLineageSlot(useAppStore.getState(), source)?.kind).toBe('working')
|
||||
expect(collapsedSlot(useAppStore.getState(), source)?.kind).toBe('working')
|
||||
useAppStore.setState({
|
||||
agentStatusByPaneKey: {
|
||||
[pane('agent')]: entry('agent', { worktreeId: 'grandchild', state: 'blocked' })
|
||||
},
|
||||
agentStatusEpoch: 2
|
||||
})
|
||||
expect(selectCollapsedLineageSlot(useAppStore.getState(), source)).toEqual({
|
||||
expect(collapsedSlot(useAppStore.getState(), source)).toEqual({
|
||||
kind: 'needs',
|
||||
count: 1
|
||||
})
|
||||
@@ -192,21 +197,21 @@ it('updates hidden subtree attention after status changes, acknowledgement, and
|
||||
},
|
||||
agentStatusEpoch: 3
|
||||
})
|
||||
expect(selectCollapsedLineageSlot(useAppStore.getState(), source)).toEqual({
|
||||
expect(collapsedSlot(useAppStore.getState(), source)).toEqual({
|
||||
kind: 'unread',
|
||||
at: 1_900_000,
|
||||
manual: false
|
||||
})
|
||||
useAppStore.setState({ acknowledgedAgentsByPaneKey: { [pane('agent')]: 1_900_000 } })
|
||||
expect(selectCollapsedLineageSlot(useAppStore.getState(), source)).toBeNull()
|
||||
expect(collapsedSlot(useAppStore.getState(), source)).toBeNull()
|
||||
useAppStore.setState({
|
||||
agentStatusByPaneKey: { [pane('agent')]: entry('agent', { worktreeId: 'grandchild' }) },
|
||||
agentStatusEpoch: 4
|
||||
})
|
||||
expect(selectCollapsedLineageSlot(useAppStore.getState(), source)?.kind).toBe('working')
|
||||
expect(collapsedSlot(useAppStore.getState(), source)?.kind).toBe('working')
|
||||
vi.setSystemTime(20_000_000)
|
||||
useAppStore.setState({ agentStatusEpoch: 5 })
|
||||
expect(selectCollapsedLineageSlot(useAppStore.getState(), source)).toBeNull()
|
||||
expect(collapsedSlot(useAppStore.getState(), source)).toBeNull()
|
||||
})
|
||||
|
||||
it('refreshes unstamped structured agent summaries when workspace ownership changes', () => {
|
||||
@@ -227,14 +232,14 @@ it('refreshes unstamped structured agent summaries when workspace ownership chan
|
||||
runtimeEnvironments: [publicRuntimeEnvironment({ id: 'box' })],
|
||||
agentStatusByPaneKey: { [paneKey]: entry('legacy-native', { paneKey, state: 'blocked' }) }
|
||||
})
|
||||
expect(selectCollapsedLineageSlot(useAppStore.getState(), [identity])).toEqual({
|
||||
expect(collapsedSlot(useAppStore.getState(), [identity])).toEqual({
|
||||
kind: 'needs',
|
||||
count: 1
|
||||
})
|
||||
expect(selectCollapsedLineageSlot(useAppStore.getState(), [remoteIdentity])).toBeNull()
|
||||
expect(collapsedSlot(useAppStore.getState(), [remoteIdentity])).toBeNull()
|
||||
useAppStore.setState({ repos: [{ ...TEST_REPO, executionHostId: 'runtime:box' }] })
|
||||
expect(selectCollapsedLineageSlot(useAppStore.getState(), [identity])).toBeNull()
|
||||
expect(selectCollapsedLineageSlot(useAppStore.getState(), [remoteIdentity])).toEqual({
|
||||
expect(collapsedSlot(useAppStore.getState(), [identity])).toBeNull()
|
||||
expect(collapsedSlot(useAppStore.getState(), [remoteIdentity])).toEqual({
|
||||
kind: 'needs',
|
||||
count: 1
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ import { selectWorktreesByRepo } from '@/store/workspaces/catalog/workspace-reco
|
||||
import {
|
||||
toLineageSlotRecord,
|
||||
fromLineageSlotRecord,
|
||||
mergeRecord
|
||||
type LineageSlotRecord
|
||||
} from './worktree-card-slot-summary'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
@@ -29,21 +29,14 @@ import {
|
||||
} from './workspace-card/activity/worktree-agent-rows-projection'
|
||||
import { selectAgentRowsUnread } from './workspace-card/activity/worktree-card-agent-ack-inputs'
|
||||
import { buildSlotAgentSignals } from './worktree-card-slot-agent-signals'
|
||||
import { slotExceptionsFor, type SlotException } from './worktree-card-slot-signal'
|
||||
import {
|
||||
slotExceptionFor,
|
||||
slotExceptionsFor,
|
||||
type SlotException,
|
||||
type SlotSignal
|
||||
} from './worktree-card-slot-signal'
|
||||
import {
|
||||
expandWorkspaceUnreadTargets,
|
||||
slotTargetsFromAgentLines,
|
||||
slotTargetsOfKind,
|
||||
workspaceUnreadTarget,
|
||||
type SlotTarget
|
||||
} from './worktree-card-slot-targets'
|
||||
|
||||
export function worktreeForIdentity(
|
||||
function worktreeForIdentity(
|
||||
state: AppState,
|
||||
identity: HostScopedWorktreeKey
|
||||
): Worktree | undefined {
|
||||
@@ -155,31 +148,30 @@ export function collectLineageSlotTargets(
|
||||
return source.flatMap((identity) => ownSlot(state, identity).targets)
|
||||
}
|
||||
|
||||
export function selectCollapsedLineageSlot(
|
||||
export function selectCollapsedLineageSlots(
|
||||
state: AppState,
|
||||
source: WorktreeLineageSource
|
||||
): SlotSignal | null {
|
||||
): LineageSlotRecord {
|
||||
return isLineageRange(source)
|
||||
? (fromLineageSlotRecord(descendantSlot(state, source.tree, source.index).record)[0] ?? null)
|
||||
: slotExceptionFor(collectLineageSlotTargets(state, source))
|
||||
? descendantSlot(state, source.tree, source.index).record
|
||||
: toLineageSlotRecord(slotExceptionsFor(collectLineageSlotTargets(state, source)))
|
||||
}
|
||||
|
||||
function createCollapsedLineageSlotsSelector(source: WorktreeLineageSource) {
|
||||
let inputs: readonly unknown[] = []
|
||||
let record = toLineageSlotRecord([])
|
||||
return (state: AppState) => {
|
||||
const next = worktreeAgentProjectionInputs(state)
|
||||
if (!next.every((value, index) => value === inputs[index])) {
|
||||
inputs = next
|
||||
record = selectCollapsedLineageSlots(state, source)
|
||||
}
|
||||
return record
|
||||
}
|
||||
}
|
||||
|
||||
export function useCollapsedLineageSlots(source: WorktreeLineageSource): SlotException[] {
|
||||
const select = useMemo(() => {
|
||||
let inputs: readonly unknown[] = []
|
||||
let record = toLineageSlotRecord([])
|
||||
return (state: AppState) => {
|
||||
const next = worktreeAgentProjectionInputs(state)
|
||||
if (next.every((value, index) => value === inputs[index])) {
|
||||
return record
|
||||
}
|
||||
inputs = next
|
||||
record = isLineageRange(source)
|
||||
? descendantSlot(state, source.tree, source.index).record
|
||||
: toLineageSlotRecord(slotExceptionsFor(collectLineageSlotTargets(state, source)))
|
||||
return record
|
||||
}
|
||||
}, [source])
|
||||
const select = useMemo(() => createCollapsedLineageSlotsSelector(source), [source])
|
||||
const record = useAppStore(useShallow(select))
|
||||
return useMemo(() => fromLineageSlotRecord(record), [record])
|
||||
}
|
||||
@@ -203,93 +195,3 @@ export function useLineageLatestAt(source: WorktreeLineageSource): number | null
|
||||
useCallback((state: AppState) => selectLineageLatestAt(state, source), [source])
|
||||
)
|
||||
}
|
||||
|
||||
function childRange(source: WorktreeLineageSource | undefined, identity: HostScopedWorktreeKey) {
|
||||
if (!source || !isLineageRange(source)) {
|
||||
return null
|
||||
}
|
||||
const index = source.tree.indexByIdentity.get(identity)
|
||||
return index === undefined
|
||||
? null
|
||||
: { tree: source.tree, index, length: source.tree.ends[index]! - index - 1 }
|
||||
}
|
||||
|
||||
function childSlot(
|
||||
state: AppState,
|
||||
identity: HostScopedWorktreeKey,
|
||||
source?: WorktreeLineageSource
|
||||
) {
|
||||
const range = childRange(source, identity)
|
||||
if (!range) {
|
||||
return slotExceptionFor(ownSlot(state, identity).targets)
|
||||
}
|
||||
const slots = slotsForTree(state, range.tree)
|
||||
const record = { ...slots.own[range.index]!.record }
|
||||
mergeRecord(record, descendantSlot(state, range.tree, range.index).record)
|
||||
return fromLineageSlotRecord(record)[0] ?? null
|
||||
}
|
||||
|
||||
export type LineageChildCounts = { needs: number; failed: number; unread: number; working: number }
|
||||
export function selectLineageChildCounts(
|
||||
state: AppState,
|
||||
children: readonly HostScopedWorktreeKey[],
|
||||
source?: WorktreeLineageSource
|
||||
): LineageChildCounts {
|
||||
const counts: LineageChildCounts = { needs: 0, failed: 0, unread: 0, working: 0 }
|
||||
for (const child of children) {
|
||||
const signal = childSlot(state, child, source)
|
||||
if (
|
||||
signal?.kind === 'needs' ||
|
||||
signal?.kind === 'failed' ||
|
||||
signal?.kind === 'unread' ||
|
||||
signal?.kind === 'working'
|
||||
) {
|
||||
counts[signal.kind]++
|
||||
}
|
||||
}
|
||||
return counts
|
||||
}
|
||||
|
||||
export function useLineageChildCounts(
|
||||
children: readonly HostScopedWorktreeKey[],
|
||||
source?: WorktreeLineageSource
|
||||
): LineageChildCounts {
|
||||
const select = useMemo(() => {
|
||||
let inputs: readonly unknown[] = []
|
||||
let counts: LineageChildCounts = { needs: 0, failed: 0, unread: 0, working: 0 }
|
||||
return (state: AppState) => {
|
||||
const next = worktreeAgentProjectionInputs(state)
|
||||
if (next.every((value, index) => value === inputs[index])) {
|
||||
return counts
|
||||
}
|
||||
inputs = next
|
||||
counts = selectLineageChildCounts(state, children, source)
|
||||
return counts
|
||||
}
|
||||
}, [children, source])
|
||||
return useAppStore(useShallow(select))
|
||||
}
|
||||
|
||||
export function collectLineageChildTargetsOfKind(
|
||||
state: AppState,
|
||||
children: readonly HostScopedWorktreeKey[],
|
||||
kind: keyof LineageChildCounts,
|
||||
source?: WorktreeLineageSource
|
||||
): SlotTarget[] {
|
||||
return slotTargetsOfKind(
|
||||
expandWorkspaceUnreadTargets(
|
||||
state,
|
||||
children.flatMap((child) => {
|
||||
if (childSlot(state, child, source)?.kind !== kind) {
|
||||
return []
|
||||
}
|
||||
const range = childRange(source, child)
|
||||
return [
|
||||
...ownSlot(state, child).targets,
|
||||
...(range ? collectLineageSlotTargets(state, range) : [])
|
||||
]
|
||||
})
|
||||
),
|
||||
kind
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { AgentDotState } from '@/components/AgentStateDot'
|
||||
import type { DashboardAgentRow } from '@/components/dashboard/useDashboardData'
|
||||
import { lastEnteredDoneAt } from '@/components/dashboard/agent-finished-timestamp'
|
||||
|
||||
/** What one agent line means for the Experimental 2.0 card, in slot priority order. */
|
||||
/** What one agent line means for the experimental card, in slot priority order. */
|
||||
export type SlotLineKind = 'needs' | 'failed' | 'unread' | 'working' | 'seen' | 'idle'
|
||||
|
||||
/** `monitoring`: working, but watching rather than taking turns; drawn with its own glyph. */
|
||||
@@ -18,15 +18,6 @@ export type SlotSignal =
|
||||
| { kind: 'time'; at: number | null }
|
||||
| { kind: 'quiet' }
|
||||
|
||||
const LINE_KIND_RANK: Record<SlotLineKind, number> = {
|
||||
needs: 0,
|
||||
failed: 1,
|
||||
unread: 2,
|
||||
working: 3,
|
||||
seen: 4,
|
||||
idle: 5
|
||||
}
|
||||
|
||||
export function classifySlotLine(args: {
|
||||
dotState: AgentDotState
|
||||
needsAttention: boolean
|
||||
@@ -65,10 +56,6 @@ export function slotLineAt(
|
||||
return lastEnteredDoneAt(agent) ?? stateStartedAt
|
||||
}
|
||||
|
||||
export function compareSlotLines(a: SlotLineSignal, b: SlotLineSignal): number {
|
||||
return LINE_KIND_RANK[a.kind] - LINE_KIND_RANK[b.kind] || (b.at ?? 0) - (a.at ?? 0)
|
||||
}
|
||||
|
||||
function latest(lines: readonly SlotLineSignal[]): number | null {
|
||||
let at: number | null = null
|
||||
for (const line of lines) {
|
||||
@@ -116,15 +103,6 @@ export function slotExceptionsFor(lines: readonly SlotLineSignal[]): SlotExcepti
|
||||
return exceptions
|
||||
}
|
||||
|
||||
/** The most urgent exception a set of lines reports, or null when every line is quiet. */
|
||||
export function slotExceptionFor(lines: readonly SlotLineSignal[]): SlotSignal | null {
|
||||
return slotExceptionsFor(lines)[0] ?? null
|
||||
}
|
||||
|
||||
export function slotForLine(line: SlotLineSignal): SlotSignal {
|
||||
return slotExceptionFor([line]) ?? { kind: 'time', at: line.at }
|
||||
}
|
||||
|
||||
function mergeSameKind(a: SlotException, b: SlotException): SlotException {
|
||||
if (a.kind === 'needs' && b.kind === 'needs') {
|
||||
return { kind: 'needs', count: a.count + b.count }
|
||||
|
||||
@@ -19,7 +19,7 @@ import { activateWorktreeAgentPane } from './workspace-card/activity/use-worktre
|
||||
import { activateRetainedAgentRow } from './workspace-card/activity/activate-retained-agent-row'
|
||||
import { expandWorktreeAgentList } from './workspace-card/activity/worktree-card-agents-expansion-state'
|
||||
import type { SlotAgentSignals } from './worktree-card-slot-agent-signals'
|
||||
import type { SlotLineKind, SlotLineSignal, SlotSignal } from './worktree-card-slot-signal'
|
||||
import type { SlotLineKind, SlotLineSignal } from './worktree-card-slot-signal'
|
||||
|
||||
/** The agent (or unread workspace) behind a rolled-up slot signal, so clicking the slot can open it. */
|
||||
export type SlotTarget = SlotLineSignal & {
|
||||
@@ -44,7 +44,7 @@ export function slotTargetsFromAgentLines(
|
||||
executionHostId?: ExecutionHostId
|
||||
): SlotTarget[] {
|
||||
const targets: SlotTarget[] = []
|
||||
for (const { agent, signal } of signals.lineByPaneKey.values()) {
|
||||
for (const { agent, signal } of signals.lines) {
|
||||
if (!TARGET_KINDS.has(signal.kind)) {
|
||||
continue
|
||||
}
|
||||
@@ -184,18 +184,3 @@ function activateUnreadTab(
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** The line kind a slot points at, or null when the slot is a time, quiet, or offline. */
|
||||
export function slotTargetKind(signal: SlotSignal): SlotLineKind | null {
|
||||
switch (signal.kind) {
|
||||
case 'needs':
|
||||
case 'failed':
|
||||
case 'unread':
|
||||
case 'working':
|
||||
return signal.kind
|
||||
case 'offline':
|
||||
case 'quiet':
|
||||
case 'time':
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
+1
-7
@@ -1,7 +1,3 @@
|
||||
import {
|
||||
getWorktreeCardStyle,
|
||||
isExperimentalWorktreeCardStyle
|
||||
} from '../../../../../../shared/worktree/card-style'
|
||||
import { Fragment, useMemo, type ComponentType, type JSX } from 'react'
|
||||
import { useAppStore } from '@/store'
|
||||
import {
|
||||
@@ -272,9 +268,7 @@ export function WorkspaceOptionsMenuItems({
|
||||
const groupBy = useAppStore((s) => s.groupBy)
|
||||
const showSleepingWorkspaces = useAppStore((s) => s.showSleepingWorkspaces)
|
||||
const hideWorkspacesFromOtherDevices = useAppStore((s) => s.hideWorkspacesFromOtherDevices)
|
||||
const newCardStyle = useAppStore((s) =>
|
||||
isExperimentalWorktreeCardStyle(getWorktreeCardStyle(s.settings))
|
||||
)
|
||||
const newCardStyle = useAppStore((s) => s.settings?.experimentalNewWorktreeCardStyle === true)
|
||||
const runtimeEnvironmentCount = useAppStore((s) => s.runtimeEnvironments.length)
|
||||
const runtimeEnvironmentCatalogHydrated = useAppStore((s) => s.runtimeEnvironmentCatalogHydrated)
|
||||
const { hostOptions } = useSidebarHostScopeOptions()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { collectRenderedSidebarRowKeys } from './rendered-row-keys'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useAppStore } from '@/store/index'
|
||||
@@ -162,11 +161,6 @@ const WorktreeList = React.memo(function WorktreeList({
|
||||
prCache,
|
||||
agentSendTargetWorktreeId
|
||||
})
|
||||
const disclosedSectionRows = rowModel.sectionRows
|
||||
const disclosedRowKeys = useMemo(
|
||||
() => collectRenderedSidebarRowKeys(disclosedSectionRows),
|
||||
[disclosedSectionRows]
|
||||
)
|
||||
const visibleProjectRepoIdsBySection = useMemo(
|
||||
() => new Map(rowModel.occurrencePlan.sections.map(({ key, group }) => [key, group.repoIds])),
|
||||
[rowModel.occurrencePlan]
|
||||
@@ -200,7 +194,7 @@ const WorktreeList = React.memo(function WorktreeList({
|
||||
)
|
||||
}, [allProjectsCollapsed, projectCollapseKeys, setCollapsedGroups])
|
||||
const selection = useSidebarWorktreeSelection({
|
||||
sectionRows: disclosedSectionRows,
|
||||
sectionRows: rowModel.sectionRows,
|
||||
pinnedDisplayPolicy
|
||||
})
|
||||
const statusMutations = useWorktreeStatusMutations({
|
||||
@@ -253,7 +247,7 @@ const WorktreeList = React.memo(function WorktreeList({
|
||||
|
||||
useSidebarRevealRequests({
|
||||
groupBy,
|
||||
renderedSidebarRowKeys: disclosedRowKeys,
|
||||
renderedSidebarRowKeys: rowModel.renderedSidebarRowKeys,
|
||||
visibleWorktrees,
|
||||
visibleFolderWorkspaces: visibleScope.visibleFolderWorkspacesForRows,
|
||||
currentSidebarWorktreeId,
|
||||
@@ -326,7 +320,7 @@ const WorktreeList = React.memo(function WorktreeList({
|
||||
<VirtualizedWorktreeViewport
|
||||
// Wake moves status headers; preserve viewport identity across those changes.
|
||||
key={`group:${groupBy}:host:${filterState.visibleWorkspaceHostIds?.join(',') ?? 'all'}:lineage`}
|
||||
rows={disclosedSectionRows}
|
||||
rows={rowModel.sectionRows}
|
||||
hostOptions={rowModel.hostOptions}
|
||||
occurrencePlan={rowModel.occurrencePlan}
|
||||
visibleProjectRepoIdsBySection={visibleProjectRepoIdsBySection}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import {
|
||||
getWorktreeCardStyle,
|
||||
isExperimentalWorktreeCardStyle
|
||||
} from '../../../../../../shared/worktree/card-style'
|
||||
import type { AppState } from '@/store/types'
|
||||
import type { WorktreeCardProperty } from '../../../../../../shared/ui-chrome-types'
|
||||
import type { WorktreeGroupBy } from '../../../../../../shared/sidebar-list/row-types'
|
||||
@@ -30,7 +26,7 @@ export function selectWorktreeListReviewCacheInputs(
|
||||
cardProperties: readonly WorktreeCardProperty[]
|
||||
): WorktreeListReviewCacheInputs {
|
||||
const hasFolderWorkspaces = selectFolderWorkspaces(state).length > 0
|
||||
const newCardStyle = isExperimentalWorktreeCardStyle(getWorktreeCardStyle(state.settings))
|
||||
const newCardStyle = state.settings?.experimentalNewWorktreeCardStyle === true
|
||||
const folderCardsNeedReview =
|
||||
hasFolderWorkspaces &&
|
||||
(newCardStyle ? cardProperties.includes('status') : cardProperties.includes('pr'))
|
||||
|
||||
@@ -171,7 +171,6 @@ export function renderFolderWorkspaceOption(args: {
|
||||
hostContextLabel={row.hostContextLabel}
|
||||
lineageChildCount={row.lineageChildCount}
|
||||
lineageChildWorktreeIds={row.lineageChildWorktreeIds}
|
||||
lineageChildIdentities={row.lineageChildIdentities}
|
||||
lineageDescendantIdentities={row.lineageDescendantIdentities}
|
||||
lineageCollapsed={row.lineageCollapsed}
|
||||
lineageChildren={lineage?.children}
|
||||
|
||||
@@ -7,8 +7,6 @@ export const FLUSH_CARD_CONTENT_PULLBACK = 4
|
||||
export const NEW_CARD_STYLE_STATUS_LANE_EXTRA_PULLBACK = 8
|
||||
// Why: keeps a zero-indent flush card off the sidebar edge.
|
||||
export const FLUSH_CARD_MIN_CONTENT_INSET = 2
|
||||
// Why: 2.0 nested cards have no indent, so their content needs its own breathing room off the card edge.
|
||||
export const SLOT_CARD_MIN_CONTENT_INSET = 6
|
||||
export const WORKTREE_CARD_SURFACE_MARGIN = 4
|
||||
// Why: level-1 lineage keeps the grouped-card anchor; deeper levels advance evenly, not cumulatively.
|
||||
export const LINEAGE_IMMEDIATE_PARENT_STEP =
|
||||
@@ -16,8 +14,6 @@ export const LINEAGE_IMMEDIATE_PARENT_STEP =
|
||||
export const LINEAGE_NESTED_ROW_SURFACE_INSET = 0
|
||||
export const LINEAGE_CHILDREN_INLINE_OFFSET =
|
||||
LINEAGE_IMMEDIATE_PARENT_STEP - FLUSH_CARD_MIN_CONTENT_INSET
|
||||
// 2.0 children start on the parent's title column after its leading icon and gap.
|
||||
export const SLOT_LINEAGE_TITLE_COLUMN_SHIFT = 7
|
||||
// Why: header icon+gap (22) vs. card margin+border+legacy lane+gap (23) minus pullback lands the title on the header label.
|
||||
const GROUPED_CARD_TITLE_HEADER_LABEL_OFFSET = 3
|
||||
export const PROJECT_GROUP_HEADER_BASE_PADDING = 10
|
||||
@@ -136,15 +132,14 @@ export function getWorktreeCardSurfaceInset(args: {
|
||||
|
||||
export function getFlushWorktreeCardPaddingLeft(
|
||||
contentIndent: number,
|
||||
applyNewCardStyleStatusLaneOffset = false,
|
||||
minContentInset = FLUSH_CARD_MIN_CONTENT_INSET
|
||||
applyNewCardStyleStatusLaneOffset = false
|
||||
): string {
|
||||
const pullback =
|
||||
FLUSH_CARD_CONTENT_PULLBACK +
|
||||
(applyNewCardStyleStatusLaneOffset ? NEW_CARD_STYLE_STATUS_LANE_EXTRA_PULLBACK : 0)
|
||||
return contentIndent > 0
|
||||
? `max(${minContentInset}px, calc(${contentIndent}px - ${pullback}px))`
|
||||
: `${minContentInset}px`
|
||||
? `max(${FLUSH_CARD_MIN_CONTENT_INSET}px, calc(${contentIndent}px - ${pullback}px))`
|
||||
: `${FLUSH_CARD_MIN_CONTENT_INSET}px`
|
||||
}
|
||||
|
||||
export function getNewCardStyleParentContentMarginLeft(contentIndent: number): number {
|
||||
|
||||
@@ -139,7 +139,6 @@ export function renderWorktreeItemRow(
|
||||
renameRowKey={itemRow.rowKey}
|
||||
lineageChildCount={itemRow.lineageChildCount}
|
||||
lineageChildWorktreeIds={itemRow.lineageChildWorktreeIds}
|
||||
lineageChildIdentities={itemRow.lineageChildIdentities}
|
||||
lineageCollapsed={itemRow.lineageCollapsed}
|
||||
lineageDescendantIdentities={itemRow.lineageDescendantIdentities}
|
||||
onLineageToggle={
|
||||
|
||||
+1
-5
@@ -1,7 +1,3 @@
|
||||
import {
|
||||
getWorktreeCardStyle,
|
||||
isExperimentalWorktreeCardStyle
|
||||
} from '../../../../../../shared/worktree/card-style'
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { useAppStore } from '@/store'
|
||||
import { createProgrammaticScrollMarks } from '@/hooks/programmatic-scroll-marks'
|
||||
@@ -81,7 +77,7 @@ export const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktr
|
||||
repoMap,
|
||||
projectGrouping: props.projectGrouping
|
||||
})
|
||||
const newCardStyle = isExperimentalWorktreeCardStyle(getWorktreeCardStyle(settings))
|
||||
const newCardStyle = settings?.experimentalNewWorktreeCardStyle === true
|
||||
const {
|
||||
handleOpenRepoSettings,
|
||||
handleOpenWorktreeVisibility,
|
||||
|
||||
+13
-32
@@ -5,10 +5,7 @@ import { useAppStore } from '@/store'
|
||||
import type { Worktree } from '../../../../../../shared/worktree/types'
|
||||
import type { WorktreeGroupBy } from '../../../../../../shared/sidebar-list/row-types'
|
||||
import type { RenderRow } from '../listing/render-row'
|
||||
import { lineageIdentities } from '../../../../../../shared/sidebar-list/worktree-lineage-range'
|
||||
import { getWorktreeIdFromHostIdentity } from '../../../../../../shared/worktree/host-qualified-identity'
|
||||
import { folderWorkspaceToWorktree } from '../../../../../../shared/folder-workspaces/folder-workspace-worktree'
|
||||
import type { WorktreeItemRow, FolderWorkspaceItemRow } from '../listing/renderable-rows'
|
||||
import type { WorktreeItemRow } from '../listing/renderable-rows'
|
||||
import { getMountedWorktreeOptionsById } from '../rows/option-dom'
|
||||
|
||||
export function installWorktreeVisibleRefreshVisibilityListener(onChange: () => void): () => void {
|
||||
@@ -46,34 +43,18 @@ export function visibleReviewCardIds(args: {
|
||||
viewportHeight: number
|
||||
}): string[] {
|
||||
const bottom = args.viewportTop + args.viewportHeight
|
||||
const ids = new Set<string>()
|
||||
for (const item of args.virtualItems) {
|
||||
if (item.start >= bottom || item.end <= args.viewportTop) {
|
||||
continue
|
||||
}
|
||||
const row = args.renderRows[item.index]
|
||||
const cards: (WorktreeItemRow | FolderWorkspaceItemRow)[] =
|
||||
row?.type === 'item' || row?.type === 'folder-workspace' ? [row] : []
|
||||
for (const card of cards) {
|
||||
const worktree =
|
||||
card.type === 'item' ? card.worktree : folderWorkspaceToWorktree(card.folderWorkspace)
|
||||
if (
|
||||
card.type === 'item' &&
|
||||
(card.repo?.kind ?? 'git') === 'git' &&
|
||||
!worktree.isBare &&
|
||||
!worktree.isArchived &&
|
||||
worktree.branch
|
||||
) {
|
||||
ids.add(worktree.id)
|
||||
}
|
||||
if (card.lineageDescendantsHidden && card.lineageDescendantIdentities) {
|
||||
for (const identity of lineageIdentities(card.lineageDescendantIdentities)) {
|
||||
ids.add(getWorktreeIdFromHostIdentity(identity))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...ids]
|
||||
return args.virtualItems
|
||||
.filter((item) => item.start < bottom && item.end > args.viewportTop)
|
||||
.map((item) => args.renderRows[item.index])
|
||||
.flatMap((row): WorktreeItemRow[] => (row?.type === 'item' ? [row] : []))
|
||||
.filter(
|
||||
(row) =>
|
||||
(row.repo?.kind ?? 'git') === 'git' &&
|
||||
!row.worktree.isBare &&
|
||||
!row.worktree.isArchived &&
|
||||
Boolean(row.worktree.branch)
|
||||
)
|
||||
.map((row) => row.worktree.id)
|
||||
}
|
||||
|
||||
export function useVisiblePrRefreshReporting(args: {
|
||||
|
||||
@@ -112,26 +112,6 @@ function GenericTerminalIcon({ size = 14 }: { size?: number }): React.JSX.Elemen
|
||||
)
|
||||
}
|
||||
|
||||
export type ShellIconKind = 'powershell' | 'cmd' | 'wsl' | 'git-bash' | 'generic'
|
||||
|
||||
export function shellIconKind(shell: string | null | undefined): ShellIconKind {
|
||||
const normalized = (shell ?? '').toLowerCase()
|
||||
const normalizedName = normalized.replaceAll('\\', '/').split('/').pop()
|
||||
if (normalized === 'powershell.exe' || normalized === 'pwsh.exe') {
|
||||
return 'powershell'
|
||||
}
|
||||
if (normalized === 'cmd.exe') {
|
||||
return 'cmd'
|
||||
}
|
||||
if (normalized === 'wsl.exe' || normalized.startsWith('wsl')) {
|
||||
return 'wsl'
|
||||
}
|
||||
if (normalized === WINDOWS_GIT_BASH_SHELL || normalizedName === 'bash.exe') {
|
||||
return 'git-bash'
|
||||
}
|
||||
return 'generic'
|
||||
}
|
||||
|
||||
export function ShellIcon({
|
||||
shell,
|
||||
size = 14
|
||||
@@ -139,16 +119,19 @@ export function ShellIcon({
|
||||
shell: string | null | undefined
|
||||
size?: number
|
||||
}): React.JSX.Element {
|
||||
switch (shellIconKind(shell)) {
|
||||
case 'powershell':
|
||||
return <PowerShellIcon size={size} />
|
||||
case 'cmd':
|
||||
return <CmdIcon size={size} />
|
||||
case 'wsl':
|
||||
return <WslIcon size={size} />
|
||||
case 'git-bash':
|
||||
return <GitBashIcon size={size} />
|
||||
case 'generic':
|
||||
return <GenericTerminalIcon size={size} />
|
||||
const normalized = (shell ?? '').toLowerCase()
|
||||
const normalizedName = normalized.replaceAll('\\', '/').split('/').pop()
|
||||
if (normalized === 'powershell.exe' || normalized === 'pwsh.exe') {
|
||||
return <PowerShellIcon size={size} />
|
||||
}
|
||||
if (normalized === 'cmd.exe') {
|
||||
return <CmdIcon size={size} />
|
||||
}
|
||||
if (normalized === 'wsl.exe' || normalized.startsWith('wsl')) {
|
||||
return <WslIcon size={size} />
|
||||
}
|
||||
if (normalized === WINDOWS_GIT_BASH_SHELL || normalizedName === 'bash.exe') {
|
||||
return <GitBashIcon size={size} />
|
||||
}
|
||||
return <GenericTerminalIcon size={size} />
|
||||
}
|
||||
|
||||
-16
@@ -659,14 +659,6 @@
|
||||
"4d03dec2d0": "Read app interface trees and perform requested actions.",
|
||||
"6b5a2cd3a5": "Accessibility"
|
||||
},
|
||||
"ExperimentalPane": {
|
||||
"newWorktreeCardStyle": {
|
||||
"copy": "Preview updated worktree-card layout, metadata placement, card-display menu options, and status presentation.",
|
||||
"description": "Preview updated worktree-card layout, metadata placement, card-display menu options, and status presentation.",
|
||||
"title": "New card style",
|
||||
"toggleLabel": "Toggle new card style"
|
||||
}
|
||||
},
|
||||
"McpConfigSection": {
|
||||
"detectedServers_one": "{{detected}} detected · {{count}} server",
|
||||
"detectedServers_other": "{{detected}} detected · {{count}} servers"
|
||||
@@ -831,14 +823,6 @@
|
||||
"permissionsRequired_one": "{{value0}} permission required before agents can operate app windows.",
|
||||
"permissionsRequired_other": "{{value0}} permissions required before agents can operate app windows."
|
||||
},
|
||||
"experimental": {
|
||||
"search": {
|
||||
"newWorktreeCardStyle": {
|
||||
"description": "Preview updated worktree-card layout, metadata placement, card-display menu options, and status presentation.",
|
||||
"title": "New card style"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"search": {
|
||||
"keepForkUpToDate": "Keep Fork Up to Date"
|
||||
|
||||
@@ -8009,12 +8009,6 @@
|
||||
"title": "Agenten-Ruhezustand",
|
||||
"toggleLabel": "Agenten-Ruhezustand umschalten"
|
||||
},
|
||||
"newWorktreeCardStyle": {
|
||||
"copy": "Vorschau auf das überarbeitete Layout der Worktree-Karten, die Anordnung der Metadaten, Menüoptionen zur Kartenanzeige und die Statusdarstellung.",
|
||||
"description": "Vorschau auf das überarbeitete Layout der Worktree-Karten, die Anordnung der Metadaten, Menüoptionen zur Kartenanzeige und die Statusdarstellung.",
|
||||
"title": "Neuer Kartenstil",
|
||||
"toggleLabel": "Neuen Kartenstil umschalten"
|
||||
},
|
||||
"ca2219fe5e": "Zeigt ein kleines animiertes Haustier, das unten rechts angeheftet ist. Im Haustiermenü der Statusleiste eine Figur (Claudino, OpenCode, Gremlin) auswählen oder eine eigene PNG-, APNG-, GIF-, WebP-, JPG- oder SVG-Datei hochladen. Es lässt sich jederzeit über dasselbe Menü ausblenden, ohne diese Einstellung zu deaktivieren.",
|
||||
"dd6f0a1d45": "Haustier",
|
||||
"0e89a574ae": "Schwebendes animiertes Haustier unten rechts.",
|
||||
@@ -10174,11 +10168,9 @@
|
||||
"newWorktreeCardStyle": {
|
||||
"card": "Karte",
|
||||
"cards": "Karten",
|
||||
"description": "Vorschau auf das überarbeitete Layout der Worktree-Karten, die Anordnung der Metadaten, Menüoptionen zur Kartenanzeige und die Statusdarstellung.",
|
||||
"menu": "Menü",
|
||||
"metadata": "Metadaten",
|
||||
"status": "Status",
|
||||
"title": "Neuer Kartenstil",
|
||||
"worktree": "Worktree",
|
||||
"worktrees": "Worktrees"
|
||||
},
|
||||
|
||||
@@ -266,7 +266,6 @@
|
||||
"needsYou": "Needs you",
|
||||
"failed": "Failed",
|
||||
"detailsRow": "Details row",
|
||||
"orcaServer": "Orca server",
|
||||
"workspaceUnread": "Unread in this workspace",
|
||||
"unreadTab": "Unread tab"
|
||||
}
|
||||
@@ -8709,12 +8708,6 @@
|
||||
"title": "Agent sleep",
|
||||
"toggleLabel": "Toggle agent sleep"
|
||||
},
|
||||
"newWorktreeCardStyle": {
|
||||
"copy": "Preview updated worktree-card layout, metadata placement, card-display menu options, and status presentation.",
|
||||
"description": "Preview updated worktree-card layout, metadata placement, card-display menu options, and status presentation.",
|
||||
"title": "New card style",
|
||||
"toggleLabel": "Toggle new card style"
|
||||
},
|
||||
"ca2219fe5e": "Shows a small animated pet pinned to the bottom-right corner. Pick a character (Claudino, OpenCode, Gremlin) or upload your own PNG, APNG, GIF, WebP, JPG, or SVG from the status-bar pet menu. Hide it any time from the same menu without disabling this setting.",
|
||||
"dd6f0a1d45": "Pet",
|
||||
"0e89a574ae": "Floating animated pet in the bottom-right corner.",
|
||||
@@ -10922,11 +10915,9 @@
|
||||
"newWorktreeCardStyle": {
|
||||
"card": "card",
|
||||
"cards": "cards",
|
||||
"description": "Preview updated worktree-card layout, metadata placement, card-display menu options, and status presentation.",
|
||||
"menu": "menu",
|
||||
"metadata": "metadata",
|
||||
"status": "status",
|
||||
"title": "New card style",
|
||||
"worktree": "worktree",
|
||||
"worktrees": "worktrees"
|
||||
},
|
||||
|
||||
@@ -7622,12 +7622,6 @@
|
||||
"title": "Suspensión de agentes",
|
||||
"toggleLabel": "Alternar suspensión de agentes"
|
||||
},
|
||||
"newWorktreeCardStyle": {
|
||||
"copy": "Previsualiza el diseño actualizado de tarjetas de worktree, la ubicación de metadatos, las opciones del menú de visualización de tarjetas y la presentación de estado.",
|
||||
"description": "Previsualiza el diseño actualizado de tarjetas de worktree, la ubicación de metadatos, las opciones del menú de visualización de tarjetas y la presentación de estado.",
|
||||
"title": "Nuevo estilo de tarjeta",
|
||||
"toggleLabel": "Alternar nuevo estilo de tarjeta"
|
||||
},
|
||||
"nativeChat": {
|
||||
"shellEnvNamesLabel": "Variables que se pasan desde tu shell",
|
||||
"shellEnvNamePlaceholder": "Nombre de la variable",
|
||||
@@ -9748,11 +9742,9 @@
|
||||
"newWorktreeCardStyle": {
|
||||
"card": "tarjeta",
|
||||
"cards": "tarjetas",
|
||||
"description": "Previsualiza el diseño actualizado de tarjetas de worktree, la ubicación de metadatos, las opciones del menú de visualización de tarjeta y la presentación de estado.",
|
||||
"menu": "menú",
|
||||
"metadata": "metadatos",
|
||||
"status": "estado",
|
||||
"title": "Nuevo estilo de tarjeta",
|
||||
"worktree": "worktree",
|
||||
"worktrees": "worktrees"
|
||||
},
|
||||
|
||||
@@ -7890,12 +7890,6 @@
|
||||
"title": "Mise en veille des agents",
|
||||
"toggleLabel": "Activer la mise en veille des agents"
|
||||
},
|
||||
"newWorktreeCardStyle": {
|
||||
"copy": "Prévisualise la nouvelle mise en page des cartes de worktree, l'emplacement des métadonnées, les options du menu d'affichage des cartes et la présentation des statuts.",
|
||||
"description": "Prévisualise la nouvelle mise en page des cartes de worktree, l'emplacement des métadonnées, les options du menu d'affichage des cartes et la présentation des statuts.",
|
||||
"title": "Nouveau style de carte",
|
||||
"toggleLabel": "Activer le nouveau style de carte"
|
||||
},
|
||||
"ca2219fe5e": "Affiche un petit compagnon animé épinglé dans le coin inférieur droit. Choisissez un personnage (Claudino, OpenCode, Gremlin) ou importez votre propre PNG, APNG, GIF, WebP, JPG ou SVG depuis le menu compagnon de la barre d'état. Masquez-le à tout moment depuis le même menu sans désactiver ce réglage.",
|
||||
"dd6f0a1d45": "Compagnon",
|
||||
"0e89a574ae": "Compagnon animé flottant dans le coin inférieur droit.",
|
||||
@@ -10056,11 +10050,9 @@
|
||||
"newWorktreeCardStyle": {
|
||||
"card": "carte",
|
||||
"cards": "cartes",
|
||||
"description": "Prévisualise la nouvelle mise en page des cartes de worktree, l'emplacement des métadonnées, les options du menu d'affichage des cartes et la présentation des statuts.",
|
||||
"menu": "menu",
|
||||
"metadata": "métadonnées",
|
||||
"status": "statut",
|
||||
"title": "Nouveau style de carte",
|
||||
"worktree": "worktree",
|
||||
"worktrees": "worktrees"
|
||||
},
|
||||
|
||||
@@ -7335,12 +7335,6 @@
|
||||
"title": "Agent のスリープ",
|
||||
"toggleLabel": "Agent のスリープを切り替え"
|
||||
},
|
||||
"newWorktreeCardStyle": {
|
||||
"copy": "更新されたワークツリーカードのレイアウト、メタデータ配置、カード表示メニュー項目、ステータス表示をプレビューします。",
|
||||
"description": "更新されたワークツリーカードのレイアウト、メタデータ配置、カード表示メニュー項目、ステータス表示をプレビューします。",
|
||||
"title": "新規カードスタイル",
|
||||
"toggleLabel": "新規カードスタイルを切り替え"
|
||||
},
|
||||
"nativeChat": {
|
||||
"shellEnvNamesLabel": "シェルから渡す変数",
|
||||
"shellEnvNamePlaceholder": "変数名",
|
||||
@@ -9496,11 +9490,9 @@
|
||||
"newWorktreeCardStyle": {
|
||||
"card": "カード",
|
||||
"cards": "カード",
|
||||
"description": "更新されたワークツリーカードのレイアウト、メタデータ配置、カード表示メニュー項目、ステータス表示をプレビューします。",
|
||||
"menu": "メニュー",
|
||||
"metadata": "メタデータ",
|
||||
"status": "状態",
|
||||
"title": "新規カードスタイル",
|
||||
"worktree": "ワークツリー",
|
||||
"worktrees": "ワークツリー"
|
||||
},
|
||||
|
||||
@@ -7332,12 +7332,6 @@
|
||||
"title": "에이전트 최대 절전",
|
||||
"toggleLabel": "에이전트 최대 절전 전환"
|
||||
},
|
||||
"newWorktreeCardStyle": {
|
||||
"copy": "업데이트된 워크트리 카드 레이아웃, 메타데이터 배치, 카드 표시 메뉴 옵션 및 상태 표시를 미리 봅니다.",
|
||||
"description": "업데이트된 워크트리 카드 레이아웃, 메타데이터 배치, 카드 표시 메뉴 옵션 및 상태 표시를 미리 봅니다.",
|
||||
"title": "새 카드 스타일",
|
||||
"toggleLabel": "새 카드 스타일 전환"
|
||||
},
|
||||
"nativeChat": {
|
||||
"shellEnvNamesLabel": "셸에서 전달할 변수",
|
||||
"shellEnvNamePlaceholder": "변수 이름",
|
||||
@@ -9456,11 +9450,9 @@
|
||||
"newWorktreeCardStyle": {
|
||||
"card": "카드",
|
||||
"cards": "카드",
|
||||
"description": "업데이트된 워크트리 카드 레이아웃, 메타데이터 배치, 카드 표시 메뉴 옵션 및 상태 표시를 미리 봅니다.",
|
||||
"menu": "메뉴",
|
||||
"metadata": "메타데이터",
|
||||
"status": "상태",
|
||||
"title": "새 카드 스타일",
|
||||
"worktree": "워크트리",
|
||||
"worktrees": "worktrees"
|
||||
},
|
||||
|
||||
@@ -5468,11 +5468,9 @@
|
||||
"newWorktreeCardStyle": {
|
||||
"card": "cartão",
|
||||
"cards": "cartões",
|
||||
"description": "Pré-visualize o layout atualizado do cartão de worktree, o posicionamento dos metadados, as opções do menu de exibição do cartão e a apresentação do status.",
|
||||
"menu": "menu",
|
||||
"metadata": "metadados",
|
||||
"status": "status",
|
||||
"title": "Novo estilo de cartão",
|
||||
"worktree": "worktree",
|
||||
"worktrees": "worktrees"
|
||||
},
|
||||
@@ -5944,12 +5942,6 @@
|
||||
"title": "Suspensão do agente",
|
||||
"toggleLabel": "Alternar suspensão do agente"
|
||||
},
|
||||
"newWorktreeCardStyle": {
|
||||
"copy": "Pré-visualize o layout atualizado do cartão de worktree, o posicionamento dos metadados, as opções do menu de exibição do cartão e a apresentação do status.",
|
||||
"description": "Pré-visualize o layout atualizado do cartão de worktree, o posicionamento dos metadados, as opções do menu de exibição do cartão e a apresentação do status.",
|
||||
"title": "Novo estilo de cartão",
|
||||
"toggleLabel": "Alternar novo estilo de cartão"
|
||||
},
|
||||
"ca2219fe5e": "Mostra um pequeno bichinho animado fixado no canto inferior direito. Escolha um personagem (Claudino, OpenCode, Gremlin) ou envie seu próprio PNG, APNG, GIF, WebP, JPG ou SVG no menu do bichinho na barra de status. Oculte-o quando quiser no mesmo menu sem desativar esta configuração.",
|
||||
"dd6f0a1d45": "Bichinho",
|
||||
"0e89a574ae": "Bichinho animado flutuante no canto inferior direito.",
|
||||
|
||||
@@ -8340,12 +8340,6 @@
|
||||
"title": "Сон агентов",
|
||||
"toggleLabel": "Переключить сон агентов"
|
||||
},
|
||||
"newWorktreeCardStyle": {
|
||||
"copy": "Предпросмотр нового вида карточек рабочих деревьев, размещения метаданных, меню отображения карточек и статусов.",
|
||||
"description": "Предпросмотр нового вида карточек рабочих деревьев, размещения метаданных, меню отображения карточек и статусов.",
|
||||
"title": "Новый вид карточек",
|
||||
"toggleLabel": "Переключить новый вид карточек"
|
||||
},
|
||||
"ca2219fe5e": "Показывает небольшого анимированного питомца в правом нижнем углу. Выберите персонажа (Claudino, OpenCode, Gremlin) или загрузите свой PNG, APNG, GIF, WebP, JPG или SVG через меню питомца в строке состояния. Там же его можно скрыть, не выключая эту настройку.",
|
||||
"dd6f0a1d45": "Питомец",
|
||||
"0e89a574ae": "Анимированный питомец в правом нижнем углу.",
|
||||
@@ -10517,11 +10511,9 @@
|
||||
"newWorktreeCardStyle": {
|
||||
"card": "карточка",
|
||||
"cards": "карточки",
|
||||
"description": "Предпросмотр нового вида карточек рабочих деревьев, размещения метаданных, меню отображения карточек и статусов.",
|
||||
"menu": "меню",
|
||||
"metadata": "метаданные",
|
||||
"status": "статус",
|
||||
"title": "Новый вид карточек",
|
||||
"worktree": "рабочее дерево",
|
||||
"worktrees": "рабочие деревья"
|
||||
},
|
||||
|
||||
@@ -7986,12 +7986,6 @@
|
||||
"title": "Ajan uykusu",
|
||||
"toggleLabel": "Ajan uykusunu aç/kapat"
|
||||
},
|
||||
"newWorktreeCardStyle": {
|
||||
"copy": "Güncellenmiş worktree kartı düzenini, meta veri yerleşimini, kart görüntüleme menü seçeneklerini ve durum sunumunu önizleyin.",
|
||||
"description": "Güncellenmiş worktree kartı düzenini, meta veri yerleşimini, kart görüntüleme menü seçeneklerini ve durum sunumunu önizleyin.",
|
||||
"title": "Yeni kart stili",
|
||||
"toggleLabel": "Yeni kart stilini aç/kapat"
|
||||
},
|
||||
"ca2219fe5e": "Sağ alt köşeye sabitlenmiş küçük, animasyonlu bir evcil hayvan gösterir. Durum çubuğu evcil hayvan menüsünden bir karakter (Claudino, OpenCode, Gremlin) seçin veya kendi PNG, APNG, GIF, WebP, JPG ya da SVG dosyanızı yükleyin. Bu ayarı devre dışı bırakmadan aynı menüden istediğiniz zaman gizleyebilirsiniz.",
|
||||
"dd6f0a1d45": "Evcil Hayvan",
|
||||
"0e89a574ae": "Sağ alt köşede yüzen animasyonlu evcil hayvan.",
|
||||
@@ -10151,11 +10145,9 @@
|
||||
"newWorktreeCardStyle": {
|
||||
"card": "kart",
|
||||
"cards": "kartlar",
|
||||
"description": "Güncellenmiş worktree kartı düzenini, meta veri yerleşimini, kart görüntüleme menüsü seçeneklerini ve durum sunumunu önizleyin.",
|
||||
"menu": "menü",
|
||||
"metadata": "meta veri",
|
||||
"status": "durum",
|
||||
"title": "Yeni kart stili",
|
||||
"worktree": "worktree",
|
||||
"worktrees": "worktree'ler"
|
||||
},
|
||||
|
||||
@@ -7830,12 +7830,6 @@
|
||||
"title": "代理程式睡眠",
|
||||
"toggleLabel": "切換代理程式睡眠"
|
||||
},
|
||||
"newWorktreeCardStyle": {
|
||||
"copy": "預覽更新後的工作樹卡片版面配置、後設資料位置、卡片顯示選單選項和狀態呈現。",
|
||||
"description": "預覽更新後的工作樹卡片版面配置、後設資料位置、卡片顯示選單選項和狀態呈現。",
|
||||
"title": "新卡片樣式",
|
||||
"toggleLabel": "切換新卡片樣式"
|
||||
},
|
||||
"ca2219fe5e": "顯示固定在右下角的小動畫寵物。從狀態列寵物選單中選擇一個角色(Claudino、OpenCode、Gremlin)或上傳您自己的 PNG、APNG、GIF、WebP、JPG 或 SVG。隨時從同一選單隱藏它,而不停用此設定。",
|
||||
"dd6f0a1d45": "寵物",
|
||||
"0e89a574ae": "右下角漂浮的動畫寵物。",
|
||||
@@ -9995,11 +9989,9 @@
|
||||
"newWorktreeCardStyle": {
|
||||
"card": "卡片",
|
||||
"cards": "卡片",
|
||||
"description": "預覽更新後的工作樹卡片版面配置、後設資料位置、卡片顯示選單選項和狀態呈現。",
|
||||
"menu": "選單",
|
||||
"metadata": "後設資料",
|
||||
"status": "狀態",
|
||||
"title": "新卡片樣式",
|
||||
"worktree": "工作樹",
|
||||
"worktrees": "工作樹"
|
||||
},
|
||||
|
||||
@@ -7553,12 +7553,6 @@
|
||||
"title": "代理睡眠",
|
||||
"toggleLabel": "切换代理睡眠"
|
||||
},
|
||||
"newWorktreeCardStyle": {
|
||||
"copy": "预览更新后的工作树卡片布局、元数据位置、卡片显示菜单选项和状态呈现",
|
||||
"description": "预览更新后的工作树卡片布局、元数据位置、卡片显示菜单选项和状态呈现",
|
||||
"title": "新卡片样式",
|
||||
"toggleLabel": "切换新卡片样式"
|
||||
},
|
||||
"nativeChat": {
|
||||
"shellEnvNamesLabel": "从 shell 传递的变量",
|
||||
"shellEnvNamePlaceholder": "变量名",
|
||||
@@ -9693,11 +9687,9 @@
|
||||
"newWorktreeCardStyle": {
|
||||
"card": "卡片",
|
||||
"cards": "卡片",
|
||||
"description": "预览更新后的 Worktree 卡片布局、元数据位置、卡片显示菜单选项和状态呈现",
|
||||
"menu": "菜单",
|
||||
"metadata": "元数据",
|
||||
"status": "状态",
|
||||
"title": "新卡片样式",
|
||||
"worktree": "工作树",
|
||||
"worktrees": "工作树"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import {
|
||||
getWorktreeCardStyle,
|
||||
isExperimentalWorktreeCardStyle
|
||||
} from '../../../../../shared/worktree/card-style'
|
||||
import type { StateCreator } from 'zustand'
|
||||
import type { AppState } from '../../types'
|
||||
import type { GitHubSlice } from '../github-slice-contract'
|
||||
@@ -39,7 +35,7 @@ export const createStaleWorktreeRefreshActions = (
|
||||
const cardProps = state.worktreeCardProperties ?? []
|
||||
const shouldRefreshPR =
|
||||
state.groupBy === 'pr-status' ||
|
||||
(isExperimentalWorktreeCardStyle(getWorktreeCardStyle(state.settings))
|
||||
(state.settings?.experimentalNewWorktreeCardStyle === true
|
||||
? cardProps.includes('status')
|
||||
: cardProps.includes('pr') || cardProps.some((property: string) => property === 'ci')) ||
|
||||
rightSidebarShowsPullRequestData(state)
|
||||
|
||||
@@ -23,10 +23,7 @@ import { callRuntimeResult } from './web-runtime-calls'
|
||||
import { followHostVoiceSettings } from './web-speech-api'
|
||||
import { requireActiveEnvironmentOrNull, webRuntimeState } from './web-runtime-session'
|
||||
import { zcodePlanSiteOwner, settingsForZcodePlanSiteOwner } from './web-zcode-plan-site'
|
||||
import {
|
||||
hostWorktreeCardStyleUpdate,
|
||||
readHostWorktreeCardStyle
|
||||
} from './web-worktree-card-style-sync'
|
||||
import * as cardPreferences from './web-worktree-card-style-sync'
|
||||
import { SETTINGS_STORAGE_KEY, UI_STORAGE_KEY, readJson, writeJson } from './web-storage'
|
||||
|
||||
export type WebSettingsApi = NonNullable<PreloadApi['settings']>
|
||||
@@ -136,15 +133,12 @@ export async function getRuntimeBackedStoredSettings(): Promise<GlobalSettings>
|
||||
}
|
||||
Object.assign(
|
||||
runtimeSettings,
|
||||
readHostWorktreeCardStyle(
|
||||
cardPreferences.read(
|
||||
result.settings,
|
||||
requestedEnvironment.id,
|
||||
currentEnvironment?.id === requestedEnvironment.id
|
||||
)
|
||||
)
|
||||
if (typeof result.settings.compactWorktreeCards === 'boolean') {
|
||||
runtimeSettings.compactWorktreeCards = result.settings.compactWorktreeCards
|
||||
}
|
||||
if (typeof result.settings.minimaxGroupId === 'string') {
|
||||
runtimeSettings.minimaxGroupId = result.settings.minimaxGroupId
|
||||
}
|
||||
@@ -211,10 +205,7 @@ export async function syncRuntimeBackedSettings(
|
||||
if (visibilityDefaults) {
|
||||
runtimeUpdates.worktreeVisibilityDefaults = visibilityDefaults
|
||||
}
|
||||
Object.assign(runtimeUpdates, hostWorktreeCardStyleUpdate(updates, requestedEnvironment.id))
|
||||
if (typeof updates.compactWorktreeCards === 'boolean') {
|
||||
runtimeUpdates.compactWorktreeCards = updates.compactWorktreeCards
|
||||
}
|
||||
Object.assign(runtimeUpdates, cardPreferences.updates(updates, requestedEnvironment.id))
|
||||
if (typeof updates.minimaxGroupId === 'string') {
|
||||
runtimeUpdates.minimaxGroupId = updates.minimaxGroupId
|
||||
}
|
||||
|
||||
@@ -26,8 +26,6 @@ export const webRuntimeState: {
|
||||
zcodePlanSiteRuntimeValue: ZcodePlanSite | null
|
||||
worktreeVisibilityDefaultsRuntimeEnvironmentId: string | null
|
||||
worktreeVisibilityDefaultsRuntimeValue: WorktreeVisibilityDefaults | null
|
||||
/** The paired host that advertised `worktreeCardStyle`; older hosts reject the unknown key. */
|
||||
worktreeCardStyleRuntimeEnvironmentId: string | null
|
||||
worktreeCardDetailsRowRuntimeEnvironmentId: string | null
|
||||
activeClient: WebRuntimeClient | null
|
||||
activeClientEnvironmentId: string | null
|
||||
@@ -38,7 +36,6 @@ export const webRuntimeState: {
|
||||
zcodePlanSiteRuntimeValue: null,
|
||||
worktreeVisibilityDefaultsRuntimeEnvironmentId: null,
|
||||
worktreeVisibilityDefaultsRuntimeValue: null,
|
||||
worktreeCardStyleRuntimeEnvironmentId: null,
|
||||
worktreeCardDetailsRowRuntimeEnvironmentId: null,
|
||||
activeClient: null,
|
||||
activeClientEnvironmentId: null,
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import type { GlobalSettings } from '../../../../shared/global-settings-types'
|
||||
import { isWorktreeCardStyle } from '../../../../shared/worktree/card-style'
|
||||
import { webRuntimeState } from './web-runtime-session'
|
||||
|
||||
type CardStyleSettings = Pick<
|
||||
Partial<GlobalSettings>,
|
||||
'experimentalNewWorktreeCardStyle' | 'worktreeCardStyle' | 'worktreeCardDetailsRow'
|
||||
'experimentalNewWorktreeCardStyle' | 'worktreeCardDetailsRow' | 'compactWorktreeCards'
|
||||
>
|
||||
|
||||
/** Mirror the host's card style, and remember whether this host accepts the three-way field. */
|
||||
export function readHostWorktreeCardStyle(
|
||||
/** Mirror card preferences and remember whether this host accepts the details-row field. */
|
||||
export function read(
|
||||
host: CardStyleSettings,
|
||||
environmentId: string,
|
||||
environmentStillActive: boolean
|
||||
@@ -16,39 +15,30 @@ export function readHostWorktreeCardStyle(
|
||||
if (environmentStillActive) {
|
||||
webRuntimeState.worktreeCardDetailsRowRuntimeEnvironmentId =
|
||||
typeof host.worktreeCardDetailsRow === 'boolean' ? environmentId : null
|
||||
webRuntimeState.worktreeCardStyleRuntimeEnvironmentId = isWorktreeCardStyle(
|
||||
host.worktreeCardStyle
|
||||
)
|
||||
? environmentId
|
||||
: null
|
||||
}
|
||||
return {
|
||||
worktreeCardDetailsRow: host.worktreeCardDetailsRow !== false,
|
||||
...(typeof host.compactWorktreeCards === 'boolean'
|
||||
? { compactWorktreeCards: host.compactWorktreeCards }
|
||||
: {}),
|
||||
...(typeof host.experimentalNewWorktreeCardStyle === 'boolean'
|
||||
? { experimentalNewWorktreeCardStyle: host.experimentalNewWorktreeCardStyle }
|
||||
: {}),
|
||||
...(isWorktreeCardStyle(host.worktreeCardStyle)
|
||||
? { worktreeCardStyle: host.worktreeCardStyle }
|
||||
: {})
|
||||
}
|
||||
}
|
||||
|
||||
/** Older hosts reject unknown setting keys, so send the style only to a host that advertised it. */
|
||||
export function hostWorktreeCardStyleUpdate(
|
||||
updates: CardStyleSettings,
|
||||
environmentId: string
|
||||
): CardStyleSettings {
|
||||
/** Older hosts reject unknown setting keys; only send the details preference when advertised. */
|
||||
export function updates(patch: CardStyleSettings, environmentId: string): CardStyleSettings {
|
||||
return {
|
||||
...(typeof updates.worktreeCardDetailsRow === 'boolean' &&
|
||||
...(typeof patch.worktreeCardDetailsRow === 'boolean' &&
|
||||
webRuntimeState.worktreeCardDetailsRowRuntimeEnvironmentId === environmentId
|
||||
? { worktreeCardDetailsRow: updates.worktreeCardDetailsRow }
|
||||
? { worktreeCardDetailsRow: patch.worktreeCardDetailsRow }
|
||||
: {}),
|
||||
...(typeof updates.experimentalNewWorktreeCardStyle === 'boolean'
|
||||
? { experimentalNewWorktreeCardStyle: updates.experimentalNewWorktreeCardStyle }
|
||||
...(typeof patch.compactWorktreeCards === 'boolean'
|
||||
? { compactWorktreeCards: patch.compactWorktreeCards }
|
||||
: {}),
|
||||
...(isWorktreeCardStyle(updates.worktreeCardStyle) &&
|
||||
webRuntimeState.worktreeCardStyleRuntimeEnvironmentId === environmentId
|
||||
? { worktreeCardStyle: updates.worktreeCardStyle }
|
||||
...(typeof patch.experimentalNewWorktreeCardStyle === 'boolean'
|
||||
? { experimentalNewWorktreeCardStyle: patch.experimentalNewWorktreeCardStyle }
|
||||
: {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ describe('web settings preload API', () => {
|
||||
ok: true,
|
||||
result: {
|
||||
settings: {
|
||||
worktreeCardStyle: 'experimental-2',
|
||||
worktreeCardDetailsRow: method === 'settings.update'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -113,7 +113,6 @@ export const SettingsUpdate = z
|
||||
.enum(['issues', 'my-issues', 'prs', 'my-prs', 'review', 'all'])
|
||||
.optional(),
|
||||
experimentalNewWorktreeCardStyle: z.boolean().optional(),
|
||||
worktreeCardStyle: z.enum(['classic', 'experimental-1', 'experimental-2']).optional(),
|
||||
worktreeCardDetailsRow: z.boolean().optional(),
|
||||
/** No UI: the host-side switch for connectors, so a headless server can be turned on. */
|
||||
experimentalConnectors: z.boolean().optional(),
|
||||
|
||||
@@ -166,7 +166,7 @@ describe('buildSidebarFeedRows nests each paired server’s own lineage', () =>
|
||||
expect([...lineageIdentities(parent.lineageDescendantIdentities ?? [])]).toEqual([
|
||||
'runtime:a|r::/r/c'
|
||||
])
|
||||
expect(parent.lineageDescendantsHidden).toBe(collapsed)
|
||||
expect(parent.lineageCollapsed).toBe(collapsed)
|
||||
expect(outline(built.sectionRows).includes('runtime:a|r::/r/c@1')).toBe(!collapsed)
|
||||
})
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { ProjectGroup } from '../project-group-types'
|
||||
import type { Repo } from '../repo-types'
|
||||
import type { DetectedWorktree, WorkspaceStatus, Worktree } from '../worktree/types'
|
||||
import type { ExecutionHostId } from '../execution-host'
|
||||
import type { HostScopedWorktreeKey } from '../worktree/host-qualified-identity'
|
||||
import type { PRGroupKey } from '../workspace-view/workspace-pr-groups'
|
||||
|
||||
export type WorktreeGroupBy = 'none' | 'workspace-status' | 'repo' | 'pr-status'
|
||||
@@ -65,13 +64,10 @@ export type WorktreeRow = {
|
||||
isLastLineageChild: boolean
|
||||
lineageChildCount: number
|
||||
lineageChildWorktreeIds?: readonly string[]
|
||||
/** Host-qualified, since workspace ids repeat across hosts. */
|
||||
lineageChildIdentities?: readonly HostScopedWorktreeKey[]
|
||||
lineageGroupKey?: string
|
||||
lineageCollapsed?: boolean
|
||||
/** Every workspace nested under this one, collapsed or not; host-qualified. */
|
||||
lineageDescendantIdentities?: WorktreeLineageSource
|
||||
lineageDescendantsHidden?: boolean
|
||||
hostContextLabel?: string
|
||||
showProjectName?: boolean
|
||||
}
|
||||
@@ -120,10 +116,7 @@ export type PendingCreationRow = {
|
||||
}
|
||||
|
||||
export type FolderWorkspaceRow = {
|
||||
occurrence?: WorktreeOccurrence<Worktree>
|
||||
lineageChildIdentities?: readonly HostScopedWorktreeKey[]
|
||||
lineageDescendantIdentities?: WorktreeLineageSource
|
||||
lineageDescendantsHidden?: boolean
|
||||
type: 'folder-workspace'
|
||||
key: string
|
||||
folderWorkspace: FolderWorkspace
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import type { Repo } from '../../repo-types'
|
||||
import type { Worktree } from '../../worktree/types'
|
||||
import {
|
||||
getWorktreeHostIdentity,
|
||||
type HostScopedWorktreeKey
|
||||
} from '../../worktree/host-qualified-identity'
|
||||
import { getWorktreeHostIdentity } from '../../worktree/host-qualified-identity'
|
||||
import { getFolderWorkspaceHostIdentity } from '../../folder-workspaces/folder-workspace-worktree'
|
||||
import { getWorktreeLineageGroupKey } from './group-keys'
|
||||
import { buildWorktreeLineageRanges, type WorktreeLineageSource } from './worktree-lineage-range'
|
||||
import { buildWorktreeLineageRanges, type WorktreeLineageSource } from '../worktree-lineage-range'
|
||||
import {
|
||||
getExpandedWorktreeOccurrences,
|
||||
type WorktreeOccurrence,
|
||||
@@ -86,15 +83,11 @@ export function appendWorktreeRows(
|
||||
const lineageRanges = buildWorktreeLineageRanges(plan)
|
||||
const sectionOccurrences = plan.bySection.get(options.sectionKey) ?? []
|
||||
const childIdsByParent = new Map<string, string[]>()
|
||||
const childIdentitiesByParent = new Map<string, HostScopedWorktreeKey[]>()
|
||||
sectionOccurrences.forEach((occurrence) => {
|
||||
if (occurrence.parent) {
|
||||
const children = childIdsByParent.get(occurrence.parent.key) ?? []
|
||||
children.push(occurrence.row.id)
|
||||
childIdsByParent.set(occurrence.parent.key, children)
|
||||
const identities = childIdentitiesByParent.get(occurrence.parent.key) ?? []
|
||||
identities.push(getWorktreeHostIdentity(occurrence.row))
|
||||
childIdentitiesByParent.set(occurrence.parent.key, identities)
|
||||
}
|
||||
})
|
||||
const occurrences = getExpandedWorktreeOccurrences(sectionOccurrences, (worktree) =>
|
||||
@@ -108,7 +101,6 @@ export function appendWorktreeRows(
|
||||
buildFolderWorkspaceRow(folderPair, options.groupDepth, occurrence, {
|
||||
collapsedGroups: options.collapsedGroups,
|
||||
childWorktreeIds: childIdsByParent.get(occurrence.key),
|
||||
childIdentities: childIdentitiesByParent.get(occurrence.key),
|
||||
descendantIdentities: lineageRanges.get(occurrence.key),
|
||||
hostContextLabel: options.hostContextLabelByWorktreeIdentity?.get(
|
||||
getWorktreeHostIdentity(worktree)
|
||||
@@ -129,11 +121,7 @@ export function appendWorktreeRows(
|
||||
isLastLineageChild: occurrence.isLastChild,
|
||||
lineageChildCount: occurrence.childCount,
|
||||
lineageChildWorktreeIds: childIdsByParent.get(occurrence.key),
|
||||
lineageChildIdentities: childIdentitiesByParent.get(occurrence.key),
|
||||
lineageDescendantIdentities: lineageRanges.get(occurrence.key),
|
||||
lineageDescendantsHidden:
|
||||
occurrence.childCount > 0 &&
|
||||
options.collapsedGroups.has(getWorktreeLineageGroupKey(worktree)),
|
||||
showProjectName: occurrence.showProjectName,
|
||||
hostContextLabel:
|
||||
options.hostContextLabelByWorktreeIdentity?.get(getWorktreeHostIdentity(worktree)) ??
|
||||
@@ -156,7 +144,6 @@ export function buildFolderWorkspaceRow(
|
||||
options?: {
|
||||
collapsedGroups: ReadonlySet<string>
|
||||
childWorktreeIds?: readonly string[]
|
||||
childIdentities?: readonly HostScopedWorktreeKey[]
|
||||
descendantIdentities?: WorktreeLineageSource
|
||||
hostContextLabel?: string
|
||||
}
|
||||
@@ -165,11 +152,7 @@ export function buildFolderWorkspaceRow(
|
||||
occurrence && occurrence.childCount > 0 ? getWorktreeLineageGroupKey(occurrence.row) : undefined
|
||||
return {
|
||||
type: 'folder-workspace',
|
||||
occurrence,
|
||||
lineageChildIdentities: options?.childIdentities,
|
||||
lineageDescendantIdentities: options?.descendantIdentities,
|
||||
lineageDescendantsHidden:
|
||||
lineageGroupKey !== undefined && options?.collapsedGroups.has(lineageGroupKey) === true,
|
||||
key: `folder-workspace:${getFolderWorkspaceHostIdentity(pair.folderWorkspace)}`,
|
||||
folderWorkspace: pair.folderWorkspace,
|
||||
projectGroup: pair.projectGroup,
|
||||
|
||||
@@ -8,11 +8,7 @@ import {
|
||||
export type WorktreeLineageTree = {
|
||||
identities: HostScopedWorktreeKey[]
|
||||
worktrees: Worktree[]
|
||||
parents: number[]
|
||||
ends: number[]
|
||||
indexByIdentity: Map<HostScopedWorktreeKey, number>
|
||||
indexByWorktreeId: Map<string, number | null>
|
||||
rangesByOccurrence: Map<string, WorktreeLineageRange>
|
||||
}
|
||||
|
||||
export type WorktreeLineageRange = {
|
||||
@@ -55,25 +51,15 @@ export function buildWorktreeLineageRanges(
|
||||
const tree: WorktreeLineageTree = {
|
||||
identities: [],
|
||||
worktrees: [],
|
||||
parents: [],
|
||||
ends: [],
|
||||
indexByIdentity: new Map(),
|
||||
indexByWorktreeId: new Map(),
|
||||
rangesByOccurrence: result
|
||||
ends: []
|
||||
}
|
||||
const stack: number[] = []
|
||||
occurrences.forEach((occurrence, index) => {
|
||||
while (stack.length && occurrences[stack.at(-1)!]!.depth >= occurrence.depth) {
|
||||
tree.ends[stack.pop()!] = index
|
||||
}
|
||||
tree.parents.push(stack.at(-1) ?? -1)
|
||||
tree.identities.push(getWorktreeHostIdentity(occurrence.row))
|
||||
tree.worktrees.push(occurrence.row)
|
||||
tree.indexByIdentity.set(getWorktreeHostIdentity(occurrence.row), index)
|
||||
tree.indexByWorktreeId.set(
|
||||
occurrence.row.id,
|
||||
tree.indexByWorktreeId.has(occurrence.row.id) ? null : index
|
||||
)
|
||||
stack.push(index)
|
||||
})
|
||||
for (const index of stack) {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
export type WorktreeCardDisplaySettings = {
|
||||
/** Which worktree card the sidebar draws ('experimental-2' is retired and reads as 'experimental-1'); `experimentalNewWorktreeCardStyle` stays in step for older clients. */
|
||||
worktreeCardStyle?: 'classic' | 'experimental-1' | 'experimental-2'
|
||||
/** Experimental card: draw the details row (Card display items) under each name unless explicitly disabled. */
|
||||
worktreeCardDetailsRow?: boolean
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getWorktreeCardStyle, worktreeCardStyleSettingsUpdate } from './card-style'
|
||||
|
||||
describe('getWorktreeCardStyle', () => {
|
||||
it('maps the legacy boolean when the style was never written', () => {
|
||||
expect(getWorktreeCardStyle({ experimentalNewWorktreeCardStyle: true })).toBe('experimental-1')
|
||||
expect(getWorktreeCardStyle({ experimentalNewWorktreeCardStyle: false })).toBe('classic')
|
||||
expect(getWorktreeCardStyle({})).toBe('classic')
|
||||
expect(getWorktreeCardStyle(null)).toBe('classic')
|
||||
})
|
||||
|
||||
it('round-trips every style through the paired settings update', () => {
|
||||
for (const style of ['classic', 'experimental-1'] as const) {
|
||||
expect(getWorktreeCardStyle(worktreeCardStyleSettingsUpdate(style))).toBe(style)
|
||||
}
|
||||
})
|
||||
|
||||
it('reads the retired second experimental card as the experimental card', () => {
|
||||
expect(
|
||||
getWorktreeCardStyle({
|
||||
worktreeCardStyle: 'experimental-2',
|
||||
experimentalNewWorktreeCardStyle: true
|
||||
})
|
||||
).toBe('experimental-1')
|
||||
})
|
||||
|
||||
it('lets an older client that only flips the boolean win', () => {
|
||||
expect(
|
||||
getWorktreeCardStyle({
|
||||
worktreeCardStyle: 'experimental-2',
|
||||
experimentalNewWorktreeCardStyle: false
|
||||
})
|
||||
).toBe('classic')
|
||||
expect(
|
||||
getWorktreeCardStyle({
|
||||
worktreeCardStyle: 'classic',
|
||||
experimentalNewWorktreeCardStyle: true
|
||||
})
|
||||
).toBe('experimental-1')
|
||||
})
|
||||
})
|
||||
@@ -1,42 +0,0 @@
|
||||
import type { GlobalSettings } from '../global-settings-types'
|
||||
|
||||
export const WORKTREE_CARD_STYLES = ['classic', 'experimental-1'] as const
|
||||
|
||||
export type WorktreeCardStyle = (typeof WORKTREE_CARD_STYLES)[number]
|
||||
|
||||
type WorktreeCardStyleSettings = Pick<
|
||||
GlobalSettings,
|
||||
'worktreeCardStyle' | 'experimentalNewWorktreeCardStyle'
|
||||
>
|
||||
|
||||
export function isWorktreeCardStyle(value: unknown): value is WorktreeCardStyle {
|
||||
return WORKTREE_CARD_STYLES.some((style) => style === value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Older clients only write the boolean, so it wins whenever the two disagree. A stored
|
||||
* 'experimental-2' (the retired second card) reads as the one experimental card.
|
||||
*/
|
||||
export function getWorktreeCardStyle(
|
||||
settings: Partial<WorktreeCardStyleSettings> | null | undefined
|
||||
): WorktreeCardStyle {
|
||||
const legacyExperimental = settings?.experimentalNewWorktreeCardStyle === true
|
||||
const style = settings?.worktreeCardStyle
|
||||
if (isWorktreeCardStyle(style) && (style !== 'classic') === legacyExperimental) {
|
||||
return style
|
||||
}
|
||||
return legacyExperimental ? 'experimental-1' : 'classic'
|
||||
}
|
||||
|
||||
export function isExperimentalWorktreeCardStyle(style: WorktreeCardStyle): boolean {
|
||||
return style !== 'classic'
|
||||
}
|
||||
|
||||
export function worktreeCardStyleSettingsUpdate(
|
||||
style: WorktreeCardStyle
|
||||
): Required<WorktreeCardStyleSettings> {
|
||||
return {
|
||||
worktreeCardStyle: style,
|
||||
experimentalNewWorktreeCardStyle: isExperimentalWorktreeCardStyle(style)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user