mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 08:03:37 +00:00
fix(native-chat): load skills in structured sessions
This commit is contained in:
@@ -89,6 +89,24 @@ describe('NativeChatPickerMenu', () => {
|
||||
expect(screen.getAllByText('Loading skills...')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('renders a retryable error instead of the loading spinner when discovery fails', () => {
|
||||
const onRetry = vi.fn()
|
||||
render(
|
||||
<NativeChatPickerMenu
|
||||
autocomplete={autocomplete({ items: [], skillStatus: 'error', skillErrorKind: 'host' })}
|
||||
activeIndex={0}
|
||||
listboxId="picker"
|
||||
onChoose={vi.fn()}
|
||||
onRetry={onRetry}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(screen.getAllByText('Could not load skills from this host')).toHaveLength(2)
|
||||
expect(screen.queryByText('Loading skills...')).toBeNull()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Retry' }))
|
||||
expect(onRetry).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('uses command-only empty copy for a picker without skill support', () => {
|
||||
render(
|
||||
<NativeChatPickerMenu
|
||||
|
||||
@@ -20,6 +20,7 @@ export type NativeChatSkillStateInputs = Pick<
|
||||
| 'restoredRuntimeHostIdByWorkspaceSessionKey'
|
||||
| 'settings'
|
||||
| 'tabsByWorktree'
|
||||
| 'unifiedTabsByWorktree'
|
||||
| 'worktreesByRepo'
|
||||
>
|
||||
|
||||
@@ -27,6 +28,7 @@ type NativeChatSkillTab = { id: string; startupCwd?: string }
|
||||
|
||||
type NativeChatSkillWorktreeState = {
|
||||
tabsByWorktree: Record<string, readonly NativeChatSkillTab[]>
|
||||
unifiedTabsByWorktree?: Record<string, readonly { id: string }[]>
|
||||
worktreesByRepo: Record<string, readonly { id: string; path: string }[]>
|
||||
}
|
||||
|
||||
@@ -49,6 +51,7 @@ export function selectNativeChatSkillStateInputs(state: AppState): NativeChatSki
|
||||
restoredRuntimeHostIdByWorkspaceSessionKey: state.restoredRuntimeHostIdByWorkspaceSessionKey,
|
||||
settings: state.settings,
|
||||
tabsByWorktree: state.tabsByWorktree,
|
||||
unifiedTabsByWorktree: state.unifiedTabsByWorktree,
|
||||
worktreesByRepo: state.worktreesByRepo
|
||||
}
|
||||
}
|
||||
@@ -57,7 +60,7 @@ export function resolveNativeChatSkillDiscoveryCwd(
|
||||
state: NativeChatSkillWorktreeState,
|
||||
terminalTabId: string
|
||||
): string | null {
|
||||
const found = findTerminalTab(state.tabsByWorktree, terminalTabId)
|
||||
const found = findNativeChatTab(state, terminalTabId)
|
||||
if (!found) {
|
||||
return null
|
||||
}
|
||||
@@ -80,7 +83,7 @@ export function resolveNativeChatSkillDiscoveryContext(
|
||||
state: NativeChatSkillStateInputs,
|
||||
terminalTabId: string
|
||||
): NativeChatSkillDiscoveryContext | null {
|
||||
const worktreeId = findTerminalTab(state.tabsByWorktree, terminalTabId)?.worktreeId ?? null
|
||||
const worktreeId = findNativeChatTab(state, terminalTabId)?.worktreeId ?? null
|
||||
if (!worktreeId) {
|
||||
return null
|
||||
}
|
||||
@@ -142,6 +145,16 @@ export function resolveNativeChatSkillDiscoveryContext(
|
||||
}
|
||||
}
|
||||
|
||||
function findNativeChatTab(
|
||||
state: Pick<NativeChatSkillWorktreeState, 'tabsByWorktree' | 'unifiedTabsByWorktree'>,
|
||||
tabId: string
|
||||
): { worktreeId: string; tab: NativeChatSkillTab } | null {
|
||||
return (
|
||||
findTerminalTab(state.tabsByWorktree, tabId) ??
|
||||
findTerminalTab(state.unifiedTabsByWorktree ?? {}, tabId)
|
||||
)
|
||||
}
|
||||
|
||||
function findTerminalTab(
|
||||
tabsByWorktree: Record<string, readonly NativeChatSkillTab[]>,
|
||||
terminalTabId: string
|
||||
|
||||
@@ -44,6 +44,7 @@ function stateForHost(hostId: string) {
|
||||
restoredRuntimeHostIdByWorkspaceSessionKey: {},
|
||||
settings: { activeRuntimeEnvironmentId: null },
|
||||
tabsByWorktree: { 'worktree-1': [{ id: 'tab-1' }] },
|
||||
unifiedTabsByWorktree: {},
|
||||
worktreesByRepo: {
|
||||
'repo-1': [{ id: 'worktree-1', repoId: 'repo-1', path: '/repo/worktree', hostId }]
|
||||
}
|
||||
@@ -111,6 +112,46 @@ describe('useNativeChatSkills', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('resolves the catalog for a structured session tab', async () => {
|
||||
mocks.state = {
|
||||
...stateForHost('local'),
|
||||
tabsByWorktree: {},
|
||||
unifiedTabsByWorktree: {
|
||||
'worktree-1': [{ id: 'tab-1', contentType: 'agent-session', entityId: 'session-1' }]
|
||||
}
|
||||
}
|
||||
render(<Probe enabled />)
|
||||
|
||||
await waitFor(() => expect(mocks.snapshots.at(-1)?.status).toBe('ready'))
|
||||
expect(mocks.snapshots.at(-1)?.skills.map((skill) => skill.name)).toEqual(['browser'])
|
||||
expect(mocks.callRuntimeRpc).toHaveBeenCalledWith(
|
||||
{ kind: 'local' },
|
||||
'skills.discover',
|
||||
{ cwd: '/repo/worktree', worktreeId: 'worktree-1' },
|
||||
{ timeoutMs: 10_000 }
|
||||
)
|
||||
})
|
||||
|
||||
it('surfaces discovery failure instead of remaining loading', async () => {
|
||||
mocks.callRuntimeRpc.mockRejectedValueOnce(new Error('scan failed'))
|
||||
render(<Probe enabled />)
|
||||
|
||||
await waitFor(() => expect(mocks.snapshots.at(-1)?.status).toBe('error'))
|
||||
expect(mocks.snapshots.at(-1)?.error?.message).toBe('scan failed')
|
||||
})
|
||||
|
||||
it('surfaces missing tab ownership instead of remaining loading', () => {
|
||||
mocks.state = {
|
||||
...stateForHost('local'),
|
||||
tabsByWorktree: {},
|
||||
unifiedTabsByWorktree: {}
|
||||
}
|
||||
render(<Probe enabled />)
|
||||
|
||||
expect(mocks.snapshots.at(-1)?.status).toBe('error')
|
||||
expect(mocks.callRuntimeRpc).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shares one in-flight request between sibling panes', async () => {
|
||||
render(
|
||||
<>
|
||||
|
||||
@@ -132,6 +132,23 @@ describe('resolveNativeChatSkillDiscoveryCwd', () => {
|
||||
).toBe('/repo/worktree')
|
||||
})
|
||||
|
||||
it('returns the owning worktree path for a structured session tab', () => {
|
||||
expect(
|
||||
resolveNativeChatSkillDiscoveryCwd(
|
||||
{
|
||||
tabsByWorktree: {},
|
||||
unifiedTabsByWorktree: {
|
||||
'repo-1::/repo/worktree': [{ id: 'structured-tab-1' }]
|
||||
},
|
||||
worktreesByRepo: {
|
||||
'repo-1': [{ id: 'repo-1::/repo/worktree', path: '/repo/worktree' }]
|
||||
}
|
||||
},
|
||||
'structured-tab-1'
|
||||
)
|
||||
).toBe('/repo/worktree')
|
||||
})
|
||||
|
||||
it('returns null when the tab has no known worktree owner', () => {
|
||||
expect(
|
||||
resolveNativeChatSkillDiscoveryCwd({ tabsByWorktree: {}, worktreesByRepo: {} }, 'tab-1')
|
||||
|
||||
@@ -43,6 +43,13 @@ const IDLE_STATE: StoredDiscoveryState = {
|
||||
error: null,
|
||||
contextKey: null
|
||||
}
|
||||
const MISSING_CONTEXT_STATE: StoredDiscoveryState = {
|
||||
status: 'error',
|
||||
skills: [],
|
||||
error: new Error('Skill discovery context is unavailable.'),
|
||||
errorKind: 'unknown',
|
||||
contextKey: null
|
||||
}
|
||||
const inFlightDiscovery = new Map<string, Promise<SkillDiscoveryResult>>()
|
||||
|
||||
export function isNativeChatSkillForAgent(
|
||||
@@ -175,11 +182,13 @@ export function useNativeChatSkills(
|
||||
|
||||
const effectiveState = useMemo(
|
||||
() =>
|
||||
!profile || !enabled || !context
|
||||
!profile || !enabled
|
||||
? IDLE_STATE
|
||||
: state.contextKey === context.key
|
||||
? state
|
||||
: { status: 'loading' as const, skills: [], error: null, contextKey: context.key },
|
||||
: !context
|
||||
? MISSING_CONTEXT_STATE
|
||||
: state.contextKey === context.key
|
||||
? state
|
||||
: { status: 'loading' as const, skills: [], error: null, contextKey: context.key },
|
||||
[context, enabled, profile, state]
|
||||
)
|
||||
const visibleSkills = useMemo(() => {
|
||||
|
||||
Reference in New Issue
Block a user