fix(desktop): support keyless plugin rows
This commit is contained in:
parent
56dc01d904
commit
6e19c20d0a
|
|
@ -0,0 +1,88 @@
|
|||
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { requestGateway } = vi.hoisted(() => ({ requestGateway: vi.fn() }))
|
||||
|
||||
vi.mock('@/app/gateway/hooks/use-gateway-request', () => ({
|
||||
useGatewayRequest: () => ({ requestGateway })
|
||||
}))
|
||||
|
||||
import { $pluginRecords } from '@/contrib/plugins-store'
|
||||
import {
|
||||
$agentPluginBusy,
|
||||
$agentPlugins,
|
||||
$agentPluginsError,
|
||||
$agentPluginsStatus,
|
||||
type AgentPluginRow
|
||||
} from '@/store/agent-plugins'
|
||||
import { $connection, $gatewayState } from '@/store/session'
|
||||
|
||||
import { PluginsSettings } from './plugins-settings'
|
||||
|
||||
const legacyRow = {
|
||||
name: 'Legacy plugin',
|
||||
version: '0.20.0',
|
||||
description: 'Returned by a pre-key backend',
|
||||
source: 'user',
|
||||
status: 'disabled'
|
||||
} satisfies AgentPluginRow
|
||||
|
||||
beforeEach(() => {
|
||||
requestGateway.mockReset()
|
||||
$pluginRecords.set({})
|
||||
$agentPlugins.set([legacyRow])
|
||||
$agentPluginsStatus.set('ready')
|
||||
$agentPluginsError.set(null)
|
||||
$agentPluginBusy.set(null)
|
||||
$gatewayState.set('idle')
|
||||
$connection.set(null)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
describe('PluginsSettings', () => {
|
||||
it('renders and searches plugin rows returned without a canonical key', () => {
|
||||
render(<PluginsSettings />)
|
||||
|
||||
expect(screen.getByText('Legacy plugin')).toBeTruthy()
|
||||
|
||||
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'pre-key' } })
|
||||
|
||||
expect(screen.getByText('Legacy plugin')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('uses the legacy name address when toggling a row without a key', async () => {
|
||||
requestGateway.mockResolvedValue({ ok: true, plugin: { ...legacyRow, status: 'enabled' } })
|
||||
|
||||
render(<PluginsSettings />)
|
||||
fireEvent.click(screen.getByRole('switch', { name: 'Enable Legacy plugin' }))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(requestGateway).toHaveBeenCalledWith('plugins.manage', {
|
||||
action: 'toggle',
|
||||
name: 'Legacy plugin',
|
||||
enable: true
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps using the canonical key when the backend provides one', async () => {
|
||||
const keyedRow = { ...legacyRow, key: 'image_gen/legacy' }
|
||||
|
||||
$agentPlugins.set([keyedRow])
|
||||
requestGateway.mockResolvedValue({ ok: true, plugin: { ...keyedRow, status: 'enabled' } })
|
||||
|
||||
render(<PluginsSettings />)
|
||||
fireEvent.click(screen.getByRole('switch', { name: 'Enable Legacy plugin' }))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(requestGateway).toHaveBeenCalledWith('plugins.manage', {
|
||||
action: 'toggle',
|
||||
key: 'image_gen/legacy',
|
||||
enable: true
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
@ -39,7 +39,11 @@ const SOURCE_ORDER: Record<string, number> = { user: 0, git: 0, project: 1, entr
|
|||
// the user-facing control for any of them, so listing them here is noise.
|
||||
const HIDDEN_KEY_PREFIXES = ['dashboard_auth/', 'model-providers/', 'platforms/']
|
||||
|
||||
const isDesktopRelevant = (row: AgentPluginRow) => !HIDDEN_KEY_PREFIXES.some(prefix => row.key.startsWith(prefix))
|
||||
const isDesktopRelevant = (row: AgentPluginRow) => {
|
||||
const key = row.key
|
||||
|
||||
return !key || !HIDDEN_KEY_PREFIXES.some(prefix => key.startsWith(prefix))
|
||||
}
|
||||
|
||||
function reveal(file: string) {
|
||||
void window.hermesDesktop?.revealPath?.(file)?.catch(() => undefined)
|
||||
|
|
@ -134,10 +138,10 @@ function AgentPluginRowView({ row }: { row: AgentPluginRow }) {
|
|||
<Switch
|
||||
aria-label={`${row.status === 'enabled' ? p.disable : p.enable} ${row.name}`}
|
||||
checked={row.status === 'enabled'}
|
||||
disabled={busy === row.key}
|
||||
disabled={busy === (row.key ?? row.name)}
|
||||
onCheckedChange={on => {
|
||||
triggerHaptic('selection')
|
||||
void toggleAgentPlugin(requestGateway, row.key, on, p.agent.toggleFailed(row.name))
|
||||
void toggleAgentPlugin(requestGateway, row, on, p.agent.toggleFailed(row.name))
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
|
@ -180,7 +184,7 @@ function AgentPluginsSection() {
|
|||
row =>
|
||||
!needle ||
|
||||
row.name.toLowerCase().includes(needle) ||
|
||||
row.key.toLowerCase().includes(needle) ||
|
||||
(row.key ?? '').toLowerCase().includes(needle) ||
|
||||
row.description.toLowerCase().includes(needle)
|
||||
)
|
||||
.sort((a, b) => (SOURCE_ORDER[a.source] ?? 9) - (SOURCE_ORDER[b.source] ?? 9) || a.name.localeCompare(b.name))
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ import { notifyError } from '@/store/notifications'
|
|||
|
||||
export interface AgentPluginRow {
|
||||
name: string
|
||||
/** Canonical registry key (e.g. `image_gen/fal`) — names can collide. */
|
||||
key: string
|
||||
/** Canonical registry key (e.g. `image_gen/fal`) — absent on legacy backends. */
|
||||
key?: string
|
||||
version: string
|
||||
description: string
|
||||
/** 'bundled' | 'user' | 'git' | 'project' | 'entrypoint' */
|
||||
|
|
@ -36,7 +36,7 @@ export type GatewayRequest = <T>(method: string, params?: Record<string, unknown
|
|||
export const $agentPlugins = atom<AgentPluginRow[]>([])
|
||||
export const $agentPluginsStatus = atom<AgentPluginsStatus>('idle')
|
||||
export const $agentPluginsError = atom<string | null>(null)
|
||||
/** Key of the row whose toggle RPC is in flight (disables its switch). */
|
||||
/** Best available address of the row whose toggle RPC is in flight. */
|
||||
export const $agentPluginBusy = atom<string | null>(null)
|
||||
|
||||
let inflight: Promise<void> | null = null
|
||||
|
|
@ -70,20 +70,22 @@ export function loadAgentPlugins(request: GatewayRequest): Promise<void> {
|
|||
}
|
||||
|
||||
/** Flip a backend plugin on/off and patch the row from the RPC's refreshed
|
||||
* copy. Addressed by canonical key — bare names collide (image_gen/fal vs
|
||||
* video_gen/fal). Returns whether the toggle stuck. */
|
||||
* copy. Current backends use canonical keys; legacy keyless rows fall back
|
||||
* to the name-addressed protocol they were returned by. */
|
||||
export async function toggleAgentPlugin(
|
||||
request: GatewayRequest,
|
||||
key: string,
|
||||
row: Pick<AgentPluginRow, 'key' | 'name'>,
|
||||
enable: boolean,
|
||||
failMessage: string
|
||||
): Promise<boolean> {
|
||||
$agentPluginBusy.set(key)
|
||||
const address = row.key ?? row.name
|
||||
|
||||
$agentPluginBusy.set(address)
|
||||
|
||||
try {
|
||||
const result = await request<{ ok?: boolean; plugin?: AgentPluginRow | null }>('plugins.manage', {
|
||||
action: 'toggle',
|
||||
key,
|
||||
...(row.key ? { key: row.key } : { name: row.name }),
|
||||
enable
|
||||
})
|
||||
|
||||
|
|
@ -94,7 +96,11 @@ export async function toggleAgentPlugin(
|
|||
const refreshed = result.plugin
|
||||
|
||||
if (refreshed) {
|
||||
$agentPlugins.set($agentPlugins.get().map(row => (row.key === key ? { ...row, ...refreshed } : row)))
|
||||
$agentPlugins.set(
|
||||
$agentPlugins
|
||||
.get()
|
||||
.map(current => ((current.key ?? current.name) === address ? { ...current, ...refreshed } : current))
|
||||
)
|
||||
} else {
|
||||
await loadAgentPlugins(request)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue