mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 00:03:47 +00:00
Fix npm server connection actions
This commit is contained in:
@@ -154,7 +154,7 @@ render credential-bearing URLs as compact clickable actions:
|
||||
```text
|
||||
Orca server is ready
|
||||
|
||||
Connect
|
||||
Connect (click for options, or Cmd/Ctrl-click)
|
||||
Web browser Open web client
|
||||
Desktop app Add this host
|
||||
|
||||
@@ -163,7 +163,9 @@ Server details
|
||||
Pairing uses ws://100.64.1.20:6768
|
||||
```
|
||||
|
||||
Redirected output prints the full web client and pairing URLs.
|
||||
Redirected output prints the full web client and pairing URLs. In Orca terminals, the desktop
|
||||
action opens the Remote Orca Servers form with the access link prefilled; its action menu also
|
||||
provides a copy fallback.
|
||||
|
||||
For supervisors, request the versioned single-line JSON contract:
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ Supported interactive terminals render credential-bearing URLs as compact clicka
|
||||
```text
|
||||
Orca server is ready
|
||||
|
||||
Connect
|
||||
Connect (click for options, or Cmd/Ctrl-click)
|
||||
Web browser Open web client
|
||||
Desktop app Add this host
|
||||
|
||||
@@ -93,7 +93,8 @@ Server details
|
||||
```
|
||||
|
||||
Redirected output prints the full web client and pairing URLs. `--json` remains the stable
|
||||
machine-readable contract.
|
||||
machine-readable contract. In Orca terminals, the desktop action opens the Remote Orca Servers
|
||||
form with the access link prefilled; its action menu also provides a copy fallback.
|
||||
|
||||
### Automation
|
||||
|
||||
|
||||
@@ -61,7 +61,9 @@ describe('ServeReadinessPublisher', () => {
|
||||
await publisher.publish(ready, { mode: 'human' })
|
||||
|
||||
const output = write.mock.calls[0]?.[0] ?? ''
|
||||
expect(output).toContain('Orca server is ready\n\nConnect')
|
||||
expect(output).toContain(
|
||||
'Orca server is ready\n\nConnect (click for options, or Cmd/Ctrl-click)'
|
||||
)
|
||||
expect(output).toContain(
|
||||
'\u001B]8;;https://orca.example.test/runtime/web-index.html#pairing=secret\u001B\\Open web client\u001B]8;;\u001B\\'
|
||||
)
|
||||
|
||||
@@ -110,7 +110,7 @@ function renderHumanReadiness(
|
||||
|
||||
const lines = ['Orca server is ready', '']
|
||||
if (readiness.pairing.available) {
|
||||
lines.push('Connect')
|
||||
lines.push('Connect (click for options, or Cmd/Ctrl-click)')
|
||||
if (readiness.pairing.webClientUrl) {
|
||||
lines.push(
|
||||
` Web browser ${terminalHyperlink('Open web client', readiness.pairing.webClientUrl)}`
|
||||
|
||||
@@ -71,7 +71,7 @@ type RuntimeEnvironmentsPaneProps = {
|
||||
setActiveRuntimeEnvironmentPreference: (environmentId: string | null) => Promise<boolean>
|
||||
canGeneratePairingUrl?: boolean
|
||||
allowLocalRuntime?: boolean
|
||||
addServerIntentSignal?: number
|
||||
addServerIntent?: { signal: number; accessLink?: string }
|
||||
}
|
||||
|
||||
export type RuntimeHostDetails = {
|
||||
@@ -263,7 +263,7 @@ export function RuntimeEnvironmentsPane({
|
||||
setActiveRuntimeEnvironmentPreference,
|
||||
canGeneratePairingUrl = true,
|
||||
allowLocalRuntime = true,
|
||||
addServerIntentSignal
|
||||
addServerIntent
|
||||
}: RuntimeEnvironmentsPaneProps): React.JSX.Element {
|
||||
const [environments, setEnvironments] = useState<PublicKnownRuntimeEnvironment[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
@@ -294,6 +294,13 @@ export function RuntimeEnvironmentsPane({
|
||||
(state) => state.setRemoteServerUpdateDialogOpen
|
||||
)
|
||||
const consumedAddServerIntentSignalRef = useRef(0)
|
||||
if (addServerIntent && consumedAddServerIntentSignalRef.current !== addServerIntent.signal) {
|
||||
// Why: consume Settings deep links before paint so the pairing form never flashes empty.
|
||||
consumedAddServerIntentSignalRef.current = addServerIntent.signal
|
||||
setPairingCode(addServerIntent.accessLink ?? '')
|
||||
setAddServerFailure(null)
|
||||
setAddServerFormOpen(true)
|
||||
}
|
||||
const mountedRef = useMountedRef()
|
||||
const updateCheckHint = getUpdateCheckHint()
|
||||
const activeValue =
|
||||
@@ -430,19 +437,6 @@ export function RuntimeEnvironmentsPane({
|
||||
useEffect(() => {
|
||||
void refreshRemoteServerUpdates()
|
||||
}, [environmentIdsKey, refreshRemoteServerUpdates])
|
||||
useEffect(() => {
|
||||
if (
|
||||
!addServerIntentSignal ||
|
||||
consumedAddServerIntentSignalRef.current === addServerIntentSignal
|
||||
) {
|
||||
return
|
||||
}
|
||||
consumedAddServerIntentSignalRef.current = addServerIntentSignal
|
||||
// Why: composer deep-links should land on the existing pairing form, not just
|
||||
// the server list.
|
||||
setAddServerFormOpen(true)
|
||||
}, [addServerIntentSignal])
|
||||
|
||||
const closeAddServerForm = (): void => {
|
||||
if (isSaving) {
|
||||
return
|
||||
|
||||
@@ -381,7 +381,10 @@ function Settings(): React.JSX.Element {
|
||||
)
|
||||
const [quickCommandAddIntentSignal, setQuickCommandAddIntentSignal] = useState(0)
|
||||
const [sshHostAddIntentSignal, setSshHostAddIntentSignal] = useState(0)
|
||||
const [remoteServerAddIntentSignal, setRemoteServerAddIntentSignal] = useState(0)
|
||||
const [remoteServerAddIntent, setRemoteServerAddIntent] = useState<{
|
||||
signal: number
|
||||
accessLink?: string
|
||||
}>({ signal: 0 })
|
||||
const [hasUnsavedCommitPromptChanges, setHasUnsavedCommitPromptChanges] = useState(false)
|
||||
const [hasUnsavedBranchPromptChanges, setHasUnsavedBranchPromptChanges] = useState(false)
|
||||
const [sourceControlAiPromptDiscardSignal, setSourceControlAiPromptDiscardSignal] = useState(0)
|
||||
@@ -696,7 +699,12 @@ function Settings(): React.JSX.Element {
|
||||
} else if (settingsNavigationTarget.intent === 'add-ssh-host') {
|
||||
setSshHostAddIntentSignal((signal) => signal + 1)
|
||||
} else if (settingsNavigationTarget.intent === 'add-remote-orca-server') {
|
||||
setRemoteServerAddIntentSignal((signal) => signal + 1)
|
||||
setRemoteServerAddIntent((current) => ({
|
||||
signal: current.signal + 1,
|
||||
...(settingsNavigationTarget.accessLink
|
||||
? { accessLink: settingsNavigationTarget.accessLink }
|
||||
: {})
|
||||
}))
|
||||
}
|
||||
setMountedSectionIds((previous) => {
|
||||
if (previous.has(paneSectionId)) {
|
||||
@@ -1728,7 +1736,7 @@ function Settings(): React.JSX.Element {
|
||||
setActiveRuntimeEnvironmentPreference={setActiveRuntimeEnvironmentPreference}
|
||||
canGeneratePairingUrl={!isWebClient}
|
||||
allowLocalRuntime={!isWebClient}
|
||||
addServerIntentSignal={remoteServerAddIntentSignal}
|
||||
addServerIntent={remoteServerAddIntent}
|
||||
/>
|
||||
) : null}
|
||||
</SettingsSection>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from './terminal-link-action-request'
|
||||
import { handleOscLink } from './terminal-osc-link-routing'
|
||||
import { handleTerminalHttpLink } from './terminal-url-link-hit-testing'
|
||||
import { encodePairingOffer } from '../../../../shared/pairing'
|
||||
|
||||
const openUrl = vi.fn()
|
||||
const createBrowserTab = vi.fn()
|
||||
@@ -250,4 +251,78 @@ describe('terminal link action routing', () => {
|
||||
).toBe(true)
|
||||
expect(request.mock.calls[0][0].destination).toBe(destination)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['macOS', 'Macintosh', { metaKey: true, ctrlKey: false }],
|
||||
['Windows', 'Windows', { metaKey: false, ctrlKey: true }],
|
||||
['Linux', 'Linux', { metaKey: false, ctrlKey: true }]
|
||||
])('opens a valid pairing link with the platform modifier on %s', (_name, userAgent, keys) => {
|
||||
vi.stubGlobal('navigator', { userAgent })
|
||||
const openPairingLink = vi.fn(() => true)
|
||||
const accessLink = encodePairingOffer({
|
||||
v: 2,
|
||||
endpoint: 'ws://192.168.1.10:6768',
|
||||
deviceToken: 'device-token',
|
||||
publicKeyB64: 'public-key'
|
||||
})
|
||||
|
||||
expect(
|
||||
handleOscLink(
|
||||
accessLink,
|
||||
{ ...plainEvent(), ...keys },
|
||||
{
|
||||
worktreeId: 'wt-1',
|
||||
worktreePath: '/repo',
|
||||
openPairingLink
|
||||
}
|
||||
)
|
||||
).toBe(true)
|
||||
expect(openPairingLink).toHaveBeenCalledExactlyOnceWith(accessLink)
|
||||
})
|
||||
|
||||
it('offers add and copy actions for a plain pairing-link click', () => {
|
||||
const request = vi.fn()
|
||||
const openPairingLink = vi.fn(() => true)
|
||||
const accessLink = encodePairingOffer({
|
||||
v: 2,
|
||||
endpoint: 'wss://server.example.test/runtime',
|
||||
deviceToken: 'device-token',
|
||||
publicKeyB64: 'public-key'
|
||||
})
|
||||
|
||||
expect(
|
||||
handleOscLink(accessLink, plainEvent(), {
|
||||
worktreeId: 'wt-1',
|
||||
worktreePath: '/repo',
|
||||
linkActionContext: actionContext(request),
|
||||
openPairingLink
|
||||
})
|
||||
).toBe(true)
|
||||
expect(request).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
destination: accessLink,
|
||||
kind: 'url',
|
||||
primary: expect.objectContaining({ label: 'Add host' })
|
||||
})
|
||||
)
|
||||
request.mock.calls[0][0].primary.run()
|
||||
expect(openPairingLink).toHaveBeenCalledExactlyOnceWith(accessLink)
|
||||
})
|
||||
|
||||
it('rejects unrelated custom-protocol OSC links', () => {
|
||||
const openPairingLink = vi.fn(() => true)
|
||||
|
||||
expect(
|
||||
handleOscLink(
|
||||
'orca://unknown?code=secret',
|
||||
{ ...plainEvent(), metaKey: true },
|
||||
{
|
||||
worktreeId: 'wt-1',
|
||||
worktreePath: '/repo',
|
||||
openPairingLink
|
||||
}
|
||||
)
|
||||
).toBe(false)
|
||||
expect(openPairingLink).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -12,8 +12,13 @@ import {
|
||||
type TerminalLinkRoutingPreferenceRequester
|
||||
} from './terminal-url-link-hit-testing'
|
||||
import type { HttpLinkSourceOwner } from '@/lib/http-link-routing'
|
||||
import type { TerminalLinkActionContext } from './terminal-link-action-request'
|
||||
import {
|
||||
requestTerminalLinkAction,
|
||||
type TerminalLinkActionContext
|
||||
} from './terminal-link-action-request'
|
||||
import { handleTerminalFileLink } from './terminal-file-link-actions'
|
||||
import { isTerminalPairingLink, openTerminalPairingLink } from './terminal-pairing-link-actions'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
type TerminalLinkEvent = Pick<MouseEvent, 'metaKey' | 'ctrlKey'> &
|
||||
Partial<
|
||||
@@ -55,6 +60,7 @@ export function handleOscLink(
|
||||
requestOpenLinksInAppPreference?: TerminalLinkRoutingPreferenceRequester
|
||||
linkActionContext?: TerminalLinkActionContext | null
|
||||
actionDestinations?: TerminalHttpLinkActionDestinations
|
||||
openPairingLink?: (accessLink: string) => boolean
|
||||
}
|
||||
): boolean {
|
||||
if (!isDesktopOscLinkActivation(event)) {
|
||||
@@ -124,6 +130,31 @@ export function handleOscLink(
|
||||
)
|
||||
}
|
||||
|
||||
if (parsed.protocol === 'orca:' && isTerminalPairingLink(rawText)) {
|
||||
const openPairingLink = (): boolean => {
|
||||
return (deps.openPairingLink ?? openTerminalPairingLink)(rawText)
|
||||
}
|
||||
const runPairingLink = (): void => {
|
||||
openPairingLink()
|
||||
}
|
||||
if (isTerminalLinkDirectActivation(event)) {
|
||||
return finish(openPairingLink())
|
||||
}
|
||||
return finish(
|
||||
requestTerminalLinkAction(event as MouseEvent, deps.linkActionContext, {
|
||||
destination: rawText,
|
||||
kind: 'url',
|
||||
primary: {
|
||||
label: translate(
|
||||
'auto.components.terminal.pane.TerminalLinkActionPopover.addHost',
|
||||
'Add host'
|
||||
),
|
||||
run: runPairingLink
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (parsed.protocol === 'file:') {
|
||||
// Why: file:// URIs should open inside Orca, not via the OS default editor
|
||||
// (shell.openPath). We extract the path from the URI and route it through
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { encodePairingOffer } from '../../../../shared/pairing'
|
||||
import { openTerminalPairingLink } from './terminal-pairing-link-actions'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
openSettingsPage: vi.fn(),
|
||||
openSettingsTarget: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: {
|
||||
getState: () => mocks
|
||||
}
|
||||
}))
|
||||
|
||||
describe('openTerminalPairingLink', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('opens the remote server form with the access link prefilled', () => {
|
||||
const accessLink = encodePairingOffer({
|
||||
v: 2,
|
||||
endpoint: 'ws://192.168.1.10:6768',
|
||||
deviceToken: 'device-token',
|
||||
publicKeyB64: 'public-key'
|
||||
})
|
||||
|
||||
expect(openTerminalPairingLink(accessLink)).toBe(true)
|
||||
expect(mocks.openSettingsTarget).toHaveBeenCalledWith({
|
||||
pane: 'servers',
|
||||
repoId: null,
|
||||
intent: 'add-remote-orca-server',
|
||||
accessLink
|
||||
})
|
||||
expect(mocks.openSettingsPage).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('does not route malformed or mobile-only pairing links', () => {
|
||||
const mobileLink = encodePairingOffer({
|
||||
v: 2,
|
||||
endpoint: 'ws://192.168.1.10:6768',
|
||||
deviceToken: 'device-token',
|
||||
publicKeyB64: 'public-key',
|
||||
scope: 'mobile'
|
||||
})
|
||||
|
||||
expect(openTerminalPairingLink('orca://unknown?code=secret')).toBe(false)
|
||||
expect(openTerminalPairingLink(mobileLink)).toBe(false)
|
||||
expect(mocks.openSettingsTarget).not.toHaveBeenCalled()
|
||||
expect(mocks.openSettingsPage).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
import { parseHostAccessLink } from '../../../../shared/remote-pairing-address'
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
export function openTerminalPairingLink(accessLink: string): boolean {
|
||||
if (!isTerminalPairingLink(accessLink)) {
|
||||
return false
|
||||
}
|
||||
const { openSettingsPage, openSettingsTarget } = useAppStore.getState()
|
||||
openSettingsTarget({
|
||||
pane: 'servers',
|
||||
repoId: null,
|
||||
intent: 'add-remote-orca-server',
|
||||
accessLink
|
||||
})
|
||||
openSettingsPage()
|
||||
return true
|
||||
}
|
||||
|
||||
export function isTerminalPairingLink(accessLink: string): boolean {
|
||||
return parseHostAccessLink(accessLink).ok
|
||||
}
|
||||
@@ -2875,7 +2875,8 @@
|
||||
"copyLink": "Copy link",
|
||||
"copied": "Copied",
|
||||
"copiedLink": "Copied link",
|
||||
"copyLinkFailed": "Failed to copy link"
|
||||
"copyLinkFailed": "Failed to copy link",
|
||||
"addHost": "Add host"
|
||||
},
|
||||
"TerminalSessionStateSaveFailureDialog": {
|
||||
"6bee0c8f17": "Open Disk Space Analyzer",
|
||||
|
||||
@@ -68,6 +68,7 @@ export type SettingsNavigationTarget = {
|
||||
hostId?: ExecutionHostId
|
||||
sectionId?: string
|
||||
intent?: (typeof SETTINGS_NAV_INTENTS)[number]
|
||||
accessLink?: string
|
||||
}
|
||||
|
||||
export function isSettingsNavigationTarget(value: unknown): value is SettingsNavigationTarget {
|
||||
@@ -83,7 +84,9 @@ export function isSettingsNavigationTarget(value: unknown): value is SettingsNav
|
||||
(typeof target.hostId === 'string' && parseExecutionHostId(target.hostId) !== null)) &&
|
||||
(target.sectionId === undefined || typeof target.sectionId === 'string') &&
|
||||
(target.intent === undefined ||
|
||||
(typeof target.intent === 'string' && SETTINGS_NAV_INTENT_SET.has(target.intent)))
|
||||
(typeof target.intent === 'string' && SETTINGS_NAV_INTENT_SET.has(target.intent))) &&
|
||||
(target.accessLink === undefined ||
|
||||
(target.intent === 'add-remote-orca-server' && typeof target.accessLink === 'string'))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1985,6 +1985,30 @@ describe('createUISlice settings navigation', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('accepts an access-link prefill only for the remote server intent', () => {
|
||||
const store = createUIStore()
|
||||
const accessLink = 'orca://pair?code=secret'
|
||||
store.getState().openSettingsTarget({
|
||||
pane: 'servers',
|
||||
repoId: null,
|
||||
intent: 'add-remote-orca-server',
|
||||
accessLink
|
||||
})
|
||||
expect(store.getState().settingsNavigationTarget).toEqual({
|
||||
pane: 'servers',
|
||||
repoId: null,
|
||||
intent: 'add-remote-orca-server',
|
||||
accessLink
|
||||
})
|
||||
|
||||
const openSettingsTarget = store.getState().openSettingsTarget as unknown as (
|
||||
target: unknown
|
||||
) => void
|
||||
expect(() => openSettingsTarget({ pane: 'servers', repoId: null, accessLink })).toThrowError(
|
||||
'openSettingsTarget received an invalid navigation target'
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects malformed settings targets before storing them', () => {
|
||||
const store = createUIStore()
|
||||
const openSettingsTarget = store.getState().openSettingsTarget as unknown as (
|
||||
|
||||
Reference in New Issue
Block a user