mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 08:03:58 +00:00
fix(ui): stop the state dot from shadowing a row's disabled reason
The shared AgentStateDot now emits a native title on every state, so at any call site nested inside an element that already has a title, the dot's generic state word wins on hover over the more useful ancestor text. That regressed the sidebar agent row, which carries `sendTargetDisabledReason ?? rowTitle`: hovering the dot showed "Working" instead of the actionable send-target reason. Same guard the review-notes send menu already uses. Also covers three hunks that shipped untested: the Radix opt-outs in ActivityPrototypePage and the AI Vault subagent line's dropped wrapper title both stayed green when reverted, and the suppression test was a `not.toContain` sweep that passed against the pre-fix tree.
This commit is contained in:
@@ -127,7 +127,11 @@ describe('AgentStateDot', () => {
|
||||
React.createElement(AgentStateDot, { state: 'interrupted', title: null })
|
||||
)
|
||||
|
||||
expect(markup).not.toContain(' title=')
|
||||
expect(markup.match(/<span[^>]*\stitle="([^"]*)"/)?.[1]).toBe(undefined)
|
||||
expect(markup).toContain('aria-label="Interrupted"')
|
||||
// Differential: the same state still labels itself when the caller omits `title`.
|
||||
expect(renderMarkup('interrupted').match(/<span[^>]*\stitle="([^"]*)"/)?.[1]).toBe(
|
||||
agentStateLabel('interrupted')
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { createElement } from 'react'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { AgentStatusEntry } from '../../../../shared/agent-status-types'
|
||||
import { formatAgentTypeLabel } from '@/lib/agent-status'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
import {
|
||||
buildActivityThreadGroups,
|
||||
buildActivityEvents,
|
||||
buildAgentPaneThreads,
|
||||
getActivityThreadGroup
|
||||
getActivityThreadGroup,
|
||||
ThreadAgentStateIndicator
|
||||
} from './ActivityPrototypePage'
|
||||
import {
|
||||
makeActivityResult,
|
||||
@@ -23,6 +27,28 @@ import {
|
||||
UNKNOWN_PANE_KEY
|
||||
} from './ActivityPrototypePage-test-fixtures'
|
||||
|
||||
describe('ThreadAgentStateIndicator', () => {
|
||||
it('labels the state through its Radix tooltip only', () => {
|
||||
const threads = makeThreads(
|
||||
makeActivityResult({ entries: { [PANE_KEY]: makeWorkingEntryWithoutHistory() } })
|
||||
)
|
||||
|
||||
const markup = renderToStaticMarkup(
|
||||
createElement(
|
||||
TooltipProvider,
|
||||
null,
|
||||
createElement(ThreadAgentStateIndicator, { thread: threads[0]! })
|
||||
)
|
||||
)
|
||||
const titles = [...markup.matchAll(/\stitle="([^"]*)"/g)].map((match) => match[1])
|
||||
|
||||
expect(markup).toContain('data-slot="tooltip-trigger"')
|
||||
expect(markup).toContain('aria-label="Working"')
|
||||
// A native title here would fire alongside the Radix tooltip as a double label.
|
||||
expect(titles).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('activity thread grouping', () => {
|
||||
it('status grouping separates interrupted done from normal done and keeps Interrupted label', () => {
|
||||
const repo = makeRepo()
|
||||
|
||||
@@ -1180,7 +1180,11 @@ export function handleActivityFilterFocusShortcut({
|
||||
return true
|
||||
}
|
||||
|
||||
function ThreadAgentStateIndicator({ thread }: { thread: AgentPaneThread }): React.JSX.Element {
|
||||
export function ThreadAgentStateIndicator({
|
||||
thread
|
||||
}: {
|
||||
thread: AgentPaneThread
|
||||
}): React.JSX.Element {
|
||||
const state = threadAgentState(thread)
|
||||
const label = threadAgentStateLabel(thread)
|
||||
return (
|
||||
|
||||
@@ -86,6 +86,22 @@ describe('SessionSubagentsSection', () => {
|
||||
expect(queryByText('First pass')).toBeNull()
|
||||
})
|
||||
|
||||
it('labels the subagent run state exactly once, on the dot itself', async () => {
|
||||
listSubagentSessions.mockResolvedValueOnce({
|
||||
sessions: [makeSubagent('Running task')],
|
||||
issues: []
|
||||
})
|
||||
const { container } = render(<SessionSubagentsSection session={makeSession()} />)
|
||||
await act(async () => {})
|
||||
|
||||
const titles = [...container.querySelectorAll('[title]')].map((element) =>
|
||||
element.getAttribute('title')
|
||||
)
|
||||
|
||||
// A wrapper title would stack a second identical tooltip on the dot's own hit area.
|
||||
expect(titles).toEqual(['Working', 'Running task', 'View Log'])
|
||||
})
|
||||
|
||||
it('does not fetch for remote sessions even when the scan counted transcripts', async () => {
|
||||
const { container } = render(
|
||||
<SessionSubagentsSection
|
||||
|
||||
@@ -33,6 +33,10 @@ function monitoringAgent(): DashboardAgentRowData {
|
||||
}
|
||||
}
|
||||
|
||||
function orderedTitles(markup: string): string[] {
|
||||
return [...markup.matchAll(/\stitle="([^"]*)"/g)].map((match) => match[1])
|
||||
}
|
||||
|
||||
describe('worktree card agent summary', () => {
|
||||
it('presents passive working as monitoring', () => {
|
||||
const agent = monitoringAgent()
|
||||
@@ -56,6 +60,34 @@ describe('worktree card agent summary', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('hands the whole row to the send-target reason, and only then', () => {
|
||||
const agent = monitoringAgent()
|
||||
agent.entry.prompt = 'Run background checks'
|
||||
|
||||
const disabled = renderToStaticMarkup(
|
||||
createElement(CompactAgentRow, {
|
||||
agent,
|
||||
now: 2000,
|
||||
onActivate: vi.fn(),
|
||||
sendTargetStatus: 'disabled',
|
||||
sendTargetDisabledReason: 'Agent needs permission'
|
||||
})
|
||||
)
|
||||
|
||||
// The dot sits inside the row, so its own state title would shadow the reason on hover.
|
||||
expect(orderedTitles(disabled)).toEqual(['Agent needs permission', 'Claude'])
|
||||
|
||||
const eligible = renderToStaticMarkup(
|
||||
createElement(CompactAgentRow, { agent, now: 2000, onActivate: vi.fn() })
|
||||
)
|
||||
|
||||
expect(orderedTitles(eligible)).toEqual([
|
||||
'Monitoring background tasks - Run background checks',
|
||||
'Monitoring background tasks',
|
||||
'Claude'
|
||||
])
|
||||
})
|
||||
|
||||
it('lists interrupted outcomes before clean completions', () => {
|
||||
const done = monitoringAgent()
|
||||
done.state = 'done'
|
||||
|
||||
@@ -200,7 +200,12 @@ export const CompactAgentRow = React.memo(function CompactAgentRow({
|
||||
) : reserveDisclosureGutter ? (
|
||||
<span className="size-4 shrink-0" aria-hidden />
|
||||
) : null}
|
||||
<AgentStateDot state={dotState} size="sm" />
|
||||
{/* Why: the row's actionable disabled reason must win on every hit area. */}
|
||||
<AgentStateDot
|
||||
state={dotState}
|
||||
size="sm"
|
||||
title={sendTargetDisabledReason ? null : undefined}
|
||||
/>
|
||||
{!hideIcon && (
|
||||
<span className="inline-flex shrink-0" title={formatAgentTypeLabel(agent.agentType)}>
|
||||
<AgentIcon agent={agentTypeToIconAgent(agent.agentType)} size={13} />
|
||||
|
||||
Reference in New Issue
Block a user