diff --git a/src/renderer/src/components/native-chat/NativeChatAutocompleteMenus.test.tsx b/src/renderer/src/components/native-chat/NativeChatAutocompleteMenus.test.tsx
index 74476af1fb2..d5dac1efab5 100644
--- a/src/renderer/src/components/native-chat/NativeChatAutocompleteMenus.test.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatAutocompleteMenus.test.tsx
@@ -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(
+
+ )
+
+ 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(
@@ -27,6 +28,7 @@ type NativeChatSkillTab = { id: string; startupCwd?: string }
type NativeChatSkillWorktreeState = {
tabsByWorktree: Record
+ unifiedTabsByWorktree?: Record
worktreesByRepo: Record
}
@@ -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,
+ tabId: string
+): { worktreeId: string; tab: NativeChatSkillTab } | null {
+ return (
+ findTerminalTab(state.tabsByWorktree, tabId) ??
+ findTerminalTab(state.unifiedTabsByWorktree ?? {}, tabId)
+ )
+}
+
function findTerminalTab(
tabsByWorktree: Record,
terminalTabId: string
diff --git a/src/renderer/src/components/native-chat/use-native-chat-skills.react.test.tsx b/src/renderer/src/components/native-chat/use-native-chat-skills.react.test.tsx
index a436ba3c7ad..f360394cf37 100644
--- a/src/renderer/src/components/native-chat/use-native-chat-skills.react.test.tsx
+++ b/src/renderer/src/components/native-chat/use-native-chat-skills.react.test.tsx
@@ -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()
+
+ 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()
+
+ 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()
+
+ expect(mocks.snapshots.at(-1)?.status).toBe('error')
+ expect(mocks.callRuntimeRpc).not.toHaveBeenCalled()
+ })
+
it('shares one in-flight request between sibling panes', async () => {
render(
<>
diff --git a/src/renderer/src/components/native-chat/use-native-chat-skills.test.ts b/src/renderer/src/components/native-chat/use-native-chat-skills.test.ts
index 6e5daaf43db..9baf747673a 100644
--- a/src/renderer/src/components/native-chat/use-native-chat-skills.test.ts
+++ b/src/renderer/src/components/native-chat/use-native-chat-skills.test.ts
@@ -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')
diff --git a/src/renderer/src/components/native-chat/use-native-chat-skills.ts b/src/renderer/src/components/native-chat/use-native-chat-skills.ts
index f8a7e943935..42adc911656 100644
--- a/src/renderer/src/components/native-chat/use-native-chat-skills.ts
+++ b/src/renderer/src/components/native-chat/use-native-chat-skills.ts
@@ -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>()
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(() => {