Route delete warnings by worktree owner

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo-H
2026-06-13 01:41:32 -07:00
co-authored by Orca
parent 0b232dbb3c
commit 95a1090201
3 changed files with 92 additions and 24 deletions
@@ -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`.
@@ -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')
})
})
@@ -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() {
>
<DialogHeader>
<DialogTitle className="text-sm">
{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'
)}
</DialogTitle>
<DialogDescription className="text-xs">
{translate("auto.components.sidebar.DeleteWorktreeDialog.91492c9ad6", "Remove")}<span className={deleteCopy.targetClassName}>{deleteCopy.targetLabel}</span>
{translate('auto.components.sidebar.DeleteWorktreeDialog.91492c9ad6', 'Remove')}
<span className={deleteCopy.targetClassName}>{deleteCopy.targetLabel}</span>
{canDeleteAllLineage ? (
<>
{' '}
{translate("auto.components.sidebar.DeleteWorktreeDialog.ff2a74ac0e", "and")}{' '}
{translate('auto.components.sidebar.DeleteWorktreeDialog.ff2a74ac0e', 'and')}{' '}
<span className="font-medium text-foreground">
{lineageDeleteCopy.childTargetLabel}
</span>{' '}