diff --git a/src/main/daemon/pty-subprocess.test.ts b/src/main/daemon/pty-subprocess.test.ts index 98e0b863b..85366a137 100644 --- a/src/main/daemon/pty-subprocess.test.ts +++ b/src/main/daemon/pty-subprocess.test.ts @@ -2028,15 +2028,14 @@ describe('createPtySubprocess', () => { } } - expect(spawnMock).toHaveBeenCalledWith( - 'wsl.exe', - expect.any(Array), - expect.objectContaining({ - env: expect.objectContaining({ - ORCA_TERMINAL_HANDLE: 'term_wsl', - WSLENV: 'FOO/u:ORCA_TERMINAL_HANDLE/u:POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD' - }) - }) + const spawnCall = spawnMock.mock.calls.at(-1)! + expect(spawnCall[0]).toBe('wsl.exe') + expect(spawnCall[1]).toEqual(expect.any(Array)) + expect(spawnCall[2].env.ORCA_TERMINAL_HANDLE).toBe('term_wsl') + // Why: the daemon inherits optional agent-hook env in development. This + // test owns only the terminal handle and Powerlevel10k WSLENV contract. + expect(spawnCall[2].env.WSLENV?.split(':')).toEqual( + expect.arrayContaining(['FOO/u', 'ORCA_TERMINAL_HANDLE/u', POWERLEVEL10K_WIZARD_DISABLE_ENV]) ) }) diff --git a/src/renderer/src/components/right-sidebar/SourceControl.preview-open.test.tsx b/src/renderer/src/components/right-sidebar/SourceControl.preview-open.test.tsx index 61bb6ac36..6d0121157 100644 --- a/src/renderer/src/components/right-sidebar/SourceControl.preview-open.test.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControl.preview-open.test.tsx @@ -47,6 +47,7 @@ const mocks = vi.hoisted(() => { openBranchDiff: vi.fn(), createEmptySplitGroup: vi.fn(), discardRuntimeGitPath: vi.fn(), + bulkStageRuntimeGitPaths: vi.fn(), refreshGitStatusForWorktree: vi.fn(), requestEditorSaveQuiesce: vi.fn(), notifyEditorExternalFileChange: vi.fn() @@ -85,7 +86,8 @@ vi.mock('@/runtime/runtime-git-client', async (importOriginal) => { const actual = await importOriginal>() return { ...actual, - discardRuntimeGitPath: mocks.calls.discardRuntimeGitPath + discardRuntimeGitPath: mocks.calls.discardRuntimeGitPath, + bulkStageRuntimeGitPaths: mocks.calls.bulkStageRuntimeGitPaths } }) @@ -140,6 +142,7 @@ function resetState(overrides: Partial> = {}): void { vi.clearAllMocks() mocks.calls.createEmptySplitGroup.mockReturnValue('group-2') mocks.calls.discardRuntimeGitPath.mockResolvedValue(undefined) + mocks.calls.bulkStageRuntimeGitPaths.mockResolvedValue(undefined) mocks.calls.refreshGitStatusForWorktree.mockResolvedValue(undefined) mocks.calls.requestEditorSaveQuiesce.mockResolvedValue(undefined) mocks.state = { @@ -437,13 +440,76 @@ describe('SourceControl preview row opens', () => { const row = container.querySelector( '[data-source-control-path="packages/nested"]' ) - expect(row?.textContent).toContain('Submodule changes - stage inside submodule') + expect(row?.textContent).toContain('Stage inside submodule') + expect( + row?.querySelector('[title*="cannot stage file changes inside a submodule"]') + ).not.toBeNull() - const stageButton = row?.querySelector( - 'button[aria-label="Stage these changes inside the submodule"]' + expect(row?.querySelector('button[aria-label="Stage"]')).toBeNull() + }) + + it('does not render a commit-area Stage All primary for submodule worktree-only rows', () => { + resetState({ + gitStatusByWorktree: { + [mocks.activeWorktree.id]: [ + gitEntry({ + path: 'june-11th-launch', + submodule: { commitChanged: false, trackedChanges: true, untrackedChanges: false } + }) + ] + } + }) + renderSourceControl() + + const row = container.querySelector( + '[data-source-control-path="june-11th-launch"]' + ) + expect(row?.textContent).toContain('Stage inside submodule') + + const stageAllButton = [...container.querySelectorAll('button')].find( + (button) => button.textContent?.trim() === 'Stage All' + ) + expect(stageAllButton).toBeUndefined() + + const commitButton = [...container.querySelectorAll('button')].find( + (button) => button.textContent?.trim() === 'Commit' + ) + expect(commitButton).toBeDefined() + expect(commitButton?.disabled).toBe(true) + + act(() => { + commitButton?.dispatchEvent(new MouseEvent('click', { bubbles: true })) + }) + expect(mocks.calls.bulkStageRuntimeGitPaths).not.toHaveBeenCalled() + }) + + it('keeps the commit-area Stage All primary for changed submodule gitlinks', async () => { + resetState({ + gitStatusByWorktree: { + [mocks.activeWorktree.id]: [ + gitEntry({ + path: 'packages/nested', + submodule: { commitChanged: true, trackedChanges: false, untrackedChanges: false } + }) + ] + } + }) + renderSourceControl() + + const stageAllButton = [...container.querySelectorAll('button')].find( + (button) => button.textContent?.trim() === 'Stage All' + ) + expect(stageAllButton).toBeDefined() + expect(stageAllButton?.disabled).toBe(false) + + await act(async () => { + stageAllButton?.dispatchEvent(new MouseEvent('click', { bubbles: true })) + await Promise.resolve() + }) + expect(mocks.calls.bulkStageRuntimeGitPaths).toHaveBeenCalledWith( + expect.objectContaining({ worktreeId: mocks.activeWorktree.id, worktreePath: '/repo/wt' }), + ['packages/nested'] ) - expect(stageButton).not.toBeNull() - expect(stageButton?.getAttribute('aria-disabled')).toBe('true') }) it('passes preview=true when a plain branch row click opens a branch diff tab', () => { diff --git a/src/renderer/src/components/right-sidebar/SourceControl.tsx b/src/renderer/src/components/right-sidebar/SourceControl.tsx index 59a446729..11803eee8 100644 --- a/src/renderer/src/components/right-sidebar/SourceControl.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControl.tsx @@ -536,8 +536,9 @@ const SOURCE_CONTROL_TREE_DIRECTORY_PADDING_PX = 8 const SOURCE_CONTROL_TREE_FILE_PADDING_PX = 20 const EMPTY_GIT_HISTORY_STATE: GitHistoryPanelState = { status: 'idle' } const DEFAULT_COLLAPSED_SECTIONS = ['history'] as const -const SUBMODULE_WORKTREE_ONLY_LABEL = 'Submodule changes - stage inside submodule' -const SUBMODULE_WORKTREE_ONLY_STAGE_TOOLTIP = 'Stage these changes inside the submodule' +const SUBMODULE_WORKTREE_ONLY_LABEL = 'Stage inside submodule' +const SUBMODULE_WORKTREE_ONLY_TOOLTIP = + 'The parent repo (including Stage All) cannot stage file changes inside a submodule' const SUBMODULE_LOADING_LABEL = 'Loading submodule changes…' const SUBMODULE_EMPTY_LABEL = 'No changes in submodule' const SUBMODULE_ERROR_LABEL = 'Failed to load submodule changes' @@ -3995,7 +3996,12 @@ function SourceControlInner(): React.JSX.Element { ]) const hasUnstagedChanges = grouped.unstaged.length > 0 || grouped.untracked.length > 0 - const hasStageableChanges = hasUnstagedChanges + const hasStageableChanges = useMemo( + () => + grouped.unstaged.some(isStageableStatusEntry) || + grouped.untracked.some(isStageableStatusEntry), + [grouped.unstaged, grouped.untracked] + ) const hasPartiallyStagedChanges = useMemo(() => { if (grouped.staged.length === 0 || grouped.unstaged.length === 0) { return false @@ -8115,9 +8121,17 @@ const UncommittedEntryRow = React.memo(function UncommittedEntryRow({ {dirPath} )} - {(conflictLabel || isSubmoduleWorktreeOnly) && ( -
- {conflictLabel ?? SUBMODULE_WORKTREE_ONLY_LABEL} + {conflictLabel && ( +
{conflictLabel}
+ )} + {isSubmoduleWorktreeOnly && ( + // Why: parent git can stage a changed gitlink, but not nested + // worktree dirtiness. Keep that boundary visible in the row. +
+ {SUBMODULE_WORKTREE_ONLY_LABEL}
)}
@@ -8176,19 +8190,14 @@ const UncommittedEntryRow = React.memo(function UncommittedEntryRow({ }} /> )} - {(canStage || isSubmoduleWorktreeOnly) && ( + {canStage && ( { event.stopPropagation() void onStage(entry.path) }} - disabled={isSubmoduleWorktreeOnly} /> )} {canUnstage && (