From 95a10902012fc1278d722a0bd665f0e8552b4276 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Sat, 13 Jun 2026 01:41:32 -0700 Subject: [PATCH] Route delete warnings by worktree owner Co-authored-by: Orca --- .../host-context-implementation-checklist.md | 1 + ...rktreeDialog.host-context-boundary.test.ts | 28 ++++++ .../sidebar/DeleteWorktreeDialog.tsx | 87 ++++++++++++++----- 3 files changed, 92 insertions(+), 24 deletions(-) create mode 100644 src/renderer/src/components/sidebar/DeleteWorktreeDialog.host-context-boundary.test.ts diff --git a/notes/host-context-implementation-checklist.md b/notes/host-context-implementation-checklist.md index e8041f0f536..535f5a73e3d 100644 --- a/notes/host-context-implementation-checklist.md +++ b/notes/host-context-implementation-checklist.md @@ -286,3 +286,4 @@ Done means: reviewers and future agents can understand the vision, implementatio - [x] 2026-06-13: Electron-verified a local project with local GitHub credentials: added the local Orca worktree to the running dev app, confirmed `gh` auth for `Jinwoo-H`, fetched 10 GitHub work items through explicit `TaskSourceContext { provider: github, hostId: local }`, verified the local source-scoped cache key, and captured the rendered Tasks page showing GitHub issues for the local project. - [x] 2026-06-13: Snapshotted Source Control PR-generation runtime ownership and reused that owner for generation, cancellation, and post-generation git refresh, preventing async PR generation from following a later focused-host switch. Verified with focused Source Control host-boundary tests, targeted oxlint, and `git diff --check`. - [x] 2026-06-13: Extended Source Control git remote operations and upstream refresh to accept explicit runtime-owner settings, then wired SourceControl and ChecksPanel to pass the repo owner host for push, pull, fetch, sync, fast-forward, rebase, publish, and PR-generation follow-up refreshes. Verified with focused editor/git-status tests, targeted oxlint, `git diff --check`, and full `pnpm run typecheck`. +- [x] 2026-06-13: Routed Delete Workspace dirty-state preloads through the selected worktree runtime owner instead of ambient focused settings, so delete warnings inspect the host that owns the workspace. Verified with focused DeleteWorktreeDialog host-boundary tests, existing worktree-owner tests, targeted oxlint, `git diff --check`, and full `pnpm run typecheck`. diff --git a/src/renderer/src/components/sidebar/DeleteWorktreeDialog.host-context-boundary.test.ts b/src/renderer/src/components/sidebar/DeleteWorktreeDialog.host-context-boundary.test.ts new file mode 100644 index 00000000000..30066815370 --- /dev/null +++ b/src/renderer/src/components/sidebar/DeleteWorktreeDialog.host-context-boundary.test.ts @@ -0,0 +1,28 @@ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' + +const SOURCE = readFileSync(join(__dirname, 'DeleteWorktreeDialog.tsx'), 'utf8') + +function sourceBetween(source: string, startPattern: string, endPattern: string): string { + const start = source.indexOf(startPattern) + expect(start).toBeGreaterThanOrEqual(0) + const end = source.indexOf(endPattern, start + startPattern.length) + expect(end).toBeGreaterThan(start) + return source.slice(start, end) +} + +describe('DeleteWorktreeDialog host-context boundaries', () => { + it('preloads git status from the selected worktree owner instead of the focused host', () => { + const effect = sourceBetween( + SOURCE, + 'const statusTargets = deleteTargets.filter(', + 'return () => {' + ) + + expect(effect).toContain('getSettingsForWorktreeRuntimeOwner') + expect(effect).toContain('worktreesByRepo: useAppStore.getState().worktreesByRepo') + expect(effect).toContain('item.id') + expect(effect).not.toContain('settings,\n worktreeId: item.id') + }) +}) diff --git a/src/renderer/src/components/sidebar/DeleteWorktreeDialog.tsx b/src/renderer/src/components/sidebar/DeleteWorktreeDialog.tsx index 64ca7e83b9b..dcd12ad5283 100644 --- a/src/renderer/src/components/sidebar/DeleteWorktreeDialog.tsx +++ b/src/renderer/src/components/sidebar/DeleteWorktreeDialog.tsx @@ -11,6 +11,7 @@ import { useAppStore } from '@/store' import { toast } from 'sonner' import { getConnectionId } from '@/lib/connection-context' import { getRuntimeGitStatus } from '@/runtime/runtime-git-client' +import { getSettingsForWorktreeRuntimeOwner } from '@/lib/worktree-runtime-owner' import { runWorktreeDeletesInParallel } from './delete-worktree-flow' import { getWorkspaceDeleteLineage } from './workspace-delete-lineage' import { DeleteWorktreeLineageNotice } from './DeleteWorktreeLineageNotice' @@ -188,7 +189,12 @@ const DeleteWorktreeDialog = React.memo(function DeleteWorktreeDialog() { let cancelled = false for (const item of statusTargets) { void getRuntimeGitStatus({ - settings, + // Why: delete warnings inspect git state for the selected workspace; + // a later focused-host switch must not make this preload query another host. + settings: getSettingsForWorktreeRuntimeOwner( + { repos, settings, worktreesByRepo: useAppStore.getState().worktreesByRepo }, + item.id + ), worktreeId: item.id, worktreePath: item.path, connectionId: getConnectionId(item.id) ?? undefined @@ -206,7 +212,7 @@ const DeleteWorktreeDialog = React.memo(function DeleteWorktreeDialog() { return () => { cancelled = true } - }, [deleteTargets, gitStatusByWorktree, isOpen, repoMap, setGitStatus, settings]) + }, [deleteTargets, gitStatusByWorktree, isOpen, repoMap, repos, setGitStatus, settings]) const handleOpenChange = useCallback( (open: boolean) => { @@ -236,21 +242,33 @@ const DeleteWorktreeDialog = React.memo(function DeleteWorktreeDialog() { // Why: the toast confirms the preference was saved and points the user at // where to undo it. The "Open Settings" action deep-links to the General // pane so they never have to hunt for the toggle if they change their mind. - toast.success(translate("auto.components.sidebar.DeleteWorktreeDialog.dd3a45bbbd", "We'll skip this confirmation next time."), { - description: translate("auto.components.sidebar.DeleteWorktreeDialog.2b56b35f53", "You can change this in Settings."), - duration: 8000, - action: { - label: translate("auto.components.sidebar.DeleteWorktreeDialog.5cc1a6701c", "Open Settings"), - onClick: () => { - openSettingsPage() - openSettingsTarget({ - pane: 'general', - repoId: null, - sectionId: 'general-skip-delete-worktree-confirm' - }) + toast.success( + translate( + 'auto.components.sidebar.DeleteWorktreeDialog.dd3a45bbbd', + "We'll skip this confirmation next time." + ), + { + description: translate( + 'auto.components.sidebar.DeleteWorktreeDialog.2b56b35f53', + 'You can change this in Settings.' + ), + duration: 8000, + action: { + label: translate( + 'auto.components.sidebar.DeleteWorktreeDialog.5cc1a6701c', + 'Open Settings' + ), + onClick: () => { + openSettingsPage() + openSettingsTarget({ + pane: 'general', + repoId: null, + sectionId: 'general-skip-delete-worktree-confirm' + }) + } } } - }) + ) }, [openSettingsPage, openSettingsTarget, updateSettings]) const handleForceDeletedFromToast = useCallback( @@ -282,17 +300,29 @@ const DeleteWorktreeDialog = React.memo(function DeleteWorktreeDialog() { deletePromise .then((result) => { if (!result.ok) { - toast.error(translate("auto.components.sidebar.DeleteWorktreeDialog.42e610d6cf", "Force delete failed"), { - description: result.error - }) + toast.error( + translate( + 'auto.components.sidebar.DeleteWorktreeDialog.42e610d6cf', + 'Force delete failed' + ), + { + description: result.error + } + ) return } onDeleted?.([worktreeId]) }) .catch((err: unknown) => { - toast.error(translate("auto.components.sidebar.DeleteWorktreeDialog.4f6750ca7b", "Failed to delete workspace"), { - description: err instanceof Error ? err.message : String(err) - }) + toast.error( + translate( + 'auto.components.sidebar.DeleteWorktreeDialog.4f6750ca7b', + 'Failed to delete workspace' + ), + { + description: err instanceof Error ? err.message : String(err) + } + ) }) } else { // Why: this modal is the destructive confirmation for the workspace @@ -365,14 +395,23 @@ const DeleteWorktreeDialog = React.memo(function DeleteWorktreeDialog() { > - {isBatchDelete ? translate("auto.components.sidebar.DeleteWorktreeDialog.86f0ae1257", "Delete Workspaces") : translate("auto.components.sidebar.DeleteWorktreeDialog.fc23c4cbdf", "Delete Workspace")} + {isBatchDelete + ? translate( + 'auto.components.sidebar.DeleteWorktreeDialog.86f0ae1257', + 'Delete Workspaces' + ) + : translate( + 'auto.components.sidebar.DeleteWorktreeDialog.fc23c4cbdf', + 'Delete Workspace' + )} - {translate("auto.components.sidebar.DeleteWorktreeDialog.91492c9ad6", "Remove")}{deleteCopy.targetLabel} + {translate('auto.components.sidebar.DeleteWorktreeDialog.91492c9ad6', 'Remove')} + {deleteCopy.targetLabel} {canDeleteAllLineage ? ( <> {' '} - {translate("auto.components.sidebar.DeleteWorktreeDialog.ff2a74ac0e", "and")}{' '} + {translate('auto.components.sidebar.DeleteWorktreeDialog.ff2a74ac0e', 'and')}{' '} {lineageDeleteCopy.childTargetLabel} {' '}