mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 16:03:20 +00:00
fix(quick-commands): address remote host review
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { shouldOpenQuickCommandAddIntent } from './QuickCommandsPane'
|
||||
import {
|
||||
getAvailableQuickCommandHostId,
|
||||
shouldOpenQuickCommandAddIntent,
|
||||
shouldShowQuickCommandsRefreshError
|
||||
} from './QuickCommandsPane'
|
||||
|
||||
describe('QuickCommandsPane add-command intent', () => {
|
||||
it('opens the add flow once for each new intent signal', () => {
|
||||
@@ -10,3 +14,33 @@ describe('QuickCommandsPane add-command intent', () => {
|
||||
expect(shouldOpenQuickCommandAddIntent(2, 1)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('QuickCommandsPane host state', () => {
|
||||
it('falls back to the local host when the selected remote host disappears', () => {
|
||||
expect(
|
||||
getAvailableQuickCommandHostId('runtime:removed', [
|
||||
{ id: 'local' },
|
||||
{ id: 'runtime:available' }
|
||||
])
|
||||
).toBe('local')
|
||||
})
|
||||
|
||||
it('keeps the selected host while it remains available', () => {
|
||||
expect(
|
||||
getAvailableQuickCommandHostId('runtime:available', [
|
||||
{ id: 'local' },
|
||||
{ id: 'runtime:available' }
|
||||
])
|
||||
).toBe('runtime:available')
|
||||
})
|
||||
|
||||
it('surfaces refresh failures only when cached commands remain usable', () => {
|
||||
expect(shouldShowQuickCommandsRefreshError(true, { error: 'offline', ready: true })).toBe(true)
|
||||
expect(shouldShowQuickCommandsRefreshError(true, { error: 'offline', ready: false })).toBe(
|
||||
false
|
||||
)
|
||||
expect(shouldShowQuickCommandsRefreshError(false, { error: 'offline', ready: true })).toBe(
|
||||
false
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -46,6 +46,22 @@ export function shouldOpenQuickCommandAddIntent(
|
||||
return Boolean(addCommandIntentSignal && consumedAddIntentSignal !== addCommandIntentSignal)
|
||||
}
|
||||
|
||||
export function getAvailableQuickCommandHostId(
|
||||
selectedHostId: ExecutionHostId,
|
||||
hostOptions: readonly { id: ExecutionHostId }[]
|
||||
): ExecutionHostId {
|
||||
return hostOptions.some((host) => host.id === selectedHostId)
|
||||
? selectedHostId
|
||||
: LOCAL_EXECUTION_HOST_ID
|
||||
}
|
||||
|
||||
export function shouldShowQuickCommandsRefreshError(
|
||||
commandsAreCurrent: boolean,
|
||||
runtimeCommands: { error: string | null; ready: boolean } | undefined
|
||||
): boolean {
|
||||
return commandsAreCurrent && runtimeCommands?.ready === true && Boolean(runtimeCommands.error)
|
||||
}
|
||||
|
||||
export function QuickCommandsPane({
|
||||
settings,
|
||||
addCommandIntentSignal
|
||||
@@ -97,6 +113,12 @@ export function QuickCommandsPane({
|
||||
const [scopeSelection, setScopeSelection] = useState<ReadonlySet<string> | null>(null)
|
||||
const [scopePopoverOpen, setScopePopoverOpen] = useState(false)
|
||||
|
||||
const availableHostId = getAvailableQuickCommandHostId(selectedHostId, hostOptions)
|
||||
if (availableHostId !== selectedHostId) {
|
||||
setSelectedHostId(availableHostId)
|
||||
setScopeSelection(null)
|
||||
}
|
||||
|
||||
const hostRepos = useMemo(
|
||||
() =>
|
||||
selectedEnvironmentId
|
||||
@@ -227,7 +249,9 @@ export function QuickCommandsPane({
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>{translate('auto.components.settings.QuickCommandsPane.savedOn', 'Saved on')}</Label>
|
||||
<Label>
|
||||
{translate('auto.components.settings.QuickCommandsPane.89f7e57fcc', 'Saved on')}
|
||||
</Label>
|
||||
<Select
|
||||
value={selectedHostId}
|
||||
onValueChange={(value) => {
|
||||
@@ -261,7 +285,7 @@ export function QuickCommandsPane({
|
||||
{selectedRuntimeCommandsAreCurrent && selectedRuntimeCommands?.supported === false ? (
|
||||
<div className="px-3 py-6 text-sm text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.settings.QuickCommandsPane.unsupportedHost',
|
||||
'auto.components.settings.QuickCommandsPane.d59bd333c3',
|
||||
'Update this Orca server to manage its quick commands.'
|
||||
)}
|
||||
</div>
|
||||
@@ -271,7 +295,7 @@ export function QuickCommandsPane({
|
||||
<div className="flex items-center justify-between gap-3 rounded-md border border-border/60 px-3 py-3">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.settings.QuickCommandsPane.loadFailed',
|
||||
'auto.components.settings.QuickCommandsPane.f2bf411640',
|
||||
'Could not load commands from this host.'
|
||||
)}
|
||||
</span>
|
||||
@@ -284,23 +308,49 @@ export function QuickCommandsPane({
|
||||
void loadRuntimeCommands(selectedEnvironmentId, { force: true })
|
||||
}
|
||||
>
|
||||
{translate('auto.components.settings.QuickCommandsPane.retry', 'Retry')}
|
||||
{translate('auto.components.settings.QuickCommandsPane.7ecfee5b8e', 'Retry')}
|
||||
</Button>
|
||||
</div>
|
||||
) : selectedEnvironmentId &&
|
||||
(!selectedRuntimeCommandsAreCurrent ||
|
||||
(selectedRuntimeCommands?.loading && !selectedRuntimeCommands.ready)) ? (
|
||||
<div className="px-3 py-6 text-sm text-muted-foreground">
|
||||
{translate('auto.components.settings.QuickCommandsPane.loading', 'Loading commands…')}
|
||||
{translate('auto.components.settings.QuickCommandsPane.601d6af51f', 'Loading commands…')}
|
||||
</div>
|
||||
) : (
|
||||
<QuickCommandsList
|
||||
commands={commands}
|
||||
visibleCommands={visibleCommands}
|
||||
repoById={repoById}
|
||||
onEdit={(command) => setEditor({ mode: 'edit', command })}
|
||||
onRemove={(command) => void removeCommand(command)}
|
||||
/>
|
||||
<>
|
||||
{shouldShowQuickCommandsRefreshError(
|
||||
selectedRuntimeCommandsAreCurrent,
|
||||
selectedRuntimeCommands
|
||||
) ? (
|
||||
<div className="flex items-center justify-between gap-3 rounded-md border border-border/60 px-3 py-3">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.settings.QuickCommandsPane.923ba89646',
|
||||
'Could not refresh commands from this host. Showing the last loaded commands.'
|
||||
)}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
onClick={() =>
|
||||
selectedEnvironmentId &&
|
||||
void loadRuntimeCommands(selectedEnvironmentId, { force: true })
|
||||
}
|
||||
>
|
||||
{translate('auto.components.settings.QuickCommandsPane.7ecfee5b8e', 'Retry')}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
<QuickCommandsList
|
||||
commands={commands}
|
||||
visibleCommands={visibleCommands}
|
||||
repoById={repoById}
|
||||
onEdit={(command) => setEditor({ mode: 'edit', command })}
|
||||
onRemove={(command) => void removeCommand(command)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{editor !== null ? (
|
||||
|
||||
@@ -20,9 +20,12 @@ export function TabBarQuickCommandAddActions({
|
||||
>
|
||||
<Play className="size-3.5" />
|
||||
{hosts.length === 1
|
||||
? translate('auto.components.tab.bar.TabBarQuickCommandsButton.a2c7a33831', 'Command')
|
||||
? translate(
|
||||
'auto.components.tab.bar.TabBarQuickCommandAddActions.45a2f36d51',
|
||||
'Command'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.tab.bar.TabBarQuickCommandsButton.commandOnHost',
|
||||
'auto.components.tab.bar.TabBarQuickCommandAddActions.b856c833ae',
|
||||
'Command on {{value0}}',
|
||||
{ value0: host.label }
|
||||
)}
|
||||
|
||||
@@ -152,7 +152,9 @@ describe('TerminalContextMenu', () => {
|
||||
})
|
||||
|
||||
it('labels commands and add actions by their owning host', () => {
|
||||
const onAddQuickCommand = vi.fn()
|
||||
const rendered = renderMenu({
|
||||
onAddQuickCommand,
|
||||
quickCommandHosts: [
|
||||
{
|
||||
hostId: 'local',
|
||||
@@ -187,5 +189,14 @@ describe('TerminalContextMenu', () => {
|
||||
expect(rendered).toContain('Local Mac')
|
||||
expect(rendered).toContain('Build Server')
|
||||
expect(markup.match(/Add to \{\{value0\}\}…/g)).toHaveLength(2)
|
||||
|
||||
const addItems = items.list.filter(
|
||||
(item) => childrenText(item.children) === 'Add to {{value0}}…'
|
||||
)
|
||||
addItems.forEach((item) => item.onSelect?.())
|
||||
expect(onAddQuickCommand.mock.calls.map(([hostId]) => hostId)).toEqual([
|
||||
'local',
|
||||
'runtime:build'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -258,7 +258,7 @@ export default function TerminalContextMenu({
|
||||
'Add Quick Command…'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.terminal.pane.TerminalContextMenu.addQuickCommandToHost',
|
||||
'auto.components.terminal.pane.TerminalContextMenu.15dd899676',
|
||||
'Add to {{value0}}…',
|
||||
{ value0: host.label }
|
||||
)}
|
||||
|
||||
@@ -1,10 +1,96 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { act, createElement } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getDefaultSettings } from '../../../shared/constants'
|
||||
import type { ExecutionHostId } from '../../../shared/execution-host'
|
||||
import type { GlobalSettings } from '../../../shared/types'
|
||||
import type { RuntimeTerminalQuickCommands } from '@/store/slices/terminal-quick-command-hosts'
|
||||
|
||||
const testState = vi.hoisted(() => ({
|
||||
executionHostId: 'runtime:build' as ExecutionHostId,
|
||||
loadRuntimeTerminalQuickCommands: vi.fn(async () => {}),
|
||||
runtimeEnvironments: [] as { id: string; name: string }[],
|
||||
runtimeStatusByEnvironmentId: new Map<string, { connectionGeneration?: number }>(),
|
||||
runtimeTerminalQuickCommands: new Map<string, RuntimeTerminalQuickCommands>(),
|
||||
settings: null as GlobalSettings | null
|
||||
}))
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: (selector: (state: typeof testState) => unknown) => selector(testState)
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/worktree-runtime-owner', () => ({
|
||||
getExecutionHostIdForWorktree: () => testState.executionHostId
|
||||
}))
|
||||
|
||||
import {
|
||||
flattenTerminalQuickCommandHosts,
|
||||
getTerminalQuickCommandHostOptions
|
||||
getTerminalQuickCommandHostOptions,
|
||||
useTerminalQuickCommandHosts,
|
||||
type TerminalQuickCommandHost
|
||||
} from './use-terminal-quick-command-hosts'
|
||||
|
||||
let renderedHosts: TerminalQuickCommandHost[] = []
|
||||
|
||||
function Probe(): null {
|
||||
renderedHosts = useTerminalQuickCommandHosts('worktree-1').hosts
|
||||
return null
|
||||
}
|
||||
|
||||
describe('useTerminalQuickCommandHosts', () => {
|
||||
let root: Root
|
||||
|
||||
beforeEach(() => {
|
||||
testState.executionHostId = 'runtime:build'
|
||||
testState.loadRuntimeTerminalQuickCommands.mockClear()
|
||||
testState.runtimeEnvironments = [{ id: 'build', name: 'Build Server' }]
|
||||
testState.runtimeStatusByEnvironmentId = new Map([['build', { connectionGeneration: 4 }]])
|
||||
testState.runtimeTerminalQuickCommands = new Map()
|
||||
testState.settings = getDefaultSettings('/tmp')
|
||||
renderedHosts = []
|
||||
const container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
root = createRoot(container)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
act(() => root.unmount())
|
||||
document.body.replaceChildren()
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ name: 'unsupported', supported: false, generation: 4, expected: ['local'] },
|
||||
{ name: 'stale generation', supported: true, generation: 3, expected: ['local'] },
|
||||
{
|
||||
name: 'supported current generation',
|
||||
supported: true,
|
||||
generation: 4,
|
||||
expected: ['local', 'runtime:build']
|
||||
}
|
||||
])('gates the remote host when it is $name', async ({ supported, generation, expected }) => {
|
||||
testState.runtimeTerminalQuickCommands = new Map([
|
||||
[
|
||||
'build',
|
||||
{
|
||||
commands: [],
|
||||
connectionGeneration: generation,
|
||||
error: null,
|
||||
loading: false,
|
||||
ready: true,
|
||||
supported
|
||||
}
|
||||
]
|
||||
])
|
||||
|
||||
await act(async () => root.render(createElement(Probe)))
|
||||
|
||||
expect(renderedHosts.map((host) => host.hostId)).toEqual(expected)
|
||||
expect(testState.loadRuntimeTerminalQuickCommands).toHaveBeenCalledWith('build')
|
||||
})
|
||||
})
|
||||
|
||||
describe('flattenTerminalQuickCommandHosts', () => {
|
||||
it('keeps identical command ids distinct by owning host', () => {
|
||||
const command = {
|
||||
|
||||
@@ -79,6 +79,7 @@ export function useTerminalQuickCommandHosts(worktreeId: string): {
|
||||
})
|
||||
const loadRemote = useAppStore((state) => state.loadRuntimeTerminalQuickCommands)
|
||||
const parsedExecutionHost = parseExecutionHostId(executionHostId)
|
||||
const remoteHostId = parsedExecutionHost?.kind === 'runtime' ? parsedExecutionHost.id : null
|
||||
const remoteEnvironmentId =
|
||||
parsedExecutionHost?.kind === 'runtime' ? parsedExecutionHost.environmentId : null
|
||||
const remoteConnectionGeneration = useAppStore((state) =>
|
||||
@@ -104,13 +105,13 @@ export function useTerminalQuickCommandHosts(worktreeId: string): {
|
||||
}
|
||||
]
|
||||
if (
|
||||
!remoteHostId ||
|
||||
!remoteEnvironmentId ||
|
||||
remoteState?.supported !== true ||
|
||||
remoteState.connectionGeneration !== remoteConnectionGeneration
|
||||
) {
|
||||
return result
|
||||
}
|
||||
const remoteHostId = executionHostId as `runtime:${string}`
|
||||
result.push({
|
||||
commands: remoteState.commands,
|
||||
hostId: remoteHostId,
|
||||
@@ -118,9 +119,9 @@ export function useTerminalQuickCommandHosts(worktreeId: string): {
|
||||
})
|
||||
return result
|
||||
}, [
|
||||
executionHostId,
|
||||
remoteConnectionGeneration,
|
||||
remoteEnvironmentId,
|
||||
remoteHostId,
|
||||
remoteState,
|
||||
runtimeEnvironments,
|
||||
settings
|
||||
|
||||
@@ -371,8 +371,14 @@
|
||||
"tryAgain": "Try again"
|
||||
}
|
||||
},
|
||||
"terminalQuickCommandHosts": {
|
||||
"saveFailed": "Failed to save quick command"
|
||||
"terminal": {
|
||||
"quick": {
|
||||
"command": {
|
||||
"hosts": {
|
||||
"5b7d781d67": "Failed to save quick command"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2809,7 +2815,7 @@
|
||||
"925f49f210": "Expand Pane",
|
||||
"df766809e0": "Collapse Pane",
|
||||
"cff67afad1": "Copy Context",
|
||||
"addQuickCommandToHost": "Add to {{value0}}…"
|
||||
"15dd899676": "Add to {{value0}}…"
|
||||
},
|
||||
"TerminalErrorToast": {
|
||||
"e4aa243f8c": "Restart daemon",
|
||||
@@ -3099,8 +3105,7 @@
|
||||
"c781f992e4": "destructive",
|
||||
"be8f0ff166": "Delete",
|
||||
"f3a8c2d1e7": "Search quick commands...",
|
||||
"b4e7f9a2c1": "No commands match",
|
||||
"commandOnHost": "Command on {{value0}}"
|
||||
"b4e7f9a2c1": "No commands match"
|
||||
},
|
||||
"shell": {
|
||||
"icons": {
|
||||
@@ -3165,6 +3170,10 @@
|
||||
},
|
||||
"TerminalTabLeadingIcon": {
|
||||
"7ab2964bea": "Unread agent completion"
|
||||
},
|
||||
"TabBarQuickCommandAddActions": {
|
||||
"45a2f36d51": "Command",
|
||||
"b856c833ae": "Command on {{value0}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -6669,11 +6678,12 @@
|
||||
"44923dd982": "destructive",
|
||||
"ec1ed99e70": "Delete",
|
||||
"d1d0976320": "None",
|
||||
"savedOn": "Saved on",
|
||||
"unsupportedHost": "Update this Orca server to manage its quick commands.",
|
||||
"loadFailed": "Could not load commands from this host.",
|
||||
"retry": "Retry",
|
||||
"loading": "Loading commands…"
|
||||
"89f7e57fcc": "Saved on",
|
||||
"d59bd333c3": "Update this Orca server to manage its quick commands.",
|
||||
"f2bf411640": "Could not load commands from this host.",
|
||||
"7ecfee5b8e": "Retry",
|
||||
"601d6af51f": "Loading commands…",
|
||||
"923ba89646": "Could not refresh commands from this host. Showing the last loaded commands."
|
||||
},
|
||||
"RecentTabOrderControl": {
|
||||
"3b17c81ede": "Tab strip order",
|
||||
|
||||
@@ -73,7 +73,7 @@ async function mutateLocalCommands(
|
||||
const message = error instanceof Error ? error.message : 'Failed to save quick command.'
|
||||
toast.error(
|
||||
translate(
|
||||
'auto.store.slices.terminalQuickCommandHosts.saveFailed',
|
||||
'auto.store.slices.terminal.quick.command.hosts.5b7d781d67',
|
||||
'Failed to save quick command'
|
||||
),
|
||||
{ description: message }
|
||||
@@ -127,7 +127,7 @@ async function mutateRemoteCommands(
|
||||
}))
|
||||
toast.error(
|
||||
translate(
|
||||
'auto.store.slices.terminalQuickCommandHosts.saveFailed',
|
||||
'auto.store.slices.terminal.quick.command.hosts.5b7d781d67',
|
||||
'Failed to save quick command'
|
||||
),
|
||||
{ description: message }
|
||||
|
||||
Reference in New Issue
Block a user