From af2d47d358b684327f83c52fbfc4cf7609ca7c7a Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Thu, 27 Aug 2026 19:29:47 -0700 Subject: [PATCH] Allow selecting parent worktree when creating workspace - Rename "Parent workspace" label to "Parent worktree" - Filter candidates by execution host and project to prevent nesting across hosts - Wire parentWorktreeId through composer state and creation request pipeline - Update translations and add new copy for nesting-related messages --- .../NewWorkspaceComposerCard.test.tsx | 30 +++--- .../components/NewWorkspaceComposerCard.tsx | 4 + .../ComposerParentWorktreePicker.test.tsx | 51 ++++++++++ .../ComposerParentWorktreePicker.tsx | 15 ++- .../NewWorkspaceComposerAdvancedSection.tsx | 6 ++ .../new-workspace-composer-card-props.ts | 3 + .../composer-state/composer-card-contract.ts | 4 + .../composer-state/composer-card-props.ts | 8 ++ .../composer-state/composer-store-actions.ts | 1 + .../composer-submit-orchestration.ts | 2 + .../full-creation-execution.test.ts | 1 + .../composer-state/full-creation-execution.ts | 6 +- .../hooks/composer-state/identity-model.ts | 2 + .../quick-creation-execution.ts | 4 + .../quick-creation-request.test.ts | 3 + .../composer-state/quick-creation-request.ts | 2 + .../workspace-identity-state.ts | 4 + .../useComposerState.integration.test.ts | 8 +- src/renderer/src/i18n/locales/en.json | 12 ++- src/renderer/src/i18n/locales/es.json | 12 ++- src/renderer/src/i18n/locales/ja.json | 12 ++- src/renderer/src/i18n/locales/ko.json | 89 +++++++++++++++-- src/renderer/src/i18n/locales/zh.json | 97 +++++++++++++++++-- 23 files changed, 335 insertions(+), 41 deletions(-) diff --git a/src/renderer/src/components/NewWorkspaceComposerCard.test.tsx b/src/renderer/src/components/NewWorkspaceComposerCard.test.tsx index 7895ecffbc0..8206354f916 100644 --- a/src/renderer/src/components/NewWorkspaceComposerCard.test.tsx +++ b/src/renderer/src/components/NewWorkspaceComposerCard.test.tsx @@ -308,6 +308,11 @@ function findRunTargetItem(label: string): HTMLElement | undefined { let current: { container: HTMLDivElement; root: Root } | null = null +function unmountCurrent(): void { + act(() => current?.root.unmount()) + current?.container.remove() +} + describe('NewWorkspaceComposerCard folder task source mode', () => { beforeEach(() => { ;(window as unknown as { api: unknown }).api = { @@ -335,8 +340,7 @@ describe('NewWorkspaceComposerCard folder task source mode', () => { }) afterEach(() => { - act(() => current?.root.unmount()) - current?.container.remove() + unmountCurrent() current = null vi.clearAllMocks() }) @@ -397,8 +401,7 @@ describe('NewWorkspaceComposerCard folder task source mode', () => { ) expect(collapsedReuse).toBeTruthy() - act(() => current?.root.unmount()) - current?.container.remove() + unmountCurrent() current = renderCard({ canReuseSelectedBranch: true, reuseSelectedBranch: true }) const reuseLabel = [...current.container.querySelectorAll('label')].find((label) => @@ -432,8 +435,7 @@ describe('NewWorkspaceComposerCard folder task source mode', () => { clickReuseCheckbox() expect(offChanges).toEqual([false]) - act(() => current?.root.unmount()) - current?.container.remove() + unmountCurrent() // Unchecked -> checked (opting into reuse — the action that pins the branch). const onChanges: boolean[] = [] @@ -460,8 +462,7 @@ describe('NewWorkspaceComposerCard folder task source mode', () => { 'Wait for setup to complete before starting agent' ) - act(() => current?.root.unmount()) - current?.container.remove() + unmountCurrent() current = renderCard({ advancedOpen: true, @@ -587,12 +588,10 @@ describe('NewWorkspaceComposerCard folder task source mode', () => { branchesEnabled: true }) - const branchInput = findInputByLabel(current.container, 'Branch name') - expect(branchInput).toBeTruthy() - const branchBlock = branchInput?.closest('div.space-y-1') - const nextField = branchBlock?.nextElementSibling - expect(nextField?.textContent).toContain('Parent workspace') - expect(nextField?.textContent).not.toContain('Note') + const nextField = findInputByLabel(current.container, 'Branch name')?.closest( + 'div.space-y-1' + )?.nextElementSibling + expect(nextField?.textContent).toMatch(/Parent worktree(?!.*Note)/) expect(nextField?.nextElementSibling?.textContent).toContain('Note') }) @@ -903,8 +902,7 @@ describe('NewWorkspaceComposerCard note sizing', () => { // Sizing is layout-driven (field-sizing) rather than a JS measure pass, and happy-dom // has no layout engine, so these assert the class contract that produces the growth. afterEach(() => { - act(() => current?.root.unmount()) - current?.container.remove() + unmountCurrent() current = null }) diff --git a/src/renderer/src/components/NewWorkspaceComposerCard.tsx b/src/renderer/src/components/NewWorkspaceComposerCard.tsx index 4f819cb4fc5..c6cca5192ea 100644 --- a/src/renderer/src/components/NewWorkspaceComposerCard.tsx +++ b/src/renderer/src/components/NewWorkspaceComposerCard.tsx @@ -57,6 +57,8 @@ export default function NewWorkspaceComposerCard( selectedProjectId = null, parentWorktreeId, onParentWorktreeIdChange, + selectedRepoExecutionHostId, + selectedRepoProjectId, activeFolderWorkspaceId, onAddProjectOverride, onNestedDialogOpenChange @@ -307,6 +309,8 @@ export default function NewWorkspaceComposerCard( showSetupAgentStartupPolicy={showSetupAgentStartupPolicy} parentWorktreeId={parentWorktreeId} onParentWorktreeIdChange={onParentWorktreeIdChange} + selectedRepoExecutionHostId={selectedRepoExecutionHostId} + selectedRepoProjectId={selectedRepoProjectId} activeFolderWorkspaceId={activeFolderWorkspaceId} /> diff --git a/src/renderer/src/components/new-workspace/ComposerParentWorktreePicker.test.tsx b/src/renderer/src/components/new-workspace/ComposerParentWorktreePicker.test.tsx index 6846a94ba38..c512abcebdb 100644 --- a/src/renderer/src/components/new-workspace/ComposerParentWorktreePicker.test.tsx +++ b/src/renderer/src/components/new-workspace/ComposerParentWorktreePicker.test.tsx @@ -323,6 +323,57 @@ describe('ComposerParentWorktreePicker', () => { expect(candidateLabels().join(' ')).not.toContain('Other repo') }) + it('excludes candidates on another execution host or project', () => { + seed([ + makeWorktree({ id: 'same', displayName: 'Same host', hostId: 'local', projectId: 'proj1' }), + makeWorktree({ + id: 'otherHost', + displayName: 'Other host', + hostId: 'ssh:box', + projectId: 'proj1' + }), + makeWorktree({ + id: 'otherProject', + displayName: 'Other project', + hostId: 'local', + projectId: 'proj2' + }) + ]) + + render( + + ) + fireEvent.click(trigger()) + + expect(candidateLabels().join(' ')).toContain('Same host') + expect(candidateLabels().join(' ')).not.toContain('Other host') + expect(candidateLabels().join(' ')).not.toContain('Other project') + }) + + // A worktree with no recorded hostId inherits its repo's host, which is the child's host too. + it('keeps candidates whose host or project is unrecorded', () => { + seed([makeWorktree({ id: 'unscoped', displayName: 'Unscoped' })]) + + render( + + ) + fireEvent.click(trigger()) + + expect(candidateLabels().join(' ')).toContain('Unscoped') + }) + it('restricts candidates to the active folder workspace subtree', () => { const attached = makeWorktree({ id: 'attached', diff --git a/src/renderer/src/components/new-workspace/ComposerParentWorktreePicker.tsx b/src/renderer/src/components/new-workspace/ComposerParentWorktreePicker.tsx index b888f80e7c7..41c22978f14 100644 --- a/src/renderer/src/components/new-workspace/ComposerParentWorktreePicker.tsx +++ b/src/renderer/src/components/new-workspace/ComposerParentWorktreePicker.tsx @@ -117,7 +117,7 @@ function ComposerParentWorktreePickerImpl({ return (
- {translate('auto.components.ComposerParentWorktreePicker.label', 'Parent workspace')} + {translate('auto.components.ComposerParentWorktreePicker.label', 'Parent worktree')} @@ -225,8 +225,14 @@ function ParentWorktreeCandidateList({ const [highlightedIndex, setHighlightedIndex] = useState(0) const candidates = useMemo(() => { - const childBoundary: WorktreeLineageBoundary | null = - executionHostId && projectId ? { repoId, hostId: executionHostId, projectId } : null + // Why `?? undefined`: an unresolved host or project is "not known yet", not "no host", and + // the boundary check reads undefined on either side as a wildcard. A candidate in this repo + // with no recorded hostId inherits that repo's host, so it is on the child's host too. + const childBoundary: WorktreeLineageBoundary = { + repoId, + hostId: executionHostId ?? undefined, + projectId: projectId ?? undefined + } const worktreeMap = getIndexedWorktreeMap(worktreesByRepo) const cyclicLineageIds = getCyclicProjectedWorktreeLineageIds(worktreeLineageById, worktreeMap) const folderSubtreeIds = activeFolderWorkspaceId @@ -241,9 +247,8 @@ function ParentWorktreeCandidateList({ .filter( (candidate) => candidate.repoId === repoId && - (executionHostId === undefined || candidate.hostId === executionHostId) && !candidate.isArchived && - (childBoundary === null || sharesWorktreeLineageBoundary(childBoundary, candidate)) && + sharesWorktreeLineageBoundary(childBoundary, candidate) && !cyclicLineageIds.has(candidate.id) && (folderSubtreeIds === null || folderSubtreeIds.has(candidate.id)) ) diff --git a/src/renderer/src/components/new-workspace/NewWorkspaceComposerAdvancedSection.tsx b/src/renderer/src/components/new-workspace/NewWorkspaceComposerAdvancedSection.tsx index 9b46a2480e9..f7b18f6ebaa 100644 --- a/src/renderer/src/components/new-workspace/NewWorkspaceComposerAdvancedSection.tsx +++ b/src/renderer/src/components/new-workspace/NewWorkspaceComposerAdvancedSection.tsx @@ -40,6 +40,8 @@ type NewWorkspaceComposerAdvancedSectionProps = Pick< | 'onBranchNameOverrideChange' | 'parentWorktreeId' | 'onParentWorktreeIdChange' + | 'selectedRepoExecutionHostId' + | 'selectedRepoProjectId' | 'activeFolderWorkspaceId' | 'note' | 'onNoteChange' @@ -80,6 +82,8 @@ export function NewWorkspaceComposerAdvancedSection({ onBranchNameOverrideChange, parentWorktreeId = null, onParentWorktreeIdChange, + selectedRepoExecutionHostId, + selectedRepoProjectId, activeFolderWorkspaceId = null, note, onNoteChange, @@ -201,6 +205,8 @@ export function NewWorkspaceComposerAdvancedSection({ {selectedRepoIsGit && branchesEnabled && onParentWorktreeIdChange ? ( ['repos'][number] export type EphemeralVmRecipeOption = NonNullable[number] @@ -63,6 +64,8 @@ export type NewWorkspaceComposerCardProps = { onBranchNameOverrideChange: (value: string | undefined) => void parentWorktreeId?: string | null onParentWorktreeIdChange?: (value: string | null) => void + selectedRepoExecutionHostId?: ExecutionHostId | null + selectedRepoProjectId?: string | null activeFolderWorkspaceId?: string | null onSmartGitHubItemSelect: (item: GitHubWorkItem) => void onSmartGitLabItemSelect: (item: GitLabWorkItem) => void diff --git a/src/renderer/src/hooks/composer-state/composer-card-contract.ts b/src/renderer/src/hooks/composer-state/composer-card-contract.ts index b41282032e4..1dffa161c32 100644 --- a/src/renderer/src/hooks/composer-state/composer-card-contract.ts +++ b/src/renderer/src/hooks/composer-state/composer-card-contract.ts @@ -14,6 +14,9 @@ export type ComposerCardSourceProps = Pick< | 'ephemeralVmRecipeError' | 'name' | 'branchNameOverride' + | 'parentWorktreeId' + | 'selectedRepoExecutionHostId' + | 'selectedRepoProjectId' | 'smartNameSelection' | 'reuseSelectedBranch' | 'createMultiple' @@ -63,6 +66,7 @@ export type ComposerCardActionProps = { smartNameRepoSwitchTarget?: 'project' | 'task-source' onNameValueChange: ComposerModel['handleNameValueChange'] onBranchNameOverrideChange: ComposerModel['handleBranchNameOverrideChange'] + onParentWorktreeIdChange: ComposerModel['setParentWorktreeId'] onSmartGitHubItemSelect: ComposerModel['handleSmartGitHubItemSelect'] onSmartGitLabItemSelect: ComposerModel['handleSmartGitLabItemSelect'] onSmartBranchSelect: ComposerModel['handleSmartBranchSelect'] diff --git a/src/renderer/src/hooks/composer-state/composer-card-props.ts b/src/renderer/src/hooks/composer-state/composer-card-props.ts index 8363910fe8f..0e126e3af75 100644 --- a/src/renderer/src/hooks/composer-state/composer-card-props.ts +++ b/src/renderer/src/hooks/composer-state/composer-card-props.ts @@ -13,6 +13,7 @@ export function buildComposerCardProps(state: ComposerModel) { attachmentPaths, baseBranch, branchNameOverride, + parentWorktreeId, createGateMode, createError, createMultiple, @@ -81,10 +82,12 @@ export function buildComposerCardProps(state: ComposerModel) { selectedProjectHostSetupId, selectedProjectId, selectedRepo, + selectedRepoExecutionHostId, selectedRepoConnectInProgress, selectedRepoConnectionId, selectedRepoGitHubSourceContext, selectedRepoIsGit, + selectedRepoProjectId, selectedRepoRequiresConnection, shouldWaitForIssueAutomationCheck, sourceIntentBlocksCreate, @@ -96,6 +99,7 @@ export function buildComposerCardProps(state: ComposerModel) { setCreateMultiple, setLinkQuery, setNote, + setParentWorktreeId, setSelectedEphemeralVmRecipeId, setSetupDecision, setSmartNameMode, @@ -157,6 +161,10 @@ export function buildComposerCardProps(state: ComposerModel) { onNameValueChange: handleNameValueChange, branchNameOverride: isProjectGroupTarget ? undefined : branchNameOverride, onBranchNameOverrideChange: isProjectGroupTarget ? () => {} : handleBranchNameOverrideChange, + parentWorktreeId: isProjectGroupTarget ? null : parentWorktreeId, + onParentWorktreeIdChange: isProjectGroupTarget ? () => {} : setParentWorktreeId, + selectedRepoExecutionHostId: isProjectGroupTarget ? null : selectedRepoExecutionHostId, + selectedRepoProjectId: isProjectGroupTarget ? null : selectedRepoProjectId, onSmartGitHubItemSelect: handleSmartGitHubItemSelect, onSmartGitLabItemSelect: handleSmartGitLabItemSelect, onSmartBranchSelect: isProjectGroupTarget ? () => {} : handleSmartBranchSelect, diff --git a/src/renderer/src/hooks/composer-state/composer-store-actions.ts b/src/renderer/src/hooks/composer-state/composer-store-actions.ts index 3c0a172dc46..3aa7d96f5d3 100644 --- a/src/renderer/src/hooks/composer-state/composer-store-actions.ts +++ b/src/renderer/src/hooks/composer-state/composer-store-actions.ts @@ -55,6 +55,7 @@ export type ComposerStoreActions = { linkedTaskSourceContext?: TaskSourceContext | null startupDraft?: string nameWasGenerated?: boolean + parentWorktreeId?: string provisionedRoot?: { runtimeId: string executionHostId: ExecutionHostId diff --git a/src/renderer/src/hooks/composer-state/composer-submit-orchestration.ts b/src/renderer/src/hooks/composer-state/composer-submit-orchestration.ts index e4289e452cb..b925eb12b2d 100644 --- a/src/renderer/src/hooks/composer-state/composer-submit-orchestration.ts +++ b/src/renderer/src/hooks/composer-state/composer-submit-orchestration.ts @@ -98,6 +98,7 @@ export function useComposerSubmitOrchestration( normalizedSparseDirectories: target.derivedComposerState.normalizedSparseDirectories, note: target.sourceContextState.note, onCreated: target.composerTargetStore.onCreated, + parentWorktreeId: target.workspaceIdentityState.parentWorktreeId, persistDraft: target.composerTargetStore.persistDraft, persistSetupAgentStartupPolicy: target.providerRuntimeSync.persistSetupAgentStartupPolicy, prepareFullSubmit: fullSubmitPreparation.prepareFullSubmit, @@ -210,6 +211,7 @@ export function useComposerSubmitOrchestration( linkedGitLabMR: target.workspaceIdentityState.linkedGitLabMR, normalizedSparseDirectories: target.derivedComposerState.normalizedSparseDirectories, onCreated: target.composerTargetStore.onCreated, + parentWorktreeId: target.workspaceIdentityState.parentWorktreeId, persistDraft: target.composerTargetStore.persistDraft, persistSetupAgentStartupPolicy: target.providerRuntimeSync.persistSetupAgentStartupPolicy, prepareQuickSubmit: quickSubmitPreparation.prepareQuickSubmit, diff --git a/src/renderer/src/hooks/composer-state/full-creation-execution.test.ts b/src/renderer/src/hooks/composer-state/full-creation-execution.test.ts index 704dac2832e..c8bca80df09 100644 --- a/src/renderer/src/hooks/composer-state/full-creation-execution.test.ts +++ b/src/renderer/src/hooks/composer-state/full-creation-execution.test.ts @@ -72,6 +72,7 @@ describe('useFullCreationExecution cancellation', () => { normalizedSparseDirectories: [], note: '', onCreated: vi.fn>(), + parentWorktreeId: null, persistDraft: false, persistSetupAgentStartupPolicy, prepareFullSubmit: vi diff --git a/src/renderer/src/hooks/composer-state/full-creation-execution.ts b/src/renderer/src/hooks/composer-state/full-creation-execution.ts index 713d42961d1..04ec3e2cf6d 100644 --- a/src/renderer/src/hooks/composer-state/full-creation-execution.ts +++ b/src/renderer/src/hooks/composer-state/full-creation-execution.ts @@ -12,6 +12,7 @@ export type FullCreationExecutionInput = Pick< | 'normalizedSparseDirectories' | 'note' | 'onCreated' + | 'parentWorktreeId' | 'persistDraft' | 'persistSetupAgentStartupPolicy' | 'prepareFullSubmit' @@ -47,6 +48,7 @@ export function useFullCreationExecution(input: FullCreationExecutionInput) { normalizedSparseDirectories, note, onCreated, + parentWorktreeId, persistDraft, persistSetupAgentStartupPolicy, prepareFullSubmit, @@ -155,7 +157,8 @@ export function useFullCreationExecution(input: FullCreationExecutionInput) { nameWasGenerated, ...(!backendStartup && startupPlan?.draftPrompt ? { startupDraft: startupPlan.draftPrompt } - : {}) + : {}), + ...(parentWorktreeId ? { parentWorktreeId } : {}) } ) @@ -251,6 +254,7 @@ export function useFullCreationExecution(input: FullCreationExecutionInput) { normalizedSparseDirectories, note, onCreated, + parentWorktreeId, persistDraft, persistSetupAgentStartupPolicy, prepareFullSubmit, diff --git a/src/renderer/src/hooks/composer-state/identity-model.ts b/src/renderer/src/hooks/composer-state/identity-model.ts index 8830e3d72bc..e375dd754d7 100644 --- a/src/renderer/src/hooks/composer-state/identity-model.ts +++ b/src/renderer/src/hooks/composer-state/identity-model.ts @@ -18,6 +18,8 @@ export type ComposerIdentityModel = { setCompareBaseRef: React.Dispatch> branchNameOverride: string | undefined setBranchNameOverride: React.Dispatch> + parentWorktreeId: string | null + setParentWorktreeId: React.Dispatch> branchNameOverridePreservesNameEdits: boolean setBranchNameOverridePreservesNameEdits: React.Dispatch> smartNameMode: SmartNameMode diff --git a/src/renderer/src/hooks/composer-state/quick-creation-execution.ts b/src/renderer/src/hooks/composer-state/quick-creation-execution.ts index 1e91f686d82..b6b14350290 100644 --- a/src/renderer/src/hooks/composer-state/quick-creation-execution.ts +++ b/src/renderer/src/hooks/composer-state/quick-creation-execution.ts @@ -12,6 +12,7 @@ type QuickCreationExecutionInput = Pick< | 'linkedGitLabMR' | 'normalizedSparseDirectories' | 'onCreated' + | 'parentWorktreeId' | 'persistDraft' | 'persistSetupAgentStartupPolicy' | 'prepareQuickSubmit' @@ -58,6 +59,7 @@ export function useQuickCreationExecution(input: QuickCreationExecutionInput) { linkedGitLabMR, normalizedSparseDirectories, onCreated, + parentWorktreeId, persistDraft, persistSetupAgentStartupPolicy, prepareQuickSubmit, @@ -217,6 +219,7 @@ export function useQuickCreationExecution(input: QuickCreationExecutionInput) { linkedLinearIssueWorkspaceId, linkedLinearIssueOrganizationUrlKey, branchNameOverride: effectiveBranchNameOverride, + parentWorktreeId, workspaceStatus: resolvedInitialWorkspaceStatus, linkedGitLabMR, linkedGitLabIssue, @@ -259,6 +262,7 @@ export function useQuickCreationExecution(input: QuickCreationExecutionInput) { linkedGitLabMR, normalizedSparseDirectories, onCreated, + parentWorktreeId, persistDraft, persistSetupAgentStartupPolicy, prepareQuickSubmit, diff --git a/src/renderer/src/hooks/composer-state/quick-creation-request.test.ts b/src/renderer/src/hooks/composer-state/quick-creation-request.test.ts index 0533bcc2e28..d5cd9a11c5b 100644 --- a/src/renderer/src/hooks/composer-state/quick-creation-request.test.ts +++ b/src/renderer/src/hooks/composer-state/quick-creation-request.test.ts @@ -29,6 +29,7 @@ function createInput( linkedLinearIssueWorkspaceId: undefined, linkedLinearIssueOrganizationUrlKey: undefined, branchNameOverride: undefined, + parentWorktreeId: null, workspaceStatus: undefined, linkedGitLabMR: null, linkedGitLabIssue: null, @@ -87,6 +88,7 @@ describe('quick composer creation request', () => { linkedLinearIssueWorkspaceId: 'workspace-1', linkedLinearIssueOrganizationUrlKey: 'orca', branchNameOverride: 'feature', + parentWorktreeId: 'repo-1::/worktrees/parent', workspaceStatus: 'in-progress', linkedGitLabMR: 9, linkedGitLabIssue: 8, @@ -107,6 +109,7 @@ describe('quick composer creation request', () => { linkedGitLabMR: 9, linkedGitLabIssue: 8, branchNameOverride: 'feature', + parentWorktreeId: 'repo-1::/worktrees/parent', launchDraftPrompt: 'draft', suppressTerminalFocusOnCompletion: true }) diff --git a/src/renderer/src/hooks/composer-state/quick-creation-request.ts b/src/renderer/src/hooks/composer-state/quick-creation-request.ts index 71f0735f70b..e12f2dd6256 100644 --- a/src/renderer/src/hooks/composer-state/quick-creation-request.ts +++ b/src/renderer/src/hooks/composer-state/quick-creation-request.ts @@ -33,6 +33,7 @@ export type QuickCreationRequestInput = { linkedLinearIssueWorkspaceId: string | undefined linkedLinearIssueOrganizationUrlKey: string | undefined branchNameOverride: string | undefined + parentWorktreeId: string | null workspaceStatus: WorktreeCreationRequest['workspaceStatus'] linkedGitLabMR: number | null linkedGitLabIssue: number | null @@ -88,6 +89,7 @@ export function buildQuickCreationRequest( ? { linkedLinearIssueOrganizationUrlKey: input.linkedLinearIssueOrganizationUrlKey } : {}), ...(input.branchNameOverride ? { branchNameOverride: input.branchNameOverride } : {}), + ...(input.parentWorktreeId ? { parentWorktreeId: input.parentWorktreeId } : {}), ...(input.workspaceStatus ? { workspaceStatus: input.workspaceStatus } : {}), ...(input.includeGitLabLinks && input.linkedGitLabMR != null ? { linkedGitLabMR: input.linkedGitLabMR } diff --git a/src/renderer/src/hooks/composer-state/workspace-identity-state.ts b/src/renderer/src/hooks/composer-state/workspace-identity-state.ts index 8d46a3551d4..6b7e73e9417 100644 --- a/src/renderer/src/hooks/composer-state/workspace-identity-state.ts +++ b/src/renderer/src/hooks/composer-state/workspace-identity-state.ts @@ -100,6 +100,8 @@ export function useWorkspaceIdentityState(input: WorkspaceIdentityStateInput) { initialLinearBranchName ) + const [parentWorktreeId, setParentWorktreeId] = useState(null) + const [branchNameOverridePreservesNameEdits, setBranchNameOverridePreservesNameEdits] = useState( Boolean(initialLinearBranchName) ) @@ -195,6 +197,8 @@ export function useWorkspaceIdentityState(input: WorkspaceIdentityStateInput) { setCompareBaseRef, branchNameOverride, setBranchNameOverride, + parentWorktreeId, + setParentWorktreeId, branchNameOverridePreservesNameEdits, setBranchNameOverridePreservesNameEdits, smartNameMode, diff --git a/src/renderer/src/hooks/useComposerState.integration.test.ts b/src/renderer/src/hooks/useComposerState.integration.test.ts index 2119e99f860..c82e1a118c6 100644 --- a/src/renderer/src/hooks/useComposerState.integration.test.ts +++ b/src/renderer/src/hooks/useComposerState.integration.test.ts @@ -1,6 +1,6 @@ // @vitest-environment happy-dom -import { renderHook } from '@testing-library/react' +import { act, renderHook } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { useAppStore } from '@/store' import { useComposerState } from './useComposerState' @@ -31,7 +31,7 @@ afterEach(() => { }) describe('useComposerState integrated lifecycle', () => { - it('composes two live composers and releases each native drop listener on unmount', () => { + it('composes two live composers and exposes the parent-worktree control state', () => { useAppStore.setState({ repos: [], projects: [], @@ -57,6 +57,10 @@ describe('useComposerState integrated lifecycle', () => { expect(first.result.current.cardProps.name).toBe('first') expect(second.result.current.cardProps.name).toBe('second') + expect(first.result.current.cardProps.parentWorktreeId).toBeNull() + expect(first.result.current.cardProps.onParentWorktreeIdChange).toBeTypeOf('function') + act(() => first.result.current.cardProps.onParentWorktreeIdChange('repo-1::/parent')) + expect(first.result.current.cardProps.parentWorktreeId).toBe('repo-1::/parent') expect(window.api.ui.onFileDrop).toHaveBeenCalledTimes(2) second.unmount() diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index bbffbf8a974..3f0a9feb9f0 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -448,7 +448,10 @@ } } } - } + }, + "createdWithoutParentNesting": "Created without nesting under \"{{value0}}\"", + "createdWithoutParentNestingUnnamed": "Created without nesting under the selected parent", + "createdWithoutParentNestingDetail": "The parent workspace was no longer available. You can set it from the workspace menu." }, "repos": { "2975400634": "Update Orca server to open non-Git folders on this runtime.", @@ -16293,6 +16296,13 @@ "remoteLabel": "Browsers on {{value0}}", "clientNoteLocalStorage": "Imports read this device’s browsers. Cookies are stored locally.", "remoteNoteRemoteStorage": "Imports read browsers on {{value0}}. Cookies are stored on that machine, and a permission prompt may appear on its screen." + }, + "ComposerParentWorktreePicker": { + "label": "Parent worktree", + "noParent": "No parent", + "description": "Nests this workspace under another in the sidebar. Does not change the base branch.", + "searchPlaceholder": "Search workspaces...", + "noMatches": "No matches." } }, "i18n": { diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 5142116a66b..5fa24d06071 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -263,7 +263,10 @@ "3b57982bf6": "Git no pudo eliminar de forma segura la rama \"{{value0}}\" después de eliminar el espacio de trabajo \"{{value1}}\", así que Orca la conservó para evitar perder commits locales.", "81f13f48d2": "Git no pudo eliminar de forma segura la rama \"{{value0}}\" después de eliminar el worktree \"{{value1}}\", así que Orca la conservó para evitar perder commits locales.", "runtimeScopeForbiddenTitle": "Esta conexión tiene acceso limitado (móvil)", - "runtimeScopeForbiddenDescription": "Los espacios de trabajo no están disponibles con un emparejamiento de acceso móvil. Vuelve a conectarte con el enlace del navegador desde Ajustes → Servidores remotos de Orca → Comparte este servidor Orca." + "runtimeScopeForbiddenDescription": "Los espacios de trabajo no están disponibles con un emparejamiento de acceso móvil. Vuelve a conectarte con el enlace del navegador desde Ajustes → Servidores remotos de Orca → Comparte este servidor Orca.", + "createdWithoutParentNesting": "Creado sin anidarlo bajo \"{{value0}}\"", + "createdWithoutParentNestingUnnamed": "Creado sin anidarlo bajo el worktree padre seleccionado", + "createdWithoutParentNestingDetail": "El espacio de trabajo padre ya no estaba disponible. Puedes establecerlo desde el menú del espacio de trabajo." }, "repos": { "2975400634": "Actualiza el servidor Orca para abrir carpetas que no sean Git en este host.", @@ -14434,6 +14437,13 @@ "deleteDescription": "“{{name}}” ya no estará disponible en su enlace público.", "delete": "Eliminar" } + }, + "ComposerParentWorktreePicker": { + "label": "Worktree padre", + "noParent": "Sin worktree padre", + "description": "Anida este espacio de trabajo bajo otro en la barra lateral. No cambia la rama base.", + "searchPlaceholder": "Buscar espacios de trabajo...", + "noMatches": "No hay coincidencias." } }, "i18n": { diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 1c72acfa367..0394b1c3d01 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -263,7 +263,10 @@ "3b57982bf6": "ワークスペース\"{{value1}}\"を削除した後、Git はブランチ\"{{value0}}\"を安全に削除できなかったため、Orca はローカルコミットを失わないように保持しました。", "81f13f48d2": "ワークツリー\"{{value1}}\"を削除した後、Git はブランチ\"{{value0}}\"を安全に削除できなかったため、Orca はローカルコミットを失わないように保持しました。", "runtimeScopeForbiddenTitle": "この接続は制限付き(モバイル)アクセスです", - "runtimeScopeForbiddenDescription": "モバイル向けペアリングではワークスペースを使用できません。[設定] → [リモート Orca サーバー] → [この Orca サーバーを共有する] から、ブラウザ用リンクで再接続してください。" + "runtimeScopeForbiddenDescription": "モバイル向けペアリングではワークスペースを使用できません。[設定] → [リモート Orca サーバー] → [この Orca サーバーを共有する] から、ブラウザ用リンクで再接続してください。", + "createdWithoutParentNesting": "「{{value0}}」の配下にネストせずに作成しました", + "createdWithoutParentNestingUnnamed": "選択した親ワークツリーの配下にネストせずに作成しました", + "createdWithoutParentNestingDetail": "親ワークスペースは利用できなくなりました。ワークスペースメニューから設定できます。" }, "repos": { "2975400634": "Orca サーバーを更新して、このランタイムで Git 以外のフォルダーを開くようにします。", @@ -14425,6 +14428,13 @@ "deleteDescription": "「{{name}}」は公開リンクで利用できなくなります。", "delete": "削除" } + }, + "ComposerParentWorktreePicker": { + "label": "親ワークツリー", + "noParent": "親なし", + "description": "このワークスペースをサイドバー内の別のワークスペースの配下にネストします。ベースブランチは変更されません。", + "searchPlaceholder": "ワークスペースを検索...", + "noMatches": "一致する項目がありません。" } }, "i18n": { diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index e5aefb9291a..3a57a64954a 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -266,7 +266,10 @@ "3b57982bf6": "\"{{value1}}\" 워크스페이스를 삭제한 후 Git이 \"{{value0}}\" 브랜치를 안전하게 삭제할 수 없어 Orca가 로컬 커밋 손실을 방지하기 위해 유지했습니다.", "81f13f48d2": "\"{{value1}}\" 워크트리를 삭제한 후 Git이 \"{{value0}}\" 브랜치를 안전하게 삭제할 수 없어 Orca가 로컬 커밋 손실을 방지하기 위해 유지했습니다.", "runtimeScopeForbiddenTitle": "이 연결은 제한된(모바일) 액세스입니다", - "runtimeScopeForbiddenDescription": "모바일 범위 페어링에서는 워크스페이스를 사용할 수 없습니다. 설정 → 원격 Orca 서버 → 이 Orca 서버 공유에서 브라우저 액세스 링크로 다시 연결하세요." + "runtimeScopeForbiddenDescription": "모바일 범위 페어링에서는 워크스페이스를 사용할 수 없습니다. 설정 → 원격 Orca 서버 → 이 Orca 서버 공유에서 브라우저 액세스 링크로 다시 연결하세요.", + "createdWithoutParentNesting": "\"{{value0}}\" 아래에 중첩하지 않고 생성했습니다", + "createdWithoutParentNestingUnnamed": "선택한 상위 워크트리 아래에 중첩하지 않고 생성했습니다", + "createdWithoutParentNestingDetail": "상위 워크스페이스를 더 이상 사용할 수 없습니다. 워크스페이스 메뉴에서 설정할 수 있습니다." }, "repos": { "2975400634": "이 런타임에서 Git이 아닌 폴더를 열려면 Orca 서버를 업데이트하세요.", @@ -3879,17 +3882,82 @@ }, "skills": { "SkillsPage": { - "cb142070b4": "새로 고침", "a68dee6a32": "스킬 검색", "f43ad6edf3": "스킬", "ea72d6185b": "스킬을 검색하지 못했습니다", "dc4c3328ee": "파일 표시", "9963dff6d3": "설명을 찾을 수 없습니다.", "995fde8337": "스킬 파일을 표시하지 못했습니다", "4acd6d68ec": "스킬을 찾을 수 없습니다", "6a62a0168c": "일치하는 항목 없음", "08a321a984": "현재 검색 및 필터와 일치하는 스킬이 없습니다.", "cd7893fbc1": "스킬 검색 중", "35b9a724a0": "사용 가능", "c13b82793c": "설치 관리", "aee7b99cc6": "링크에서 설치", "filterProvider": "에이전트별 필터", "filterSource": "소스별 필터", "allSources": "전체", "clearFilters": "필터 지우기", "closeSkills": "스킬 닫기", "closeTooltip": "닫기 · Esc", "moreActions": "추가 작업", "sharedLinks": "공유 링크", "emptyCopy": "검색한 스킬 폴더가 비어 있습니다. 공유 번들을 설치하거나 스킬을 추가한 후 새로 고치세요.", "retry": "다시 시도", "remoteShareNotice": "이 스킬은 {{host}}에 있습니다. 공유하려면 해당 머신에서 스킬을 여세요.", "viewSwitch": "표시", "searchLinks": "링크 검색", "deleteSkills": "스킬 삭제…" + "cb142070b4": "새로 고침", + "a68dee6a32": "스킬 검색", + "f43ad6edf3": "스킬", + "ea72d6185b": "스킬을 검색하지 못했습니다", + "dc4c3328ee": "파일 표시", + "9963dff6d3": "설명을 찾을 수 없습니다.", + "995fde8337": "스킬 파일을 표시하지 못했습니다", + "4acd6d68ec": "스킬을 찾을 수 없습니다", + "6a62a0168c": "일치하는 항목 없음", + "08a321a984": "현재 검색 및 필터와 일치하는 스킬이 없습니다.", + "cd7893fbc1": "스킬 검색 중", + "35b9a724a0": "사용 가능", + "c13b82793c": "설치 관리", + "aee7b99cc6": "링크에서 설치", + "filterProvider": "에이전트별 필터", + "filterSource": "소스별 필터", + "allSources": "전체", + "clearFilters": "필터 지우기", + "closeSkills": "스킬 닫기", + "closeTooltip": "닫기 · Esc", + "moreActions": "추가 작업", + "sharedLinks": "공유 링크", + "emptyCopy": "검색한 스킬 폴더가 비어 있습니다. 공유 번들을 설치하거나 스킬을 추가한 후 새로 고치세요.", + "retry": "다시 시도", + "remoteShareNotice": "이 스킬은 {{host}}에 있습니다. 공유하려면 해당 머신에서 스킬을 여세요.", + "viewSwitch": "표시", + "searchLinks": "링크 검색", + "deleteSkills": "스킬 삭제…" }, "SkillShareSelectionControls": { "01c5a15e02": "스킬 공유" }, - "SkillRow": { "updatedUnknown": "날짜 없음", "pathCopied": "경로 복사됨", "copyPath": "경로 복사", "detailPath": "경로", "skillActions": "{{value0}} 작업", "viewDetails": "세부 정보 보기", "deleteSkill": "삭제…" }, + "SkillRow": { + "updatedUnknown": "날짜 없음", + "pathCopied": "경로 복사됨", + "copyPath": "경로 복사", + "detailPath": "경로", + "skillActions": "{{value0}} 작업", + "viewDetails": "세부 정보 보기", + "deleteSkill": "삭제…" + }, "SkillsList": { "listLabel": "스킬" }, - "sourceStatus": { "missing": "폴더를 찾을 수 없음", "remoteRepo": "원격 리포지토리 — 검색 안 됨", "unavailable": "검색 안 됨" }, + "sourceStatus": { + "missing": "폴더를 찾을 수 없음", + "remoteRepo": "원격 리포지토리 — 검색 안 됨", + "unavailable": "검색 안 됨" + }, "sources": { "heading": "스킬 폴더" }, - "sourceKind": { "home": "홈", "workspace": "워크스페이스", "bundled": "번들", "plugin": "플러그인" }, - "count": { "skillOne": "스킬 {{count}}개", "skillOther": "스킬 {{count}}개", "sourceOne": "소스 {{count}}개", "sourceOther": "소스 {{count}}개", "fileOne": "파일 {{count}}개", "fileOther": "파일 {{count}}개", "resultOne": "결과 {{count}}개", "resultOther": "결과 {{count}}개", "selected": "{{count}}개 선택됨", "shareOne": "스킬 {{count}}개 공유", "shareOther": "스킬 {{count}}개 공유", "linkOne": "링크 {{count}}개", "linkOther": "링크 {{count}}개" }, + "sourceKind": { + "home": "홈", + "workspace": "워크스페이스", + "bundled": "번들", + "plugin": "플러그인" + }, + "count": { + "skillOne": "스킬 {{count}}개", + "skillOther": "스킬 {{count}}개", + "sourceOne": "소스 {{count}}개", + "sourceOther": "소스 {{count}}개", + "fileOne": "파일 {{count}}개", + "fileOther": "파일 {{count}}개", + "resultOne": "결과 {{count}}개", + "resultOther": "결과 {{count}}개", + "selected": "{{count}}개 선택됨", + "shareOne": "스킬 {{count}}개 공유", + "shareOther": "스킬 {{count}}개 공유", + "linkOne": "링크 {{count}}개", + "linkOther": "링크 {{count}}개" + }, "filter": { "allAgents": "모든 에이전트", "sharedAgent": "공유됨 (.agents)" }, - "SkillsSelectionHeader": { "exit": "선택 나가기", "exitTooltip": "선택 나가기 · Esc", "title": "공유할 스킬 선택", "selectAll": "가능한 {{count}}개 모두 선택", "clear": "지우기", "deleteTitle": "삭제할 스킬 선택" }, + "SkillsSelectionHeader": { + "exit": "선택 나가기", + "exitTooltip": "선택 나가기 · Esc", + "title": "공유할 스킬 선택", + "selectAll": "가능한 {{count}}개 모두 선택", + "clear": "지우기", + "deleteTitle": "삭제할 스킬 선택" + }, "SkillDetailDialog": { "agents": "에이전트", "updated": "업데이트됨", "copy": "복사" }, "SkillFreshnessNudge": { "titleOne": "설치된 Orca 스킬이 오래되었습니다", @@ -14392,6 +14460,13 @@ "deleteDescription": "“{{name}}”은(는) 공개 링크에서 더 이상 사용할 수 없습니다.", "delete": "삭제" } + }, + "ComposerParentWorktreePicker": { + "label": "상위 워크트리", + "noParent": "상위 워크트리 없음", + "description": "이 워크스페이스를 사이드바의 다른 워크스페이스 아래에 중첩합니다. 기준 브랜치는 변경되지 않습니다.", + "searchPlaceholder": "워크스페이스 검색...", + "noMatches": "일치하는 항목이 없습니다." } }, "i18n": { diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 6b5cae8fc24..0e55995d3d9 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -266,7 +266,10 @@ "3b57982bf6": "删除工作区\"{{value1}}\"后,Git 无法安全删除分支\"{{value0}}\",因此 Orca 将其保留以避免丢失本地提交。", "81f13f48d2": "删除工作树\"{{value1}}\"后,Git 无法安全删除分支\"{{value0}}\",因此 Orca 将其保留以避免丢失本地提交。", "runtimeScopeForbiddenTitle": "此连接具有受限(移动端)访问权限", - "runtimeScopeForbiddenDescription": "移动端范围的配对无法使用工作区。请通过设置 → 远程 Orca 服务器 → 分享此 Orca 服务器中的浏览器访问链接重新连接。" + "runtimeScopeForbiddenDescription": "移动端范围的配对无法使用工作区。请通过设置 → 远程 Orca 服务器 → 分享此 Orca 服务器中的浏览器访问链接重新连接。", + "createdWithoutParentNesting": "已创建,但未嵌套在“{{value0}}”下", + "createdWithoutParentNestingUnnamed": "已创建,但未嵌套在所选父工作树下", + "createdWithoutParentNestingDetail": "父工作区已不可用。您可以从工作区菜单中重新设置。" }, "repos": { "2975400634": "请更新 Orca 服务器,以便在此运行时打开非 Git 文件夹。", @@ -3889,17 +3892,90 @@ }, "skills": { "SkillsPage": { - "cb142070b4": "刷新", "a68dee6a32": "搜索技能", "f43ad6edf3": "技能", "ea72d6185b": "无法扫描技能", "dc4c3328ee": "显示文件", "9963dff6d3": "未找到说明。", "995fde8337": "无法显示技能文件", "4acd6d68ec": "未找到技能", "6a62a0168c": "无匹配项", "08a321a984": "没有技能匹配当前搜索和筛选条件。", "cd7893fbc1": "正在扫描技能", "35b9a724a0": "可用", "c13b82793c": "管理安装", "aee7b99cc6": "从链接安装", "filterProvider": "按 Agent 筛选", "filterSource": "按来源筛选", "allSources": "全部", "clearFilters": "清除筛选", "closeSkills": "关闭技能", "closeTooltip": "关闭 · Esc", "moreActions": "更多操作", "sharedLinks": "共享链接", "emptyCopy": "扫描的技能文件夹为空。请安装共享包,或添加技能后刷新。", "retry": "重试", "remoteShareNotice": "这些技能位于 {{host}}。请在该机器上打开“技能”以进行共享。", "viewSwitch": "显示", "searchLinks": "搜索链接", "deleteSkills": "删除技能…" + "cb142070b4": "刷新", + "a68dee6a32": "搜索技能", + "f43ad6edf3": "技能", + "ea72d6185b": "无法扫描技能", + "dc4c3328ee": "显示文件", + "9963dff6d3": "未找到说明。", + "995fde8337": "无法显示技能文件", + "4acd6d68ec": "未找到技能", + "6a62a0168c": "无匹配项", + "08a321a984": "没有技能匹配当前搜索和筛选条件。", + "cd7893fbc1": "正在扫描技能", + "35b9a724a0": "可用", + "c13b82793c": "管理安装", + "aee7b99cc6": "从链接安装", + "filterProvider": "按 Agent 筛选", + "filterSource": "按来源筛选", + "allSources": "全部", + "clearFilters": "清除筛选", + "closeSkills": "关闭技能", + "closeTooltip": "关闭 · Esc", + "moreActions": "更多操作", + "sharedLinks": "共享链接", + "emptyCopy": "扫描的技能文件夹为空。请安装共享包,或添加技能后刷新。", + "retry": "重试", + "remoteShareNotice": "这些技能位于 {{host}}。请在该机器上打开“技能”以进行共享。", + "viewSwitch": "显示", + "searchLinks": "搜索链接", + "deleteSkills": "删除技能…" }, "SkillShareSelectionControls": { "01c5a15e02": "共享技能" }, - "SkillRow": { "updatedUnknown": "无日期", "pathCopied": "路径已复制", "copyPath": "复制路径", "detailPath": "路径", "skillActions": "{{value0}} 的操作", "viewDetails": "查看详情", "deleteSkill": "删除…" }, + "SkillRow": { + "updatedUnknown": "无日期", + "pathCopied": "路径已复制", + "copyPath": "复制路径", + "detailPath": "路径", + "skillActions": "{{value0}} 的操作", + "viewDetails": "查看详情", + "deleteSkill": "删除…" + }, "SkillsList": { "listLabel": "技能" }, - "sourceStatus": { "missing": "未找到文件夹", "remoteRepo": "远程仓库 — 未扫描", "unavailable": "未扫描" }, + "sourceStatus": { + "missing": "未找到文件夹", + "remoteRepo": "远程仓库 — 未扫描", + "unavailable": "未扫描" + }, "sources": { "heading": "技能文件夹" }, - "sourceKind": { "home": "主目录", "workspace": "工作区", "bundled": "内置", "plugin": "插件" }, - "count": { "skillOne": "{{count}} 个技能", "skillOther": "{{count}} 个技能", "sourceOne": "{{count}} 个来源", "sourceOther": "{{count}} 个来源", "fileOne": "{{count}} 个文件", "fileOther": "{{count}} 个文件", "resultOne": "{{count}} 个结果", "resultOther": "{{count}} 个结果", "selected": "已选择 {{count}} 个", "shareOne": "共享 {{count}} 个技能", "shareOther": "共享 {{count}} 个技能", "linkOne": "{{count}} 个链接", "linkOther": "{{count}} 个链接", "deleteOne": "删除 {{count}} 个技能", "deleteOther": "删除 {{count}} 个技能", "deletedOne": "已删除 {{count}} 个技能", "deletedOther": "已删除 {{count}} 个技能", "deleteFolderOne": "{{count}} 个文件夹", "deleteFolderOther": "{{count}} 个文件夹", "deleteLinkOne": "{{count}} 个链接", "deleteLinkOther": "{{count}} 个链接" }, + "sourceKind": { + "home": "主目录", + "workspace": "工作区", + "bundled": "内置", + "plugin": "插件" + }, + "count": { + "skillOne": "{{count}} 个技能", + "skillOther": "{{count}} 个技能", + "sourceOne": "{{count}} 个来源", + "sourceOther": "{{count}} 个来源", + "fileOne": "{{count}} 个文件", + "fileOther": "{{count}} 个文件", + "resultOne": "{{count}} 个结果", + "resultOther": "{{count}} 个结果", + "selected": "已选择 {{count}} 个", + "shareOne": "共享 {{count}} 个技能", + "shareOther": "共享 {{count}} 个技能", + "linkOne": "{{count}} 个链接", + "linkOther": "{{count}} 个链接", + "deleteOne": "删除 {{count}} 个技能", + "deleteOther": "删除 {{count}} 个技能", + "deletedOne": "已删除 {{count}} 个技能", + "deletedOther": "已删除 {{count}} 个技能", + "deleteFolderOne": "{{count}} 个文件夹", + "deleteFolderOther": "{{count}} 个文件夹", + "deleteLinkOne": "{{count}} 个链接", + "deleteLinkOther": "{{count}} 个链接" + }, "filter": { "allAgents": "所有 Agent", "sharedAgent": "共享 (.agents)" }, - "SkillsSelectionHeader": { "exit": "退出选择", "exitTooltip": "退出选择 · Esc", "title": "选择要共享的技能", "selectAll": "选择全部 {{count}} 个符合条件的项目", "clear": "清除", "deleteTitle": "选择要删除的技能" }, + "SkillsSelectionHeader": { + "exit": "退出选择", + "exitTooltip": "退出选择 · Esc", + "title": "选择要共享的技能", + "selectAll": "选择全部 {{count}} 个符合条件的项目", + "clear": "清除", + "deleteTitle": "选择要删除的技能" + }, "SkillDetailDialog": { "agents": "Agent", "updated": "已更新", "copy": "复制" }, "SkillFreshnessNudge": { "titleOne": "已安装的 Orca 技能已过期", @@ -14408,6 +14484,13 @@ "deleteDescription": "“{{name}}” 将无法再通过其公共链接访问。", "delete": "删除" } + }, + "ComposerParentWorktreePicker": { + "label": "父工作树", + "noParent": "无父工作树", + "description": "在侧边栏中将此工作区嵌套到另一个工作区下。不会更改基础分支。", + "searchPlaceholder": "搜索工作区...", + "noMatches": "无匹配项。" } }, "i18n": {