diff --git a/src/renderer/src/components/feature-wall/AddReposAnimatedVisual.tsx b/src/renderer/src/components/feature-wall/AddReposAnimatedVisual.tsx index cb8f24ee5ae..06f6350c52a 100644 --- a/src/renderer/src/components/feature-wall/AddReposAnimatedVisual.tsx +++ b/src/renderer/src/components/feature-wall/AddReposAnimatedVisual.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' import type { JSX, ReactNode } from 'react' -import { Briefcase, FolderGit2, House } from 'lucide-react' +import { FolderGit2 } from 'lucide-react' import { cn } from '@/lib/utils' import { ClaudeIcon } from '../status-bar/icons' import { CodexInlineIcon, WorkingSpinner } from './feature-tour-preview-glyphs' @@ -9,29 +9,32 @@ type RepoFocus = 'personal' | 'work' | 'both' type RepoVisual = { id: Exclude - label: string name: string worktree: string + prompt: string + action: string agent: 'Claude Code' | 'Codex' - icon: ReactNode + agentIcon: ReactNode } const REPOS: readonly RepoVisual[] = [ { id: 'personal', - label: 'Personal', name: 'recipe-box', worktree: 'weekend polish', + prompt: 'refine recipe search', + action: 'Editing search filters', agent: 'Claude Code', - icon: + agentIcon: }, { id: 'work', - label: 'Work', name: 'billing-app', worktree: 'checkout fix', + prompt: 'fix checkout bug', + action: 'Updating checkout tests', agent: 'Codex', - icon: + agentIcon: } ] @@ -62,59 +65,107 @@ function RepoProjectCard(props: { return (
-
- - {props.repo.icon} - -
-
- {props.repo.name} -
-
- {props.repo.label} -
-
-
+ -
-
-
- - - - - {props.repo.worktree} - -
-
- - > - {props.repo.id === 'personal' ? 'refine recipe search' : 'fix checkout bug'} - - - - {props.repo.agent} working - - - {props.repo.agent === 'Codex' ? : } - {props.repo.id === 'personal' ? 'Updating UI' : 'Editing tests'} - -
+ + +
+ +
+ + > + {props.repo.prompt} + + + {props.repo.agentIcon} + {props.repo.agent} session ready + + + + {props.repo.action} +
) } +function ProjectSidebarRow(props: { name: string }): JSX.Element { + return ( +
+ + + + + {props.name} + + + 1 worktree + +
+ ) +} + +function ProjectWorktreeRow(props: { + repo: RepoVisual + active: boolean + reducedMotion: boolean +}): JSX.Element { + return ( +
+
+ + + {props.repo.worktree} + +
+
+ + + {props.repo.agentIcon} + + + {props.repo.prompt} + +
+
+ ) +} + +function VisualTitlebar(props: { title: string }): JSX.Element { + return ( +
+ + + + + {props.title} + +
+ ) +} + function TerminalLine(props: { children: ReactNode; muted?: boolean }): JSX.Element { return (
{WORKTREE_LABEL}
-
- +
+ - orca computer{' '} - get-app-state - - } - arg='--app "Notes"' + commandLines={['orca computer get-app-state', '--app "Notes"']} active={props.phase === 'inspect'} done={props.targetVisible} output={ @@ -191,31 +181,19 @@ function AgentWorktreeTerminal(props: { } /> - orca computer{' '} - click - - } - arg="--element-index 7" + commandLines={['orca computer click', '--element-index 7']} active={props.phase === 'target' || props.phase === 'click'} done={props.clicked} output={props.clicked ? <>click sent : null} /> - orca computer{' '} - get-app-state - - } - arg='--app "Notes" --json' + commandLines={['orca computer get-app-state', '--app "Notes" --json']} active={props.phase === 'click'} done={props.verified} output={ props.verified ? ( <> - state: Approved + → computer ) : null } @@ -225,62 +203,38 @@ function AgentWorktreeTerminal(props: { ) } -function ClaudePrompt(props: { text: string; active: boolean; done: boolean }): JSX.Element { +function TerminalRequest(props: { text: string }): JSX.Element { return ( -
- > - - {props.text} - +
+ {props.text}
) } function CliCommandBlock(props: { - command: JSX.Element - arg: string + commandLines: readonly string[] active: boolean done: boolean output: JSX.Element | null }): JSX.Element { + const commandTone = props.active || props.done ? 'text-foreground' : 'text-muted-foreground/65' return (
-
- - $ - -
-
- {props.command} +
$
+
+ {props.commandLines.map((line) => ( +
+ {line}
-
{props.arg}
-
+ ))}
- {props.done ? ( - - ) : ( - → - )} - {props.output ?? ' '} + {props.output ?? ' '}
) diff --git a/src/renderer/src/components/feature-wall/feature-wall-setup-progress.test.ts b/src/renderer/src/components/feature-wall/feature-wall-setup-progress.test.ts index 9a7eec78499..6ffdef92125 100644 --- a/src/renderer/src/components/feature-wall/feature-wall-setup-progress.test.ts +++ b/src/renderer/src/components/feature-wall/feature-wall-setup-progress.test.ts @@ -18,7 +18,7 @@ function makeInput( gitRepoCount: 0, worktreesByRepo: {}, tabsByWorktree: {}, - runtimePaneTitlesByTabId: {}, + agentStatusByPaneKey: {}, hasSetupScript: false, ...overrides } @@ -65,7 +65,7 @@ describe('getFeatureWallSetupProgress', () => { expect(progress.stepDone['two-agents']).toBe(false) }) - it('marks two agents complete once two agent sessions exist in one worktree', () => { + it('does not mark two agents complete from terminal titles alone', () => { const progress = getFeatureWallSetupProgress( makeInput({ worktreesByRepo: { 'repo-1': [makeWorktree('worktree-1')] }, @@ -78,9 +78,82 @@ describe('getFeatureWallSetupProgress', () => { }) ) + expect(progress.stepDone['two-agents']).toBe(false) + }) + + it('marks two agents complete once two hook-reported agent sessions exist in one worktree', () => { + const progress = getFeatureWallSetupProgress( + makeInput({ + worktreesByRepo: { 'repo-1': [makeWorktree('worktree-1')] }, + tabsByWorktree: { + 'worktree-1': [ + { id: 'tab-1', title: 'Terminal' }, + { id: 'tab-2', title: 'Terminal' } + ] as never + }, + agentStatusByPaneKey: { + 'tab-1:00000000-0000-4000-8000-000000000001': { + paneKey: 'tab-1:00000000-0000-4000-8000-000000000001', + state: 'working', + prompt: 'first task', + updatedAt: 1, + stateStartedAt: 1, + agentType: 'claude', + stateHistory: [] + }, + 'tab-2:00000000-0000-4000-8000-000000000002': { + paneKey: 'tab-2:00000000-0000-4000-8000-000000000002', + state: 'waiting', + prompt: 'second task', + updatedAt: 2, + stateStartedAt: 2, + agentType: 'codex', + stateHistory: [] + } + } + }) + ) + expect(progress.stepDone['two-agents']).toBe(true) }) + it('does not mark two agents complete when hook-reported agents are in separate worktrees', () => { + const progress = getFeatureWallSetupProgress( + makeInput({ + worktreesByRepo: { + 'repo-1': [makeWorktree('worktree-1')], + 'repo-2': [makeWorktree('worktree-2')] + }, + tabsByWorktree: { + 'worktree-1': [{ id: 'tab-1', title: 'Terminal' }] as never, + 'worktree-2': [{ id: 'tab-2', title: 'Terminal' }] as never + }, + agentStatusByPaneKey: { + 'tab-1:00000000-0000-4000-8000-000000000001': { + paneKey: 'tab-1:00000000-0000-4000-8000-000000000001', + state: 'working', + prompt: 'first task', + updatedAt: 1, + stateStartedAt: 1, + agentType: 'claude', + stateHistory: [] + }, + 'tab-2:00000000-0000-4000-8000-000000000002': { + paneKey: 'tab-2:00000000-0000-4000-8000-000000000002', + state: 'working', + prompt: 'second task', + updatedAt: 2, + stateStartedAt: 2, + agentType: 'codex', + stateHistory: [] + } + } + }) + ) + + expect(progress.stepDone['two-agents']).toBe(false) + }) + it('marks worktrees complete once two worktrees exist', () => { expect( getFeatureWallSetupProgress( diff --git a/src/renderer/src/components/feature-wall/feature-wall-setup-progress.ts b/src/renderer/src/components/feature-wall/feature-wall-setup-progress.ts index c41a3271e20..c8fcb045bfd 100644 --- a/src/renderer/src/components/feature-wall/feature-wall-setup-progress.ts +++ b/src/renderer/src/components/feature-wall/feature-wall-setup-progress.ts @@ -4,7 +4,8 @@ import { type FeatureWallSetupStepId } from '../../../../shared/feature-wall-setup-steps' import type { GlobalSettings, TerminalTab, Worktree } from '../../../../shared/types' -import { getAgentLabel } from '../../lib/agent-status' +import type { AgentStatusEntry } from '../../../../shared/agent-status-types' +import { parsePaneKey } from '../../../../shared/stable-pane-id' export type FeatureWallSetupProgressInput = { settings: GlobalSettings | null @@ -17,7 +18,7 @@ export type FeatureWallSetupProgressInput = { gitRepoCount: number worktreesByRepo: Record tabsByWorktree: Record - runtimePaneTitlesByTabId: Record> + agentStatusByPaneKey: Record hasSetupScript: boolean } @@ -27,30 +28,38 @@ export type FeatureWallSetupProgress = { coreTotal: number } -function hasTwoAgentSessionsInOneWorktree(input: FeatureWallSetupProgressInput): boolean { +function hasTwoHookReportedAgentsInOneWorktree(input: FeatureWallSetupProgressInput): boolean { + const validWorktreeIds = new Set( + Object.values(input.worktreesByRepo) + .flat() + .map((worktree) => worktree.id) + ) + const tabIdToWorktreeId = new Map() for (const [worktreeId, tabs] of Object.entries(input.tabsByWorktree)) { - if ( - !Object.values(input.worktreesByRepo).some((worktrees) => - worktrees.some((w) => w.id === worktreeId) - ) - ) { + if (!validWorktreeIds.has(worktreeId)) { continue } - let agentSessionCount = 0 for (const tab of tabs) { - const paneTitles = input.runtimePaneTitlesByTabId[tab.id] - if (paneTitles && Object.keys(paneTitles).length > 0) { - agentSessionCount += Object.values(paneTitles).filter((title) => - getAgentLabel(title) - ).length - } else if (getAgentLabel(tab.title)) { - agentSessionCount += 1 - } - if (agentSessionCount >= 2) { - return true - } + tabIdToWorktreeId.set(tab.id, worktreeId) } } + + const agentCountsByWorktree = new Map() + for (const paneKey of Object.keys(input.agentStatusByPaneKey)) { + const parsed = parsePaneKey(paneKey) + if (!parsed) { + continue + } + const worktreeId = tabIdToWorktreeId.get(parsed.tabId) + if (!worktreeId) { + continue + } + const count = (agentCountsByWorktree.get(worktreeId) ?? 0) + 1 + if (count >= 2) { + return true + } + agentCountsByWorktree.set(worktreeId, count) + } return false } @@ -73,7 +82,7 @@ export function getFeatureWallSetupProgress( notifications: input.settings?.notifications.enabled === true && input.settings.notifications.agentTaskComplete === true, - 'two-agents': hasTwoAgentSessionsInOneWorktree(input), + 'two-agents': hasTwoHookReportedAgentsInOneWorktree(input), 'three-workspaces': countWorkspaces(input.worktreesByRepo) >= 2, 'task-sources': input.hasConnectedTaskSource, 'agent-capabilities': agentCapabilitiesDone, diff --git a/src/renderer/src/components/setup-guide/use-setup-guide-progress.ts b/src/renderer/src/components/setup-guide/use-setup-guide-progress.ts index bc6197c6c34..58630be6f4f 100644 --- a/src/renderer/src/components/setup-guide/use-setup-guide-progress.ts +++ b/src/renderer/src/components/setup-guide/use-setup-guide-progress.ts @@ -26,7 +26,7 @@ export function useSetupGuideProgress( const featureInteractions = useAppStore((s) => s.featureInteractions) const worktreesByRepo = useAppStore((s) => s.worktreesByRepo) const tabsByWorktree = useAppStore((s) => s.tabsByWorktree) - const runtimePaneTitlesByTabId = useAppStore((s) => s.runtimePaneTitlesByTabId) + const agentStatusByPaneKey = useAppStore((s) => s.agentStatusByPaneKey) const preflightStatus = useAppStore((s) => s.preflightStatus) const preflightStatusChecked = useAppStore((s) => s.preflightStatusChecked) const refreshPreflightStatus = useAppStore((s) => s.refreshPreflightStatus) @@ -168,7 +168,7 @@ export function useSetupGuideProgress( gitRepoCount, worktreesByRepo, tabsByWorktree, - runtimePaneTitlesByTabId, + agentStatusByPaneKey, hasSetupScript }), [ @@ -178,11 +178,11 @@ export function useSetupGuideProgress( detectedBrowserUseSkillInstalled, detectedOrchestrationSkillInstalled, featureInteractions, + agentStatusByPaneKey, gitRepoCount, hasConnectedTaskSource, hasSetupScript, orchestrationSkillInstalled, - runtimePaneTitlesByTabId, settings, tabsByWorktree, worktreesByRepo diff --git a/src/shared/feature-wall-setup-steps.ts b/src/shared/feature-wall-setup-steps.ts index abc1011535b..fc3860812a0 100644 --- a/src/shared/feature-wall-setup-steps.ts +++ b/src/shared/feature-wall-setup-steps.ts @@ -48,8 +48,8 @@ export const FEATURE_WALL_SETUP_STEPS: readonly FeatureWallSetupStep[] = [ }, { id: 'agent-capabilities', - name: 'Enable advanced agent capabilities', - subtitle: 'Enable advanced agent capabilities', + name: 'Install CLI & Skills', + subtitle: 'Install CLI & Skills', description: 'Enable the tools that let agents work more independently.' }, {