fix(quick-commands): address remote host review

This commit is contained in:
Jinwoo-H
2026-08-07 19:07:11 -04:00
parent 8b903c74b6
commit eb61c59aff
9 changed files with 227 additions and 32 deletions
@@ -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
+20 -10
View File
@@ -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 }