mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 08:03:13 +00:00
Show destination worktree label on agent rows for live-worktree mismatch
This commit is contained in:
@@ -417,4 +417,31 @@ describe('DashboardAgentRow', () => {
|
||||
expect(classTokens(markup)).toContain('worktree-agent-lineage-parent-row')
|
||||
expect(markup).toContain('aria-label="Show 2 child agents"')
|
||||
})
|
||||
|
||||
it('notes the destination worktree beside the prompt without displacing the row', () => {
|
||||
const markup = renderRow(
|
||||
makeAgent({
|
||||
liveWorktreeMismatch: {
|
||||
destinationWorktreeId: 'wt-scratch',
|
||||
destinationLabel: 'scratch-fix'
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
expect(markup).toContain('data-agent-live-worktree-mismatch')
|
||||
expect(markup).toContain('in scratch-fix')
|
||||
expect(markup).toContain('Fix hover scope')
|
||||
expect(markup).toContain('title="Fix hover scope in scratch-fix"')
|
||||
expect(markup).toContain('aria-label="Dismiss agent"')
|
||||
expect(classTokensForTaggedElement(markup, 'data-agent-live-worktree-mismatch')).toContain(
|
||||
'text-muted-foreground/70'
|
||||
)
|
||||
})
|
||||
|
||||
it('omits the destination note when the agent runs in its own worktree', () => {
|
||||
const markup = renderRow(makeAgent())
|
||||
|
||||
expect(markup).not.toContain('data-agent-live-worktree-mismatch')
|
||||
expect(markup).toContain('title="Fix hover scope"')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -413,4 +413,52 @@ describe('WorktreeCardAgents activation', () => {
|
||||
act(() => root.unmount())
|
||||
host.remove()
|
||||
})
|
||||
|
||||
it('activates the owning pane even when the row reports another worktree', async () => {
|
||||
mockAgentActivityDisplayMode = 'compact'
|
||||
const tabId = 'mismatch-tab'
|
||||
const paneKey = makePaneKey(tabId, LEAF_B)
|
||||
mockAgents = [
|
||||
{
|
||||
...mockAgent({
|
||||
paneKey,
|
||||
tabId,
|
||||
agentType: 'claude',
|
||||
prompt: 'Editing elsewhere',
|
||||
worktreeId: 'wt-1'
|
||||
}),
|
||||
// Why: the note is display-only; it must not redirect activation.
|
||||
liveWorktreeMismatch: {
|
||||
destinationWorktreeId: 'wt-scratch',
|
||||
destinationLabel: 'scratch-fix'
|
||||
}
|
||||
} as DashboardAgentRowData
|
||||
]
|
||||
mockAgentStatusByPaneKey = { [paneKey]: { worktreeId: 'wt-1' } }
|
||||
mockTabsByWorktree = { 'wt-1': [{ id: tabId }] }
|
||||
const host = document.createElement('div')
|
||||
document.body.append(host)
|
||||
const root: Root = createRoot(host)
|
||||
const { default: WorktreeCardAgents } = await import('./WorktreeCardAgents')
|
||||
|
||||
await act(async () => {
|
||||
root.render(<WorktreeCardAgents worktreeId="wt-1" />)
|
||||
})
|
||||
const row = host.querySelector('.compact-agent-row')
|
||||
expect(row?.textContent).toContain('in scratch-fix')
|
||||
|
||||
await act(async () => {
|
||||
row?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||
})
|
||||
|
||||
expect(activationMocks.activateAndRevealWorktree).toHaveBeenCalledWith('wt-1')
|
||||
expect(activationMocks.activateAndRevealWorktree).not.toHaveBeenCalledWith('wt-scratch')
|
||||
expect(activationMocks.activateTabAndFocusPane).toHaveBeenCalledWith(tabId, LEAF_B, {
|
||||
ackPaneKeyOnSuccess: paneKey,
|
||||
flashFocusedPane: true,
|
||||
scrollToBottomIfOutputSinceLastView: true
|
||||
})
|
||||
act(() => root.unmount())
|
||||
host.remove()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -29,6 +29,7 @@ type MockAgentOptions = {
|
||||
isLastSibling: boolean
|
||||
childCount: number
|
||||
}
|
||||
liveWorktreeMismatch?: { destinationWorktreeId: string; destinationLabel: string }
|
||||
}
|
||||
|
||||
function mockAgent({
|
||||
@@ -43,13 +44,15 @@ function mockAgent({
|
||||
stateStartedAt = 1000,
|
||||
terminalHandle,
|
||||
orchestration,
|
||||
lineage
|
||||
lineage,
|
||||
liveWorktreeMismatch
|
||||
}: MockAgentOptions = {}): unknown {
|
||||
return {
|
||||
paneKey,
|
||||
tab: { id: tabId },
|
||||
agentType,
|
||||
rowSource,
|
||||
liveWorktreeMismatch,
|
||||
state,
|
||||
startedAt,
|
||||
entry: {
|
||||
@@ -895,4 +898,105 @@ describe('WorktreeCardAgents', () => {
|
||||
expect(markup).not.toContain('Child A')
|
||||
expect(markup).not.toContain('compact-agent-row')
|
||||
})
|
||||
|
||||
it('notes the destination worktree on a compact row without dropping its details', async () => {
|
||||
mockAgentActivityDisplayMode = 'compact'
|
||||
mockAgents = [
|
||||
mockAgent({
|
||||
agentType: 'codex',
|
||||
rowSource: 'live',
|
||||
startedAt: 1000,
|
||||
prompt: 'Run tests',
|
||||
lastAssistantMessage: 'Inspecting changes',
|
||||
liveWorktreeMismatch: {
|
||||
destinationWorktreeId: 'wt-scratch',
|
||||
destinationLabel: 'scratch-fix'
|
||||
}
|
||||
})
|
||||
]
|
||||
const { default: WorktreeCardAgents } = await import('./WorktreeCardAgents')
|
||||
|
||||
const markup = renderToStaticMarkup(<WorktreeCardAgents worktreeId="wt-1" />)
|
||||
|
||||
expect(markup).toContain('data-agent-live-worktree-mismatch')
|
||||
expect(markup).toContain(' · in scratch-fix')
|
||||
expect(markup).toContain('<span class="text-muted-foreground/90">Run tests</span>')
|
||||
expect(markup).toContain('<span class="text-muted-foreground/65"> - Inspecting changes</span>')
|
||||
expect(markup).toContain('title="Run tests · in scratch-fix - Inspecting changes"')
|
||||
})
|
||||
|
||||
it('summarizes hidden panes running elsewhere on the collapsed compact affordance', async () => {
|
||||
mockAgentActivityDisplayMode = 'compact'
|
||||
mockAgents = [
|
||||
mockAgent({
|
||||
agentType: 'codex',
|
||||
rowSource: 'live',
|
||||
startedAt: 1000,
|
||||
prompt: 'One',
|
||||
liveWorktreeMismatch: {
|
||||
destinationWorktreeId: 'wt-scratch',
|
||||
destinationLabel: 'scratch-fix'
|
||||
}
|
||||
}),
|
||||
mockAgent({
|
||||
paneKey: 'tab-1:2',
|
||||
agentType: 'claude',
|
||||
rowSource: 'live',
|
||||
startedAt: 1500,
|
||||
stateStartedAt: 1500,
|
||||
prompt: 'Two',
|
||||
liveWorktreeMismatch: {
|
||||
destinationWorktreeId: 'wt-other',
|
||||
destinationLabel: 'other-fix'
|
||||
}
|
||||
}),
|
||||
mockAgent({
|
||||
paneKey: 'tab-1:3',
|
||||
agentType: 'gemini',
|
||||
rowSource: 'live',
|
||||
startedAt: 1700,
|
||||
stateStartedAt: 1700,
|
||||
prompt: 'Three'
|
||||
})
|
||||
]
|
||||
const { default: WorktreeCardAgents } = await import('./WorktreeCardAgents')
|
||||
|
||||
const markup = renderToStaticMarkup(<WorktreeCardAgents worktreeId="wt-1" />)
|
||||
|
||||
expect(markup).toContain('aria-expanded="false"')
|
||||
expect(markup).toContain('data-agent-live-worktree-mismatch-summary')
|
||||
expect(markup).toContain('2 elsewhere')
|
||||
expect(markup).toContain('2 agents in another worktree')
|
||||
expect(markup).not.toContain('3 agents in another worktree')
|
||||
})
|
||||
|
||||
it('uses the singular hidden-elsewhere phrasing for a single mismatched pane', async () => {
|
||||
mockAgentActivityDisplayMode = 'compact'
|
||||
mockAgents = [
|
||||
mockAgent({
|
||||
agentType: 'codex',
|
||||
rowSource: 'live',
|
||||
startedAt: 1000,
|
||||
prompt: 'One',
|
||||
liveWorktreeMismatch: {
|
||||
destinationWorktreeId: 'wt-scratch',
|
||||
destinationLabel: 'scratch-fix'
|
||||
}
|
||||
}),
|
||||
mockAgent({
|
||||
paneKey: 'tab-1:2',
|
||||
agentType: 'claude',
|
||||
rowSource: 'live',
|
||||
startedAt: 1500,
|
||||
stateStartedAt: 1500,
|
||||
prompt: 'Two'
|
||||
})
|
||||
]
|
||||
const { default: WorktreeCardAgents } = await import('./WorktreeCardAgents')
|
||||
|
||||
const markup = renderToStaticMarkup(<WorktreeCardAgents worktreeId="wt-1" />)
|
||||
|
||||
expect(markup).toContain('1 elsewhere')
|
||||
expect(markup).toContain('1 agent in another worktree')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -382,7 +382,14 @@ describe('fetchWorktrees', () => {
|
||||
mockApi.worktrees.listDetected.mockResolvedValueOnce(detected)
|
||||
store.setState({
|
||||
worktreesByRepo: { repo1: [existing] },
|
||||
detectedWorktreesByRepo: { repo1: detected },
|
||||
// Why: a committed refresh always carries the per-host authority record, so an
|
||||
// unchanged payload only stays unchanged when the seed carries it too.
|
||||
detectedWorktreesByRepo: {
|
||||
repo1: {
|
||||
...detected,
|
||||
authorityByHostId: { local: { authoritative: true, source: 'git' } }
|
||||
}
|
||||
},
|
||||
sortEpoch: 7
|
||||
} as Partial<AppState>)
|
||||
|
||||
@@ -981,7 +988,7 @@ describe('fetchWorktrees', () => {
|
||||
[makeWorktree({ id: 'repo-authority::/repo', repoId: 'repo-authority', path: '/repo' })],
|
||||
{ authoritative: true, source: 'git' }
|
||||
)
|
||||
mockApi.worktrees.listDetected.mockResolvedValue(detected)
|
||||
mockApi.worktrees.listDetected.mockResolvedValueOnce(detected).mockResolvedValueOnce(detected)
|
||||
await store.getState().fetchDetectedWorktrees('repo-authority')
|
||||
|
||||
const committed = store.getState().detectedWorktreesByRepo['repo-authority']!
|
||||
|
||||
Reference in New Issue
Block a user