mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 18:17:36 +00:00
Add the rules that decide which editor tabs are the same file (phase 1b-1, unused) (#31272)
* feat(editor): unused editor-buffer identity key (phase 1b-1) Adds `editorBufferIdentityOf`, not consumed yet: [environment, host, pairing root, path key] with exact case, normalized Windows separators and drive letter, WSL alias folding only for a local Windows client, SSH keyed with its runtime environment, and a detached record id for retired hosts. Splits `resolveEditorModelOwner` out of the owner-key function; the owner key is unchanged. Co-Authored-By: Claude <noreply@anthropic.com> * fix(editor): editor-buffer identity keys can't falsely merge files (phase 1b-1 review) - A folded WSL alias is a tagged tuple, so a real `\\wsl\<distro>` share can't collide with it. - `//` is ambiguous between POSIX and UNC, so it folds only for a known-Windows (local) host; drive and `\\` paths still fold separators and the drive letter on any host. - A proven UNC path folds its server name, matching the Monaco URI authority today. - SSH identity includes the target's endpoint incarnation: a target id edited to point at another machine gets a new key, a reconnect keeps it; an unknown endpoint never shares. - Documents the live pairing resolution and per-event retirement ids. Co-Authored-By: Claude <noreply@anthropic.com> * fix(editor): identity takes the tab's captured SSH endpoint and a verified host path flavor - The SSH endpoint is the one the tab captured at open, passed in by the caller; a live lookup would match an old tab to whatever machine the target id names now. Missing means never share. - Remote hosts accept a verified path flavor (`windows` | `posix` | `unknown`), so a Windows host's `//SERVER/share` folds like `\\SERVER\share`, as the Monaco URI authority does today. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,12 @@ import type { WorktreeOperationRouteState } from '@/lib/worktree-operation-route
|
||||
import { resolveWorktreeOperationRoute } from '@/lib/worktree-operation-route'
|
||||
import { toRuntimeExecutionHostId, toSshExecutionHostId } from '../../../../shared/execution-host'
|
||||
|
||||
export function getEditorModelOwnerKey(file: OpenFile, state: WorktreeOperationRouteState): string {
|
||||
export type EditorModelOwner = { environmentId: string | null; hostId: string | null }
|
||||
|
||||
export function resolveEditorModelOwner(
|
||||
file: OpenFile,
|
||||
state: WorktreeOperationRouteState
|
||||
): EditorModelOwner {
|
||||
const captured = file.operationProvenance?.generation.route
|
||||
const route =
|
||||
captured ??
|
||||
@@ -28,6 +33,11 @@ export function getEditorModelOwnerKey(file: OpenFile, state: WorktreeOperationR
|
||||
: environmentId
|
||||
? toRuntimeExecutionHostId(environmentId)
|
||||
: null
|
||||
return { environmentId, hostId }
|
||||
}
|
||||
|
||||
export function getEditorModelOwnerKey(file: OpenFile, state: WorktreeOperationRouteState): string {
|
||||
const { environmentId, hostId } = resolveEditorModelOwner(file, state)
|
||||
if (hostId === 'local' && !environmentId) {
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { OpenFile } from '../types/open-file'
|
||||
import type { WorktreeOperationRoute } from '@/lib/worktree-operation-route'
|
||||
import {
|
||||
onRuntimeEnvironmentsRetired,
|
||||
replaceRuntimeEnvironmentRevisions
|
||||
} from '@/runtime/runtime-environment-revision'
|
||||
import {
|
||||
detachedEditorBufferId,
|
||||
editorBufferIdentityOf,
|
||||
editorBufferPathKey,
|
||||
isEditorBufferIdentityRetired,
|
||||
type EditorBufferHostFacts,
|
||||
type EditorBufferHostPathFlavor
|
||||
} from './editor-buffer-identity'
|
||||
|
||||
const LOCAL: WorktreeOperationRoute = { executionHostId: 'local', runtimeEnvironmentId: null }
|
||||
|
||||
function file(overrides: Partial<OpenFile> = {}, route?: WorktreeOperationRoute): OpenFile {
|
||||
return {
|
||||
id: 'a',
|
||||
filePath: '/repo/a.ts',
|
||||
relativePath: 'a.ts',
|
||||
worktreeId: 'wt',
|
||||
language: 'typescript',
|
||||
isDirty: false,
|
||||
mode: 'edit',
|
||||
...(route
|
||||
? {
|
||||
runtimeEnvironmentId: route.runtimeEnvironmentId,
|
||||
operationProvenance: {
|
||||
ownershipProjection: 'explicit' as const,
|
||||
generation: {
|
||||
route,
|
||||
runtimeConnectionGeneration: null,
|
||||
runtimePairingRevision: undefined,
|
||||
runtimeSshGeneration: null,
|
||||
nestedSshGeneration: null,
|
||||
directSshGeneration: null
|
||||
}
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
const HOST_A: EditorBufferHostFacts = { capturedSshEndpoint: 'host-a:22' }
|
||||
|
||||
function keyOf(tab: OpenFile, facts: EditorBufferHostFacts = HOST_A): string | undefined {
|
||||
return editorBufferIdentityOf(tab, {}, facts)?.key
|
||||
}
|
||||
|
||||
function pathKey(
|
||||
filePath: string,
|
||||
flavor: EditorBufferHostPathFlavor = 'unknown',
|
||||
foldWsl = false
|
||||
) {
|
||||
return JSON.stringify(editorBufferPathKey(filePath, flavor, foldWsl))
|
||||
}
|
||||
|
||||
function runtimeTab(id: string, pairingRevision?: number): OpenFile {
|
||||
const tab = file({ id, runtimeEnvironmentId: 'env' })
|
||||
return pairingRevision === undefined
|
||||
? tab
|
||||
: file(
|
||||
{
|
||||
id,
|
||||
operationProvenance: {
|
||||
ownershipProjection: 'explicit',
|
||||
generation: {
|
||||
route: { executionHostId: 'runtime:env', runtimeEnvironmentId: 'env' },
|
||||
runtimeConnectionGeneration: null,
|
||||
runtimePairingRevision: pairingRevision,
|
||||
runtimeSshGeneration: null,
|
||||
nestedSshGeneration: null,
|
||||
directSshGeneration: null
|
||||
}
|
||||
}
|
||||
},
|
||||
{ executionHostId: 'runtime:env', runtimeEnvironmentId: 'env' }
|
||||
)
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
replaceRuntimeEnvironmentRevisions([])
|
||||
})
|
||||
|
||||
describe('editorBufferPathKey', () => {
|
||||
it.each([
|
||||
['C:\\Repo\\A.ts', 'c:/Repo/A.ts'],
|
||||
['C:/Repo//A.ts', 'c:\\Repo\\A.ts'],
|
||||
['\\\\server\\share\\A.ts', '\\\\SERVER\\share\\A.ts']
|
||||
])('treats %s and %s as one file on any host', (left, right) => {
|
||||
expect(pathKey(left)).toBe(pathKey(right))
|
||||
})
|
||||
|
||||
it('folds the UNC server name of a forward-slash path on a Windows host only', () => {
|
||||
expect(pathKey('//SERVER/share/a.ts', 'windows')).toBe(
|
||||
pathKey('//server/share/a.ts', 'windows')
|
||||
)
|
||||
expect(pathKey('\\\\server\\share\\a.ts', 'windows')).toBe(
|
||||
pathKey('//SERVER/share/a.ts', 'windows')
|
||||
)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['C:/Repo/A.ts', 'C:/repo/a.ts'],
|
||||
['\\\\server\\share\\A.ts', '\\\\server\\share\\a.ts'],
|
||||
['\\\\server\\Share\\a.ts', '\\\\server\\share\\a.ts'],
|
||||
['//wsl$/Ubuntu/home/me/A.ts', '//wsl$/Ubuntu/home/me/a.ts'],
|
||||
['/repo/A.ts', '/repo/a.ts'],
|
||||
['/repo/a\\b.ts', '/repo/a/b.ts']
|
||||
])('never folds path case or POSIX backslashes: %s is not %s', (left, right) => {
|
||||
expect(pathKey(left, 'windows', true)).not.toBe(pathKey(right, 'windows', true))
|
||||
})
|
||||
|
||||
it('keeps an ambiguous `//` path exact unless the host is known Windows', () => {
|
||||
// Valid POSIX names on Linux or an SSH host: a backslash is part of the file name.
|
||||
expect(pathKey('//repo/a\\b.ts')).not.toBe(pathKey('//repo/a/b.ts'))
|
||||
expect(pathKey('//SERVER/share/a.ts')).not.toBe(pathKey('//server/share/a.ts'))
|
||||
expect(pathKey('//repo/a\\b.ts', 'windows')).toBe(pathKey('//repo/a/b.ts', 'windows'))
|
||||
})
|
||||
|
||||
it.each([
|
||||
['\\\\wsl$\\Ubuntu\\home\\me\\a.ts', '\\\\wsl.localhost\\ubuntu\\home\\me\\a.ts'],
|
||||
['//wsl$/Ubuntu/home/me/a.ts', '\\\\wsl.localhost\\Ubuntu\\home\\me\\a.ts']
|
||||
])('folds the WSL aliases %s and %s only when asked', (left, right) => {
|
||||
expect(pathKey(left, 'windows', true)).toBe(pathKey(right, 'windows', true))
|
||||
expect(pathKey(left, 'windows')).not.toBe(pathKey(right, 'windows'))
|
||||
})
|
||||
|
||||
it('never lets a folded WSL key equal a real share named wsl', () => {
|
||||
const folded = pathKey('\\\\wsl$\\Ubuntu\\home\\a.ts', 'windows', true)
|
||||
for (const real of [
|
||||
'\\\\wsl\\ubuntu\\home\\a.ts',
|
||||
'//wsl/ubuntu/home/a.ts',
|
||||
'\\\\wsl\\Ubuntu\\home\\a.ts'
|
||||
]) {
|
||||
expect(pathKey(real, 'windows', true)).not.toBe(folded)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('editorBufferIdentityOf', () => {
|
||||
it('gives one local file in two workspaces one key', () => {
|
||||
expect(keyOf(file({}, LOCAL))).toBe(keyOf(file({ id: 'b', worktreeId: 'wt-2' }, LOCAL)))
|
||||
})
|
||||
|
||||
it('folds WSL aliases for a local Windows client only', () => {
|
||||
const wslDollar = file({ filePath: '\\\\wsl$\\Ubuntu\\home\\a.ts' }, LOCAL)
|
||||
const wslLocalhost = file({ id: 'b', filePath: '\\\\wsl.localhost\\Ubuntu\\home\\a.ts' }, LOCAL)
|
||||
vi.stubGlobal('navigator', { userAgent: 'Mozilla/5.0 (Macintosh)' })
|
||||
expect(keyOf(wslDollar)).not.toBe(keyOf(wslLocalhost))
|
||||
|
||||
vi.stubGlobal('navigator', { userAgent: 'Mozilla/5.0 (Windows NT 10.0)' })
|
||||
expect(keyOf(wslDollar)).toBe(keyOf(wslLocalhost))
|
||||
const ssh: WorktreeOperationRoute = {
|
||||
executionHostId: 'ssh:target',
|
||||
runtimeEnvironmentId: null
|
||||
}
|
||||
expect(keyOf(file({ filePath: wslDollar.filePath }, ssh))).not.toBe(
|
||||
keyOf(file({ filePath: wslLocalhost.filePath }, ssh))
|
||||
)
|
||||
})
|
||||
|
||||
it('keys an SSH target by its runtime environment as well as its target id', () => {
|
||||
const direct = keyOf(file({ externalSshTargetId: 'target', runtimeEnvironmentId: null }))
|
||||
const nested = keyOf(file({ externalSshTargetId: 'target', runtimeEnvironmentId: 'env' }))
|
||||
const nestedElsewhere = keyOf(
|
||||
file({
|
||||
id: 'b',
|
||||
worktreeId: 'wt-2',
|
||||
externalSshTargetId: 'target',
|
||||
runtimeEnvironmentId: 'env'
|
||||
})
|
||||
)
|
||||
const otherRuntime = keyOf(
|
||||
file({ externalSshTargetId: 'target', runtimeEnvironmentId: 'env-2' })
|
||||
)
|
||||
|
||||
expect(direct).toBeDefined()
|
||||
expect(new Set([direct, nested, otherRuntime]).size).toBe(3)
|
||||
expect(nested).toBe(nestedElsewhere)
|
||||
})
|
||||
|
||||
it('keys SSH tabs by the endpoint each captured, never the target id alone', () => {
|
||||
const tab = (id: string) =>
|
||||
file({ id, externalSshTargetId: 'target', runtimeEnvironmentId: null })
|
||||
// The target id now points at B; the old tab still carries the endpoint it opened on.
|
||||
const openedOnA = keyOf(tab('a'), { capturedSshEndpoint: 'host-a:22' })
|
||||
const openedOnB = keyOf(tab('b'), { capturedSshEndpoint: 'host-b:22' })
|
||||
expect(openedOnA).toBeDefined()
|
||||
expect(openedOnA).not.toBe(openedOnB)
|
||||
// A reconnect to the same machine captures the same endpoint.
|
||||
expect(keyOf(tab('c'), { capturedSshEndpoint: 'host-a:22' })).toBe(openedOnA)
|
||||
})
|
||||
|
||||
it('never shares an SSH tab with no captured endpoint', () => {
|
||||
const tab = file({ externalSshTargetId: 'target', runtimeEnvironmentId: null })
|
||||
expect(keyOf(tab, {})).toBeUndefined()
|
||||
expect(keyOf(tab, { capturedSshEndpoint: '' })).toBeUndefined()
|
||||
})
|
||||
|
||||
it('folds a forward-slash UNC server only on a remote host verified as Windows', () => {
|
||||
const route: WorktreeOperationRoute = {
|
||||
executionHostId: 'runtime:env',
|
||||
runtimeEnvironmentId: 'env'
|
||||
}
|
||||
const upper = file({ filePath: '//SERVER/share/a.ts' }, route)
|
||||
const backslash = file({ id: 'b', filePath: '\\\\SERVER\\share\\a.ts' }, route)
|
||||
expect(keyOf(upper, { remotePathFlavor: 'windows' })).toBe(
|
||||
keyOf(backslash, { remotePathFlavor: 'windows' })
|
||||
)
|
||||
for (const remotePathFlavor of ['posix', 'unknown'] as const) {
|
||||
expect(keyOf(upper, { remotePathFlavor })).not.toBe(keyOf(backslash, { remotePathFlavor }))
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps a runtime host and the local machine apart for one path', () => {
|
||||
expect(keyOf(file({}, LOCAL))).not.toBe(keyOf(runtimeTab('b')))
|
||||
})
|
||||
|
||||
it('never shares a tab whose owner is unresolved, or a non-edit tab', () => {
|
||||
expect(editorBufferIdentityOf(file({ runtimeEnvironmentId: null }), {}, HOST_A)).toBeNull()
|
||||
expect(editorBufferIdentityOf(file({ mode: 'diff' }, LOCAL), {}, HOST_A)).toBeNull()
|
||||
})
|
||||
|
||||
it('joins a re-pair proven to be the same machine', () => {
|
||||
replaceRuntimeEnvironmentRevisions([{ id: 'env', createdAt: 0, pairingRevision: 1 }])
|
||||
replaceRuntimeEnvironmentRevisions([{ id: 'env', createdAt: 0, pairingRevision: 2 }], {
|
||||
retired: [],
|
||||
sameHost: [{ id: 'env', fromRevision: 1, toRevision: 2 }]
|
||||
})
|
||||
expect(keyOf(runtimeTab('old', 1))).toBe(keyOf(runtimeTab('new', 2)))
|
||||
expect(keyOf(runtimeTab('old', 1))).toBe(keyOf(runtimeTab('unpinned')))
|
||||
})
|
||||
|
||||
it('keeps a pairing apart from an unproven re-pair', () => {
|
||||
replaceRuntimeEnvironmentRevisions([{ id: 'env', createdAt: 0, pairingRevision: 1 }])
|
||||
replaceRuntimeEnvironmentRevisions([{ id: 'env', createdAt: 0, pairingRevision: 2 }], {
|
||||
retired: [],
|
||||
sameHost: [],
|
||||
pending: [{ id: 'env', fromRevision: 1 }]
|
||||
})
|
||||
expect(keyOf(runtimeTab('old', 1))).not.toBe(keyOf(runtimeTab('new', 2)))
|
||||
})
|
||||
|
||||
it('quarantines a retired host: its replacement with the same path stays apart', () => {
|
||||
replaceRuntimeEnvironmentRevisions([{ id: 'env', createdAt: 0, pairingRevision: 1 }])
|
||||
const old = runtimeTab('old')
|
||||
const local = editorBufferIdentityOf(file({}, LOCAL), {}, HOST_A)
|
||||
const quarantine = new Map<string, string>()
|
||||
// Retirement fires before the new revision is published, while the old key still stands.
|
||||
const stop = onRuntimeEnvironmentsRetired((retired) => {
|
||||
for (const [viewId, identity] of [
|
||||
['old', editorBufferIdentityOf(old, {}, HOST_A)],
|
||||
['local', local]
|
||||
] as const) {
|
||||
if (identity && isEditorBufferIdentityRetired(identity, retired)) {
|
||||
quarantine.set(viewId, detachedEditorBufferId(viewId))
|
||||
}
|
||||
}
|
||||
})
|
||||
replaceRuntimeEnvironmentRevisions([{ id: 'env', createdAt: 0, pairingRevision: 2 }], {
|
||||
retired: ['env'],
|
||||
sameHost: []
|
||||
})
|
||||
stop()
|
||||
|
||||
// An unpinned tab re-resolves to the replacement: its key alone can't tell the machines apart.
|
||||
const replacementKey = keyOf(runtimeTab('new'))
|
||||
expect(keyOf(old)).toBe(replacementKey)
|
||||
expect([...quarantine]).toEqual([['old', 'detached:old']])
|
||||
expect(quarantine.get('old')).not.toBe(replacementKey)
|
||||
expect(detachedEditorBufferId('old')).not.toBe(detachedEditorBufferId('new'))
|
||||
})
|
||||
|
||||
it('resolves an unpinned tab live, so its key moves once revisions load', () => {
|
||||
const unpinned = runtimeTab('a')
|
||||
const beforeLoad = keyOf(unpinned)
|
||||
replaceRuntimeEnvironmentRevisions([{ id: 'env', createdAt: 0, pairingRevision: 1 }])
|
||||
expect(keyOf(unpinned)).not.toBe(beforeLoad)
|
||||
expect(keyOf(unpinned)).toBe(keyOf(runtimeTab('pinned', 1)))
|
||||
})
|
||||
|
||||
it('keeps a pinned tab of a retired pairing apart from the replacement', () => {
|
||||
replaceRuntimeEnvironmentRevisions([{ id: 'env', createdAt: 0, pairingRevision: 1 }])
|
||||
replaceRuntimeEnvironmentRevisions([{ id: 'env', createdAt: 0, pairingRevision: 2 }], {
|
||||
retired: ['env'],
|
||||
sameHost: []
|
||||
})
|
||||
expect(keyOf(runtimeTab('old', 1))).not.toBe(keyOf(runtimeTab('new')))
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,135 @@
|
||||
import type { OpenFile } from '../types/open-file'
|
||||
import type { WorktreeOperationRouteState } from '@/lib/worktree-operation-route'
|
||||
import { resolveEditorModelOwner } from '@/components/editor/editor-model-owner'
|
||||
import { isLocalWindowsDesktopClient } from '@/lib/desktop-window-chrome'
|
||||
import { resolveContinuedRuntimeEnvironmentRevision } from '@/runtime/runtime-environment-revision'
|
||||
import { parseExecutionHostId } from '../../../../../../shared/execution-host'
|
||||
import { parseWslUncPath } from '../../../../../../shared/wsl-paths'
|
||||
import { normalizeRuntimePathSeparators } from '../../../../../../shared/cross-platform-path'
|
||||
|
||||
/** The physical file an edit tab shows. Renderer-local; the workspace is never part of it. */
|
||||
export type EditorBufferIdentity = {
|
||||
/** Environment, host, pairing root, SSH endpoint and path key: equal keys are one file. */
|
||||
key: string
|
||||
environmentId: string | null
|
||||
}
|
||||
|
||||
/** `windows` or `posix` only when verified for the host; `unknown` never folds an ambiguous path. */
|
||||
export type EditorBufferHostPathFlavor = 'windows' | 'posix' | 'unknown'
|
||||
|
||||
/** Facts about the tab's execution host, supplied per tab by the caller. */
|
||||
export type EditorBufferHostFacts = {
|
||||
/**
|
||||
* The SSH endpoint this tab captured when it opened. Never the target's current endpoint: a
|
||||
* target id can be edited in place to name another machine, and a live lookup would then match
|
||||
* an old tab to the new machine. Undefined for an SSH tab means it never shares.
|
||||
*/
|
||||
capturedSshEndpoint?: string
|
||||
/** Verified path flavor of a remote host. The local client's flavor is detected here. */
|
||||
remotePathFlavor?: EditorBufferHostPathFlavor
|
||||
}
|
||||
|
||||
/** A WSL key is a tuple, so no real path string can ever equal it. */
|
||||
export type EditorBufferPathKey = string | readonly ['wsl', string, string]
|
||||
|
||||
/**
|
||||
* Case is kept, because case sensitivity can differ per directory and a folded key would merge
|
||||
* distinct files. Why not `normalizeRuntimePathForComparison`: it lowercases whole Windows paths.
|
||||
*/
|
||||
export function editorBufferPathKey(
|
||||
filePath: string,
|
||||
flavor: EditorBufferHostPathFlavor,
|
||||
foldWslAliases = false
|
||||
): EditorBufferPathKey {
|
||||
const wsl = foldWslAliases ? parseWslUncPath(filePath) : null
|
||||
if (wsl) {
|
||||
// Distro names are case-insensitive; the Linux path under them is not.
|
||||
return ['wsl', wsl.distro.toLowerCase(), wsl.linuxPath]
|
||||
}
|
||||
// Absolute POSIX paths start with `/`, so a drive or `\\` prefix proves Windows. `//` does not.
|
||||
const drive = /^[A-Za-z]:[\\/]/.test(filePath)
|
||||
const unc = filePath.startsWith('\\\\') || (flavor === 'windows' && filePath.startsWith('//'))
|
||||
if (!drive && !unc) {
|
||||
return filePath
|
||||
}
|
||||
const slashed = normalizeRuntimePathSeparators(filePath)
|
||||
if (drive) {
|
||||
return `${slashed[0].toLowerCase()}${slashed.slice(1)}`
|
||||
}
|
||||
// The UNC server is a case-insensitive host name, as in the Monaco model URI's authority.
|
||||
const serverEnd = slashed.indexOf('/', 2)
|
||||
return serverEnd === -1
|
||||
? slashed.toLowerCase()
|
||||
: `${slashed.slice(0, serverEnd).toLowerCase()}${slashed.slice(serverEnd)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Null for tabs that never share: non-edit views, unresolved owners, and SSH tabs with no captured
|
||||
* endpoint.
|
||||
*
|
||||
* The key is not fixed for a live tab: an unpinned runtime tab resolves its pairing live (null
|
||||
* before the revision registry loads, the newest proven revision after). A caller must treat a key
|
||||
* change as a re-key or join of the existing record, never as a fresh record.
|
||||
*/
|
||||
export function editorBufferIdentityOf(
|
||||
file: OpenFile,
|
||||
state: WorktreeOperationRouteState,
|
||||
hostFacts: EditorBufferHostFacts
|
||||
): EditorBufferIdentity | null {
|
||||
if (file.mode !== 'edit') {
|
||||
return null
|
||||
}
|
||||
const { environmentId, hostId } = resolveEditorModelOwner(file, state)
|
||||
const host = parseExecutionHostId(hostId)
|
||||
if (!host) {
|
||||
return null
|
||||
}
|
||||
const sshEndpoint = host.kind === 'ssh' ? (hostFacts.capturedSshEndpoint ?? null) : null
|
||||
if (host.kind === 'ssh' && !sshEndpoint) {
|
||||
return null
|
||||
}
|
||||
// A same-host re-pair resolves to the revision it continues; an unproven one stays apart.
|
||||
const revisionRoot = environmentId
|
||||
? (resolveContinuedRuntimeEnvironmentRevision(
|
||||
environmentId,
|
||||
file.operationProvenance?.generation.runtimePairingRevision
|
||||
) ?? null)
|
||||
: null
|
||||
// `wsl$` and `wsl.localhost` name one distro only on the local Windows client; a paired web
|
||||
// client (including `orca serve` on Windows) keeps them apart, as today.
|
||||
const localWindows = host.kind === 'local' && !environmentId && isLocalWindowsDesktopClient()
|
||||
const flavor = localWindows
|
||||
? 'windows'
|
||||
: host.kind === 'local' && !environmentId
|
||||
? 'unknown'
|
||||
: (hostFacts.remotePathFlavor ?? 'unknown')
|
||||
return {
|
||||
key: JSON.stringify([
|
||||
environmentId,
|
||||
host.id,
|
||||
revisionRoot,
|
||||
sshEndpoint,
|
||||
editorBufferPathKey(file.filePath, flavor, localWindows)
|
||||
]),
|
||||
environmentId
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the identity's environment now names another machine. Pass the ids from one
|
||||
* `onRuntimeEnvironmentsRetired` event, not a long-lived list: a retired id can be re-paired.
|
||||
*/
|
||||
export function isEditorBufferIdentityRetired(
|
||||
identity: EditorBufferIdentity,
|
||||
retiredEnvironmentIds: readonly string[]
|
||||
): boolean {
|
||||
return identity.environmentId !== null && retiredEnvironmentIds.includes(identity.environmentId)
|
||||
}
|
||||
|
||||
/**
|
||||
* A record id no identity key can equal. Records on a retired environment move here and are never
|
||||
* merged into the replacement machine's records, even with an identical path and baseline.
|
||||
*/
|
||||
export function detachedEditorBufferId(viewId: string): string {
|
||||
return `detached:${viewId}`
|
||||
}
|
||||
Reference in New Issue
Block a user