diff --git a/src/renderer/src/store/slices/worktrees/host-scoped-row-merges.test.ts b/src/renderer/src/store/slices/worktrees/host-scoped-row-merges.test.ts index 3743a541c4b..da2ad0d1da5 100644 --- a/src/renderer/src/store/slices/worktrees/host-scoped-row-merges.test.ts +++ b/src/renderer/src/store/slices/worktrees/host-scoped-row-merges.test.ts @@ -89,4 +89,20 @@ describe('row writers touch only the copy on their own host', () => { hostId: 'runtime:hub' }) }) + + it("never appends a server's copy when only the local twin is listed", () => { + const store = createTestStore() + const { local } = seedTwins(store) + store.setState({ worktreesByRepo: { [REPO_ID]: [local] } }) + applyWorktreeLineageUpdate(store.setState, WORKTREE_ID, { + target: { kind: 'environment', environmentId: 'hub' }, + lineage: null, + updatedRemoteWorktree: makeWorktree({ + id: WORKTREE_ID, + repoId: REPO_ID, + displayName: 'server after lineage' + }) + }) + expect(store.getState().worktreesByRepo[REPO_ID]).toEqual([local]) + }) }) diff --git a/src/renderer/src/store/slices/worktrees/metadata/worktree-lineage-refresh.ts b/src/renderer/src/store/slices/worktrees/metadata/worktree-lineage-refresh.ts index 62a4eabc75e..dc309a6dbf3 100644 --- a/src/renderer/src/store/slices/worktrees/metadata/worktree-lineage-refresh.ts +++ b/src/renderer/src/store/slices/worktrees/metadata/worktree-lineage-refresh.ts @@ -189,18 +189,16 @@ export function applyWorktreeLineageUpdate( } else { delete next[worktreeId] } + // Why the transport's host: focus can name the local twin of a row a server just answered for. + const transportHostId = getRuntimeTargetHostId(result.target) const worktreesByRepo = result.target.kind === 'local' ? projectLocalWorktreeLineageUpdate(s.worktreesByRepo, worktreeId, result.lineage) : result.updatedRemoteWorktree - ? // Why the transport's host: focus can name the local twin of a row a server just answered for. - replaceWorktreeInRepoLists( + ? replaceWorktreeInRepoLists( s, - withRepoHostOwnership( - result.updatedRemoteWorktree, - getRuntimeTargetHostId(result.target) - ), - getRuntimeTargetHostId(result.target) + withRepoHostOwnership(result.updatedRemoteWorktree, transportHostId), + transportHostId ) : s.worktreesByRepo return { diff --git a/src/renderer/src/store/terminals/workspace-terminal-placeholders.ts b/src/renderer/src/store/terminals/workspace-terminal-placeholders.ts index 1a4ef660ace..17ceee080db 100644 --- a/src/renderer/src/store/terminals/workspace-terminal-placeholders.ts +++ b/src/renderer/src/store/terminals/workspace-terminal-placeholders.ts @@ -4,9 +4,9 @@ import { DEFAULT_REPO_BADGE_COLOR } from '../../../../shared/constants' import { getRepoExecutionHostId, parseExecutionHostId, - toSshExecutionHostId, type ExecutionHostId } from '../../../../shared/execution-host' +import { repoRowOwnerStamp } from './workspace-terminal-ssh-placeholders' import { parseWorkspaceKey } from '../../../../shared/workspace-scope' import { splitWorktreeIdForFilesystem } from '../../../../shared/worktree/id' @@ -16,22 +16,13 @@ export function getPathDisplayName(path: string, fallback: string): string { return basename || fallback } -/** - * A server's repo that nests an SSH target lists its worktrees as `ssh:` reached through - * that server; a bare `runtime:` stamp would hide the target from an `ssh:` host selection. - */ function runtimePlaceholderOwner( repos: readonly Repo[], repoId: string, hostId: ExecutionHostId ): Pick { - const host = parseExecutionHostId(hostId) - const connectionId = repos - .find((repo) => repo.id === repoId && getRepoExecutionHostId(repo) === hostId) - ?.connectionId?.trim() - return connectionId && host?.kind === 'runtime' - ? { hostId: toSshExecutionHostId(connectionId), runtimeOwnerEnvironmentId: host.environmentId } - : { hostId } + const row = repos.find((repo) => repo.id === repoId && getRepoExecutionHostId(repo) === hostId) + return row ? repoRowOwnerStamp(row) : { hostId } } export function buildRuntimeSessionPlaceholders({ diff --git a/src/renderer/src/store/terminals/workspace-terminal-ssh-placeholders.ts b/src/renderer/src/store/terminals/workspace-terminal-ssh-placeholders.ts index b7b3b37c326..8a4ee529318 100644 --- a/src/renderer/src/store/terminals/workspace-terminal-ssh-placeholders.ts +++ b/src/renderer/src/store/terminals/workspace-terminal-ssh-placeholders.ts @@ -13,23 +13,33 @@ import { type PlaceholderOwner = Pick +/** + * The owner a listing stamps on this repo row's worktrees. A server's repo that nests an SSH + * target lists them as `ssh:` through that server, so a bare `runtime:` stamp would hide + * the target from an `ssh:` host selection. + */ +export function repoRowOwnerStamp(row: Repo): PlaceholderOwner { + const host = parseExecutionHostId(getRepoExecutionHostId(row)) + const connectionId = row.connectionId?.trim() + if (host?.kind === 'runtime') { + return connectionId + ? { + hostId: toSshExecutionHostId(connectionId), + runtimeOwnerEnvironmentId: host.environmentId + } + : { hostId: host.id } + } + // Why: a row whose explicit host disagrees with its connection is not evidence of either. + return connectionId && host?.id === toSshExecutionHostId(connectionId) ? { hostId: host.id } : {} +} + /** * The SSH host, and the server proxying it, that a listing would stamp. A repo id with rows on * several hosts names neither, so its placeholder stays unstamped rather than guessing a twin. */ function placeholderOwner(repos: readonly Repo[], repoId: string): PlaceholderOwner { const rows = repos.filter((repo) => repo.id === repoId) - const connectionId = rows.length === 1 ? rows[0].connectionId?.trim() : undefined - if (!connectionId) { - return {} - } - const sshHostId = toSshExecutionHostId(connectionId) - const host = parseExecutionHostId(getRepoExecutionHostId(rows[0])) - if (host?.kind === 'runtime') { - return { hostId: sshHostId, runtimeOwnerEnvironmentId: host.environmentId } - } - // Why: a row whose explicit host disagrees with its connection is not evidence of either. - return host?.id === sshHostId ? { hostId: sshHostId } : {} + return rows.length === 1 && rows[0].connectionId?.trim() ? repoRowOwnerStamp(rows[0]) : {} } export function addHydratedSshWorktreePlaceholders( diff --git a/src/main/ipc/markdown-documents-remote-paths.test.ts b/src/shared/markdown-document-paths-windows-client.test.ts similarity index 93% rename from src/main/ipc/markdown-documents-remote-paths.test.ts rename to src/shared/markdown-document-paths-windows-client.test.ts index 0fb41971efe..3d170a8784a 100644 --- a/src/main/ipc/markdown-documents-remote-paths.test.ts +++ b/src/shared/markdown-document-paths-windows-client.test.ts @@ -6,10 +6,7 @@ vi.mock('node:path', async (importOriginal) => { return { ...actual, extname: actual.win32.extname } }) -import { - isMarkdownDocumentName, - markdownDocumentFromRelativePath -} from '../../shared/markdown-document-paths' +import { isMarkdownDocumentName, markdownDocumentFromRelativePath } from './markdown-document-paths' describe('remote Markdown filenames on a Windows client', () => { it('keeps the local filename helper on Windows semantics', () => { diff --git a/src/main/ipc/markdown-documents.test.ts b/src/shared/markdown-document-paths.test.ts similarity index 97% rename from src/main/ipc/markdown-documents.test.ts rename to src/shared/markdown-document-paths.test.ts index 8bc6b12f4c1..cefcd5b618c 100644 --- a/src/main/ipc/markdown-documents.test.ts +++ b/src/shared/markdown-document-paths.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' import { markdownDocumentFromFilePath, markdownDocumentFromRelativePath -} from '../../shared/markdown-document-paths' +} from './markdown-document-paths' describe('markdownDocumentFromFilePath', () => { it.skipIf(process.platform === 'win32')('preserves local literal backslash names', () => {