Remove source control group order preference (#12785)

* Reorder source control to show staged changes first by default

Stages are closest to the commit action and most relevant to the
commit workflow. Merges untracked files into Changes visually while
preserving their Git area. Removes the untracked-first preset and
includes migration logic for existing user settings.

* Drop source control group order user preference

Remove the sourceControlGroupOrder setting and related UI, migrations, and persistence logic. The source control view now always displays sections in the order: staged changes, unstaged changes, untracked files.

* Reorder source control to show changes before staged

Aligns with the edit-stage-commit workflow by showing unstaged
changes (active edits) before staged changes (queued for commit).
This commit is contained in:
Jinjing
2026-08-05 15:29:46 -07:00
committed by GitHub
parent 2c2a3266a6
commit ae1ed5e886
33 changed files with 267 additions and 468 deletions
+1 -6
View File
@@ -150,10 +150,5 @@
}
}
],
"ignorePatterns": [
"**/node_modules",
"**/dist",
"**/out",
"tests/e2e/.cross-version-checkouts"
]
"ignorePatterns": ["**/node_modules", "**/dist", "**/out", "tests/e2e/.cross-version-checkouts"]
}
+10 -12
View File
@@ -109,9 +109,7 @@ function waitForStdoutSentinel(proc, protocol, stderr) {
settled = true
proc.stdout.off('data', onData)
rejectPromise(
new Error(
`process exited before sentinel (code=${code}, signal=${signal})\n${stderr()}`
)
new Error(`process exited before sentinel (code=${code}, signal=${signal})\n${stderr()}`)
)
}
proc.stdout.on('data', onData)
@@ -162,11 +160,7 @@ function createRelayClient(entryPath, args, env, protocol) {
})
const waitForMessage = (startIndex, predicate, label) =>
pollUntil(
() => messages.slice(startIndex).find(predicate),
label,
streams.stderr
)
pollUntil(() => messages.slice(startIndex).find(predicate), label, streams.stderr)
const request = async (method, params = {}) => {
const id = nextSequence++
@@ -317,8 +311,10 @@ async function main() {
)
await relay.request('fs.watch', { rootPath: watchRoot })
const firstWatcherPid = await waitForWatcherPid(pidFile, undefined, () =>
`${daemonStreams.stderr()}\n${relay.stderr()}`
const firstWatcherPid = await waitForWatcherPid(
pidFile,
undefined,
() => `${daemonStreams.stderr()}\n${relay.stderr()}`
)
const beforePath = join(watchRoot, 'before.txt')
startIndex = relay.messageCount()
@@ -330,8 +326,10 @@ async function main() {
const faultSignal = process.platform === 'win32' ? 'SIGTERM' : 'SIGSEGV'
startIndex = relay.messageCount()
process.kill(firstWatcherPid, faultSignal)
const replacementWatcherPid = await waitForWatcherPid(pidFile, firstWatcherPid, () =>
`${daemonStreams.stderr()}\n${relay.stderr()}`
const replacementWatcherPid = await waitForWatcherPid(
pidFile,
firstWatcherPid,
() => `${daemonStreams.stderr()}\n${relay.stderr()}`
)
await relay.waitForNotification(startIndex, 'fs.changed', (params) =>
Array.isArray(params.events)
@@ -29,11 +29,8 @@ describe('mobile source control status helpers', () => {
it('builds sections in the mobile source control order', () => {
const sections = buildMobileSourceControlSections(entries)
expect(sections.map((section) => section.title)).toEqual([
'Changes',
'Untracked Files',
'Staged Changes'
])
expect(sections.map((section) => section.title)).toEqual(['Changes', 'Staged Changes'])
expect(sections[0]?.data.map((entry) => entry.path)).toEqual(['a.ts', 'new.ts'])
})
it('computes actionable path sets', () => {
@@ -19,7 +19,7 @@ export type MobileSourceControlSection<TEntry extends MobileGitStatusEntry = Mob
data: TEntry[]
}
const AREA_ORDER: MobileGitStagingArea[] = ['unstaged', 'untracked', 'staged']
const AREA_ORDER: MobileGitStagingArea[] = ['unstaged', 'staged']
const AREA_TITLES: Record<MobileGitStagingArea, string> = {
unstaged: 'Changes',
@@ -59,7 +59,13 @@ export function buildMobileSourceControlSections<TEntry extends MobileGitStatusE
return AREA_ORDER.map((area) => ({
area,
title: AREA_TITLES[area],
data: entries.filter((entry) => entry.area === area).sort(compareGitStatusEntries)
data: entries
.filter((entry) =>
area === 'unstaged'
? entry.area === 'unstaged' || entry.area === 'untracked'
: entry.area === area
)
.sort(compareGitStatusEntries)
})).filter((section) => section.data.length > 0)
}
@@ -105,7 +105,6 @@ function createSettings(overrides: TestSettingsOverrides = {}): GlobalSettings {
openLinksInAppPreferencePrompted: false,
rightSidebarOpenByDefault: true,
sourceControlViewMode: 'list',
sourceControlGroupOrder: 'changes-first',
sourceControlCompareAgainstUpstream: false,
showTitlebarAppName: true,
showTasksButton: true,
-1
View File
@@ -97,7 +97,6 @@ function createSettings(overrides: Partial<GlobalSettings> = {}): GlobalSettings
openLinksInAppPreferencePrompted: false,
rightSidebarOpenByDefault: true,
sourceControlViewMode: 'list',
sourceControlGroupOrder: 'changes-first',
sourceControlCompareAgainstUpstream: false,
showTitlebarAppName: true,
showTasksButton: true,
+16 -33
View File
@@ -676,7 +676,6 @@ describe('Store', () => {
const settings = store.getSettings()
expect(settings.branchPrefix).toBe('git-username')
expect(settings.refreshLocalBaseRefOnWorktreeCreate).toBe(false)
expect(settings.sourceControlGroupOrder).toBe('changes-first')
expect(settings.theme).toBe('system')
expect(settings.appIcon).toBe('classic')
expect(settings.appFontFamily).toBe('Geist')
@@ -2784,21 +2783,6 @@ describe('Store', () => {
expect(store.getSettings().terminalShortcutPolicy).toBe('orca-first')
})
it('normalizes malformed source control group order on load', async () => {
writeDataFile({
schemaVersion: 1,
repos: [],
worktreeMeta: {},
settings: { sourceControlGroupOrder: 'tracked-first' },
ui: {},
githubCache: { pr: {}, issue: {} },
workspaceSession: {}
})
const store = await createStore()
expect(store.getSettings().sourceControlGroupOrder).toBe('changes-first')
})
it('repairs drifted task provider defaults on load', async () => {
writeDataFile({
schemaVersion: 1,
@@ -5817,15 +5801,21 @@ describe('Store', () => {
expect(store.getSettings().sourceControlViewMode).toBe('tree')
})
it('updateSettings persists sourceControlGroupOrder as a user setting', async () => {
it('drops retired Source Control order preferences', async () => {
writeDataFile({
...getDefaultPersistedState(testState.dir),
settings: {
...getDefaultPersistedState(testState.dir).settings,
sourceControlGroupOrder: 'changes-first',
sourceControlHierarchyDefaultedV2: true
}
} as never)
const store = await createStore()
expect(store.getSettings().sourceControlGroupOrder).toBe('changes-first')
store.updateSettings({ sourceControlGroupOrder: 'staged-first' })
expect(store.getSettings().sourceControlGroupOrder).toBe('staged-first')
store.updateSettings({ sourceControlGroupOrder: 'tracked-first' as never })
expect(store.getSettings().sourceControlGroupOrder).toBe('changes-first')
store.flush()
const persisted = readDataFile() as { settings?: Record<string, unknown> }
expect(persisted.settings).not.toHaveProperty('sourceControlGroupOrder')
expect(persisted.settings).not.toHaveProperty('sourceControlHierarchyDefaultedV2')
})
it('updateSettings normalizes terminal shortcut policy', async () => {
@@ -5883,18 +5873,15 @@ describe('Store', () => {
const store = await createStore()
expect(store.getSettings().sourceControlViewMode).toBe('list')
expect(store.getSettings().sourceControlGroupOrder).toBe('changes-first')
store.updateSettings({ sourceControlViewMode: 'tree', sourceControlGroupOrder: 'staged-first' })
store.updateSettings({ sourceControlViewMode: 'tree' })
store.flush()
const persisted = readDataFile() as {
settings?: { sourceControlGroupOrder?: string; sourceControlViewMode?: string }
settings?: { sourceControlViewMode?: string }
workspaceSession?: typeof workspaceSession
worktreeMeta?: Record<string, unknown>
}
expect(persisted.settings?.sourceControlViewMode).toBe('tree')
expect(persisted.settings?.sourceControlGroupOrder).toBe('staged-first')
expect(persisted.workspaceSession).toEqual({
...getDefaultWorkspaceSession(),
...workspaceSession
@@ -5906,13 +5893,9 @@ describe('Store', () => {
expect(collectPropertyPaths(persisted, 'sourceControlViewMode')).toEqual([
'settings.sourceControlViewMode'
])
expect(collectPropertyPaths(persisted, 'sourceControlGroupOrder')).toEqual([
'settings.sourceControlGroupOrder'
])
const reloaded = await createStore()
expect(reloaded.getSettings().sourceControlViewMode).toBe('tree')
expect(reloaded.getSettings().sourceControlGroupOrder).toBe('staged-first')
expect(reloaded.getWorkspaceSession().activeWorktreeId).toBe('repo1::/worktree-a')
})
+15 -21
View File
@@ -182,7 +182,6 @@ import {
} from '../shared/mobile-pairing-custom-address'
import { normalizeOpenInApplications } from '../shared/open-in-applications'
import { normalizeTerminalShortcutPolicy } from '../shared/keybindings'
import { normalizeSourceControlGroupOrder } from '../shared/source-control-group-order'
import { normalizeAppIconId } from '../shared/app-icon'
import { normalizeTerminalCustomThemes } from '../shared/terminal-custom-themes'
import {
@@ -650,12 +649,22 @@ function readLegacyTerminalScrollbackSettings(settings: unknown): LegacyTerminal
: {}
}
function stripLegacyTerminalScrollbackBytes(
function stripRetiredSettingsFields(
settings: Partial<GlobalSettings> | undefined
): Partial<GlobalSettings> {
const { terminalScrollbackBytes: _legacyScrollbackBytes, ...rest } = (settings ??
{}) as Partial<GlobalSettings> & { terminalScrollbackBytes?: unknown }
const {
terminalScrollbackBytes: _legacyScrollbackBytes,
sourceControlGroupOrder: _sourceControlGroupOrder,
sourceControlHierarchyDefaultedV2: _sourceControlHierarchyDefaultedV2,
...rest
} = (settings ?? {}) as Partial<GlobalSettings> & {
terminalScrollbackBytes?: unknown
sourceControlGroupOrder?: unknown
sourceControlHierarchyDefaultedV2?: unknown
}
void _legacyScrollbackBytes
void _sourceControlGroupOrder
void _sourceControlHierarchyDefaultedV2
return rest
}
@@ -3316,15 +3325,6 @@ export class Store {
) {
this.loadNeedsSave = true
}
const normalizedSourceControlGroupOrder = normalizeSourceControlGroupOrder(
parsed.settings?.sourceControlGroupOrder
)
if (
parsed.settings?.sourceControlGroupOrder !== undefined &&
parsed.settings.sourceControlGroupOrder !== normalizedSourceControlGroupOrder
) {
this.loadNeedsSave = true
}
result = {
...defaults,
...parsed,
@@ -3346,7 +3346,7 @@ export class Store {
settings: {
...defaults.settings,
// Why (#7977): keep persisted experimentalNewWorktreeCardStyle:true — v1.4.130's onboarding auto-wrote it as a plain boolean, so it's indistinguishable from a real opt-in; only the default changed.
...stripLegacyTerminalScrollbackBytes(parsed.settings),
...stripRetiredSettingsFields(parsed.settings),
prBotAuthorOverrides: normalizePRBotAuthorOverrides(
parsed.settings?.prBotAuthorOverrides
),
@@ -3420,7 +3420,6 @@ export class Store {
}),
notifications: normalizeNotificationSettings(parsed.settings?.notifications),
sourceControlAi: migratedSourceControlAi,
sourceControlGroupOrder: normalizedSourceControlGroupOrder,
// Why: rollback builds still read commitMessageAi, so refresh the legacy projection from sourceControlAi for compat.
commitMessageAi: projectSourceControlAiToLegacyCommitMessageAi(
migratedSourceControlAi,
@@ -5691,7 +5690,7 @@ export class Store {
updates: Partial<GlobalSettings>,
options: { notifyListeners?: boolean; originWebContentsId?: number } = {}
): GlobalSettings {
const sanitizedUpdates = stripLegacyTerminalScrollbackBytes(updates)
const sanitizedUpdates = stripRetiredSettingsFields(updates)
// Why: coerce to boolean here (not the IPC edge) so every write path is covered and a truthy non-bool can't persist as "tray-minimize on".
if ('minimizeToTrayOnClose' in updates) {
sanitizedUpdates.minimizeToTrayOnClose = updates.minimizeToTrayOnClose === true
@@ -5759,11 +5758,6 @@ export class Store {
updates.terminalShortcutPolicy
)
}
if ('sourceControlGroupOrder' in updates) {
sanitizedUpdates.sourceControlGroupOrder = normalizeSourceControlGroupOrder(
updates.sourceControlGroupOrder
)
}
if ('appIcon' in updates) {
sanitizedUpdates.appIcon = normalizeAppIconId(updates.appIcon)
}
+1 -4
View File
@@ -10762,10 +10762,7 @@ export class OrcaRuntimeService {
// A spawn published (or admission pending) this generation already
// attaches the provider stream; a replacement under a reused id must not
// read as the discovered never-attached session it replaced.
if (
this.spawnPublishedPtys.has(ptyId) ||
this.pendingPtyRegistrationIncarnations.has(ptyId)
) {
if (this.spawnPublishedPtys.has(ptyId) || this.pendingPtyRegistrationIncarnations.has(ptyId)) {
return false
}
// SSH panes have their own lease/reattach machinery.
@@ -10,7 +10,7 @@ import {
compactSourceControlTree,
flattenSourceControlTree
} from '@/components/right-sidebar/source-control-tree'
import type { GitBranchChangeEntry, GitStagingArea, GitStatusEntry } from '../../../../shared/types'
import type { GitBranchChangeEntry, GitStagingArea } from '../../../../shared/types'
import {
getEntryExtension,
getFilteredCombinedDiffFileTreeEntries,
@@ -22,6 +22,11 @@ import {
import { CombinedDiffFileTreeRow, type CombinedDiffTreeNode } from './combined-diff-file-tree-row'
import { useCombinedDiffFileTreeResize } from './use-combined-diff-file-tree-resize'
import { translate } from '@/i18n/i18n'
import {
mergeUntrackedIntoChanges,
SOURCE_CONTROL_GROUP_ORDER,
type SourceControlEntryGroups
} from '@/components/right-sidebar/source-control-section-order'
export {
createCombinedDiffSectionIndexMap,
@@ -30,7 +35,6 @@ export {
handleCombinedDiffFileTreeNavigation
} from './combined-diff-file-tree-model'
const UNCOMMITTED_AREA_ORDER: readonly GitStagingArea[] = ['unstaged', 'staged', 'untracked']
const UNCOMMITTED_AREA_LABELS: Record<GitStagingArea, string> = {
unstaged: 'Changes',
staged: 'Staged Changes',
@@ -39,26 +43,35 @@ const UNCOMMITTED_AREA_LABELS: Record<GitStagingArea, string> = {
function buildUncommittedRows(
entries: readonly CombinedDiffFileTreeEntry[],
collapsedDirectoryKeys: ReadonlySet<string>
collapsedDirectoryKeys: ReadonlySet<string>,
areaOrder: readonly GitStagingArea[]
): { area: GitStagingArea; label: string; rows: CombinedDiffTreeNode[] }[] {
return UNCOMMITTED_AREA_ORDER.map((area) => {
const areaEntries = entries.filter(
(entry): entry is GitStatusEntry => isGitStatusEntry(entry) && entry.area === area
)
if (areaEntries.length === 0) {
return null
const groups: SourceControlEntryGroups = { staged: [], unstaged: [], untracked: [] }
for (const entry of entries) {
if (isGitStatusEntry(entry)) {
groups[entry.area].push(entry)
}
}
const displayGroups = mergeUntrackedIntoChanges(groups)
const roots = compactSourceControlTree(buildGitStatusSourceControlTree(area, areaEntries))
return {
area,
label: UNCOMMITTED_AREA_LABELS[area],
rows: flattenSourceControlTree(roots, collapsedDirectoryKeys) as CombinedDiffTreeNode[]
}
}).filter(
(group): group is { area: GitStagingArea; label: string; rows: CombinedDiffTreeNode[] } =>
Boolean(group)
)
return areaOrder
.map((area) => {
const areaEntries = displayGroups[area]
if (areaEntries.length === 0) {
return null
}
const roots = compactSourceControlTree(buildGitStatusSourceControlTree(area, areaEntries))
return {
area,
label: UNCOMMITTED_AREA_LABELS[area],
rows: flattenSourceControlTree(roots, collapsedDirectoryKeys) as CombinedDiffTreeNode[]
}
})
.filter(
(group): group is { area: GitStagingArea; label: string; rows: CombinedDiffTreeNode[] } =>
Boolean(group)
)
}
function buildBranchRows(
@@ -153,7 +166,7 @@ export function CombinedDiffFileTree({
const uncommittedGroups = React.useMemo(
() =>
mode === 'all' || mode === 'uncommitted'
? buildUncommittedRows(filteredEntries, collapsedDirectoryKeys)
? buildUncommittedRows(filteredEntries, collapsedDirectoryKeys, SOURCE_CONTROL_GROUP_ORDER)
: [],
[collapsedDirectoryKeys, filteredEntries, mode]
)
@@ -13,7 +13,10 @@ export function GitHubMarkdownComposerEditorPane({
const scrollContainerRef = useRef<HTMLDivElement | null>(null)
return (
<div ref={scrollContainerRef} className="relative max-h-[360px] overflow-y-auto scrollbar-sleek">
<div
ref={scrollContainerRef}
className="relative max-h-[360px] overflow-y-auto scrollbar-sleek"
>
<EditorContent editor={editor} />
<RichMarkdownTableControls
disabled={disabled}
@@ -111,8 +111,9 @@ import { useSourceControlSubmoduleStatus } from './useSourceControlSubmoduleStat
import {
buildSourceControlDisplaySections,
getSourceControlSectionViewAction,
resolveSourceControlGroupOrder,
mergeUntrackedIntoChanges,
SOURCE_CONTROL_AREAS,
SOURCE_CONTROL_GROUP_ORDER,
type SourceControlDisplaySectionId,
type SourceControlEntryGroups,
type SourceControlSectionArea
@@ -644,6 +645,11 @@ type SourceControlDirectoryActionPaths = {
stagePaths: string[]
unstagePaths: string[]
discardPaths: string[]
discardHasUntracked: boolean
}
function discardAllAreaFilter(area: DiscardAllArea): DiscardAllArea | readonly DiscardAllArea[] {
return area === 'unstaged' ? (['unstaged', 'untracked'] as const) : area
}
function getSourceControlDirectoryActionPaths(
@@ -655,8 +661,9 @@ function getSourceControlDirectoryActionPaths(
unstagePaths: getUnstageAllPaths(entries),
discardPaths:
node.area === 'unstaged' || node.area === 'untracked'
? getDiscardAllPaths(entries, node.area)
: []
? getDiscardAllPaths(entries, discardAllAreaFilter(node.area))
: [],
discardHasUntracked: entries.some((entry) => entry.area === 'untracked')
}
}
@@ -1060,7 +1067,6 @@ function SourceControlInner(): React.JSX.Element {
settings?.sourceControlViewMode
)
const sourceControlViewMode = persistedSourceControlViewMode
const sourceControlGroupOrder = resolveSourceControlGroupOrder(settings?.sourceControlGroupOrder)
const [collapsedTreeDirs, setCollapsedTreeDirs] = useState<Set<string>>(new Set())
const [baseRefDialogOpen, setBaseRefDialogOpen] = useState(false)
const [pendingDiscard, setPendingDiscard] = useState<PendingDiscardConfirmation | null>(null)
@@ -1796,13 +1802,19 @@ function SourceControlInner(): React.JSX.Element {
[fileFilterState, grouped]
)
const mergedGrouped = useMemo(() => mergeUntrackedIntoChanges(grouped), [grouped])
const mergedFilteredGrouped = useMemo(
() => mergeUntrackedIntoChanges(filteredGrouped),
[filteredGrouped]
)
const displaySections = useMemo(
() => buildSourceControlDisplaySections(filteredGrouped, sourceControlGroupOrder),
[filteredGrouped, sourceControlGroupOrder]
() => buildSourceControlDisplaySections(mergedFilteredGrouped, SOURCE_CONTROL_GROUP_ORDER),
[mergedFilteredGrouped]
)
const unfilteredDisplaySections = useMemo(
() => buildSourceControlDisplaySections(grouped, sourceControlGroupOrder),
[grouped, sourceControlGroupOrder]
() => buildSourceControlDisplaySections(mergedGrouped, SOURCE_CONTROL_GROUP_ORDER),
[mergedGrouped]
)
const unfilteredDisplaySectionsById = useMemo(
() => new Map(unfilteredDisplaySections.map((section) => [section.id, section])),
@@ -5400,7 +5412,9 @@ function SourceControlInner(): React.JSX.Element {
if (!worktreePath || !activeWorktreeId || isExecutingBulk) {
return
}
const paths = confirmedPaths ? [...confirmedPaths] : getDiscardAllPaths(grouped[area], area)
const paths = confirmedPaths
? [...confirmedPaths]
: getDiscardAllPaths(mergedGrouped[area], discardAllAreaFilter(area))
if (paths.length === 0) {
return
}
@@ -5472,7 +5486,7 @@ function SourceControlInner(): React.JSX.Element {
activeRepoSettings,
worktreePath,
activeWorktreeId,
grouped,
mergedGrouped,
isExecutingBulk,
clearSelection,
discardMany,
@@ -5486,13 +5500,21 @@ function SourceControlInner(): React.JSX.Element {
if (!worktreePath || !activeWorktreeId || isExecutingBulk) {
return
}
const paths = confirmedPaths ? [...confirmedPaths] : getDiscardAllPaths(grouped[area], area)
const paths = confirmedPaths
? [...confirmedPaths]
: getDiscardAllPaths(mergedGrouped[area], discardAllAreaFilter(area))
if (paths.length === 0) {
return
}
setPendingDiscard({ kind: 'area', area, paths })
setPendingDiscard({
kind: 'area',
area,
paths,
hasUntracked:
area === 'unstaged' && mergedGrouped.unstaged.some((entry) => entry.area === 'untracked')
})
},
[activeWorktreeId, grouped, isExecutingBulk, worktreePath]
[activeWorktreeId, isExecutingBulk, mergedGrouped, worktreePath]
)
const requestDiscardEntry = useCallback(
@@ -5914,7 +5936,8 @@ function SourceControlInner(): React.JSX.Element {
.filter(isStageableStatusEntry)
.map((entry) => entry.path)
const unstageAllPaths = getUnstageAllPaths(actionItems)
const discardAllPaths = getDiscardAllPaths(actionItems, area)
const discardAllPaths = getDiscardAllPaths(actionItems, discardAllAreaFilter(area))
const discardHasUntracked = actionItems.some((entry) => entry.area === 'untracked')
const canStageAll = !normalizedFilter && stageAllPaths.length > 0
const canUnstageAll = !normalizedFilter && unstageAllPaths.length > 0
const canRevertAll = !normalizedFilter && discardAllPaths.length > 0
@@ -5937,7 +5960,7 @@ function SourceControlInner(): React.JSX.Element {
<div className="flex items-center can-hover:opacity-0 transition-opacity group-hover/section:opacity-100 focus-within:opacity-100">
{canRevertAll && (
<ActionButton
icon={area === 'untracked' ? Trash : Undo2}
icon={discardHasUntracked ? Trash : Undo2}
// Why: for untracked files, discard deletes outright (rm -rf), so label the destructive variant explicitly.
title={
area === 'untracked'
@@ -5945,10 +5968,15 @@ function SourceControlInner(): React.JSX.Element {
'auto.components.right.sidebar.SourceControl.2f609a2e7c',
'Delete all untracked'
)
: translate(
'auto.components.right.sidebar.SourceControl.ce41708855',
'Discard all'
)
: discardHasUntracked
? translate(
'auto.components.right.sidebar.SourceControl.discardAllMixedUntracked',
'Discard changes and delete untracked files'
)
: translate(
'auto.components.right.sidebar.SourceControl.ce41708855',
'Discard all'
)
}
onClick={(event) => {
event.stopPropagation()
@@ -6055,11 +6083,12 @@ function SourceControlInner(): React.JSX.Element {
isExecutingBulk={isExecutingBulk}
isCollapsed={collapsedTreeDirs.has(node.key)}
onToggle={() => toggleTreeDir(node.key)}
onRequestDiscardPaths={(discardArea, paths) =>
onRequestDiscardPaths={(discardArea, paths, hasUntracked) =>
setPendingDiscard({
kind: 'area',
area: discardArea,
paths
paths,
hasUntracked
})
}
onStagePaths={handleStageAllPaths}
@@ -7698,7 +7727,11 @@ function SourceControlTreeDirectoryRow({
isExecutingBulk: boolean
isCollapsed: boolean
onToggle: () => void
onRequestDiscardPaths: (area: DiscardAllArea, paths: readonly string[]) => void
onRequestDiscardPaths: (
area: DiscardAllArea,
paths: readonly string[],
hasUntracked: boolean
) => void
onStagePaths: (paths: readonly string[]) => Promise<void>
onUnstagePaths: (paths: readonly string[]) => Promise<void>
}): React.JSX.Element {
@@ -7737,21 +7770,30 @@ function SourceControlTreeDirectoryRow({
<div className={SOURCE_CONTROL_ROW_ACTION_OVERLAY_CLASS}>
{canDiscard && (
<ActionButton
icon={node.area === 'untracked' ? Trash : Undo2}
icon={actionPaths.discardHasUntracked ? Trash : Undo2}
title={
node.area === 'untracked'
? translate(
'auto.components.right.sidebar.SourceControl.9b367363b6',
'Delete untracked in folder'
)
: translate(
'auto.components.right.sidebar.SourceControl.6d7f2a47e5',
'Discard folder'
)
: actionPaths.discardHasUntracked
? translate(
'auto.components.right.sidebar.SourceControl.discardFolderMixedUntracked',
'Discard changes and delete untracked files in folder'
)
: translate(
'auto.components.right.sidebar.SourceControl.6d7f2a47e5',
'Discard folder'
)
}
onClick={(event) => {
event.stopPropagation()
onRequestDiscardPaths(node.area, actionPaths.discardPaths)
onRequestDiscardPaths(
node.area,
actionPaths.discardPaths,
actionPaths.discardHasUntracked
)
}}
disabled={isExecutingBulk}
/>
@@ -30,6 +30,15 @@ describe('getDiscardAllPaths', () => {
expect(getDiscardAllPaths(entries, 'untracked')).toEqual(['c.ts'])
})
it('accepts mixed Changes areas when untracked files are combined', () => {
const entries: GitStatusEntry[] = [
entry({ path: 'changed.ts', area: 'unstaged' }),
entry({ path: 'new.ts', area: 'untracked', status: 'untracked' }),
entry({ path: 'ready.ts', area: 'staged' })
]
expect(getDiscardAllPaths(entries, ['unstaged', 'untracked'])).toEqual(['changed.ts', 'new.ts'])
})
it('skips entries with an unresolved conflict', () => {
const entries: GitStatusEntry[] = [
entry({ path: 'clean.ts', area: 'unstaged' }),
@@ -9,12 +9,13 @@ export type DiscardAllArea = 'staged' | 'unstaged' | 'untracked'
*/
export function getDiscardAllPaths(
entries: readonly GitStatusEntry[],
area: DiscardAllArea
area: DiscardAllArea | readonly DiscardAllArea[]
): string[] {
const areas = Array.isArray(area) ? area : [area]
return entries
.filter(
(entry) =>
entry.area === area &&
areas.includes(entry.area) &&
entry.conflictStatus !== 'unresolved' &&
entry.conflictStatus !== 'resolved_locally'
)
@@ -111,4 +111,13 @@ describe('getDiscardAreaConfirmationCopy', () => {
confirmLabel: 'Discard all'
})
})
it('discloses permanent deletion for combined Changes', () => {
expect(getDiscardAreaConfirmationCopy('unstaged', 3, true)).toEqual({
title: 'Discard changes and delete untracked files?',
description:
'Tracked changes will be reverted and untracked files will be permanently deleted. This cannot be undone.',
confirmLabel: 'Delete and discard'
})
})
})
@@ -62,8 +62,25 @@ export function getDiscardEntryConfirmationCopy(
export function getDiscardAreaConfirmationCopy(
area: DiscardAllArea,
count: number
count: number,
hasUntracked = false
): DiscardConfirmationCopy {
if (area === 'unstaged' && hasUntracked) {
return {
title: translate(
'auto.components.right.sidebar.source.control.discard.confirmation.mixedTitle',
'Discard changes and delete untracked files?'
),
description: translate(
'auto.components.right.sidebar.source.control.discard.confirmation.mixedDescription',
'Tracked changes will be reverted and untracked files will be permanently deleted. This cannot be undone.'
),
confirmLabel: translate(
'auto.components.right.sidebar.source.control.discard.confirmation.mixedConfirm',
'Delete and discard'
)
}
}
switch (area) {
case 'untracked':
return {
@@ -20,7 +20,12 @@ import { translate } from '@/i18n/i18n'
export type PendingDiscardConfirmation =
| { kind: 'entry'; entry: GitStatusEntry }
| { kind: 'area'; area: DiscardAllArea; paths: readonly string[] }
| {
kind: 'area'
area: DiscardAllArea
paths: readonly string[]
hasUntracked?: boolean
}
export function focusDiscardDialogConfirmButton(
event: Event,
@@ -51,7 +56,11 @@ export function SourceControlDiscardDialog({
if (pendingDiscard.kind === 'entry') {
return getDiscardEntryConfirmationCopy(pendingDiscard.entry)
}
return getDiscardAreaConfirmationCopy(pendingDiscard.area, pendingDiscard.paths.length)
return getDiscardAreaConfirmationCopy(
pendingDiscard.area,
pendingDiscard.paths.length,
pendingDiscard.hasUntracked
)
}, [pendingDiscard])
const PendingDiscardIcon = pendingDiscardCopy?.confirmLabel.startsWith('Delete') ? Trash : Undo2
@@ -4,7 +4,8 @@ import {
buildSourceControlDisplaySections,
getConflictReviewEntries,
getSourceControlSectionViewAction,
resolveSourceControlGroupOrder,
mergeUntrackedIntoChanges,
SOURCE_CONTROL_GROUP_ORDER,
splitPinnedSourceControlConflicts,
type SourceControlEntryGroups
} from './source-control-section-order'
@@ -26,37 +27,38 @@ function groups(partial: Partial<SourceControlEntryGroups>): SourceControlEntryG
}
}
describe('resolveSourceControlGroupOrder', () => {
it('keeps Changes first by default', () => {
expect(resolveSourceControlGroupOrder(undefined)).toEqual(['unstaged', 'staged', 'untracked'])
describe('SOURCE_CONTROL_GROUP_ORDER', () => {
it('follows the edit, stage, commit workflow', () => {
expect(SOURCE_CONTROL_GROUP_ORDER).toEqual(['unstaged', 'staged', 'untracked'])
})
})
it('supports staged-first and untracked-first presets', () => {
expect(resolveSourceControlGroupOrder('staged-first')).toEqual([
'staged',
'unstaged',
'untracked'
])
expect(resolveSourceControlGroupOrder('untracked-first')).toEqual([
'untracked',
'unstaged',
'staged'
])
describe('mergeUntrackedIntoChanges', () => {
it('folds untracked entries into Changes without changing their Git area', () => {
const unstaged = entry({ area: 'unstaged', path: 'changed.ts' })
const untracked = entry({ area: 'untracked', path: 'new.ts', status: 'untracked' })
const merged = mergeUntrackedIntoChanges(
groups({ unstaged: [unstaged], untracked: [untracked] })
)
expect(merged.unstaged).toEqual([unstaged, untracked])
expect(merged.untracked).toEqual([])
expect(merged.unstaged[1]?.area).toBe('untracked')
})
})
describe('buildSourceControlDisplaySections', () => {
it('uses the configured order for normal sections', () => {
it('uses the fixed workflow order for normal sections', () => {
const sections = buildSourceControlDisplaySections(
groups({
staged: [entry({ area: 'staged', path: 'staged.ts' })],
unstaged: [entry({ area: 'unstaged', path: 'changed.ts' })],
untracked: [entry({ area: 'untracked', path: 'new.ts', status: 'untracked' })]
}),
resolveSourceControlGroupOrder('staged-first')
SOURCE_CONTROL_GROUP_ORDER
)
expect(sections.map((section) => section.id)).toEqual(['staged', 'unstaged', 'untracked'])
expect(sections.map((section) => section.id)).toEqual(['unstaged', 'staged', 'untracked'])
})
it('keeps conflicts pinned before the configured normal order', () => {
@@ -74,13 +76,13 @@ describe('buildSourceControlDisplaySections', () => {
],
untracked: [entry({ area: 'untracked', path: 'new.ts', status: 'untracked' })]
}),
resolveSourceControlGroupOrder('staged-first')
SOURCE_CONTROL_GROUP_ORDER
)
expect(sections.map((section) => section.id)).toEqual([
'conflicts',
'staged',
'unstaged',
'staged',
'untracked'
])
})
@@ -100,10 +102,7 @@ describe('buildSourceControlDisplaySections', () => {
const input = groups({ unstaged: [unresolved, resolved, normal] })
const split = splitPinnedSourceControlConflicts(input)
const sections = buildSourceControlDisplaySections(
input,
resolveSourceControlGroupOrder('changes-first')
)
const sections = buildSourceControlDisplaySections(input, SOURCE_CONTROL_GROUP_ORDER)
expect(split.pinnedConflicts.map((item) => item.path)).toEqual(['conflict.ts', 'resolved.ts'])
expect(split.normalGroups.unstaged.map((item) => item.path)).toEqual(['normal.ts'])
@@ -123,10 +122,7 @@ describe('buildSourceControlDisplaySections', () => {
const input = groups({ staged: [resolvedStaged, staged] })
const split = splitPinnedSourceControlConflicts(input)
const sections = buildSourceControlDisplaySections(
input,
resolveSourceControlGroupOrder('staged-first')
)
const sections = buildSourceControlDisplaySections(input, SOURCE_CONTROL_GROUP_ORDER)
expect(split.pinnedConflicts).toEqual([resolvedStaged])
expect(split.normalGroups.staged).toEqual([staged])
@@ -167,7 +163,7 @@ describe('buildSourceControlDisplaySections', () => {
entry({ area: 'unstaged', path: 'normal.ts' })
]
}),
resolveSourceControlGroupOrder('changes-first')
SOURCE_CONTROL_GROUP_ORDER
)
expect(getSourceControlSectionViewAction(sections[0]!)).toEqual({
@@ -191,7 +187,7 @@ describe('buildSourceControlDisplaySections', () => {
const normal = entry({ area: 'unstaged', path: 'normal.ts' })
const sections = buildSourceControlDisplaySections(
groups({ unstaged: [pinned, normal] }),
resolveSourceControlGroupOrder('changes-first')
SOURCE_CONTROL_GROUP_ORDER
)
expect(getSourceControlSectionViewAction(sections[1]!)).toEqual({
@@ -212,7 +208,7 @@ describe('buildSourceControlDisplaySections', () => {
groups({
unstaged: [resolved]
}),
resolveSourceControlGroupOrder('changes-first')
SOURCE_CONTROL_GROUP_ORDER
)
expect(getSourceControlSectionViewAction(sections[0]!)).toEqual({
@@ -240,7 +236,7 @@ describe('buildSourceControlDisplaySections', () => {
staged: [staged],
unstaged: [unstaged]
}),
resolveSourceControlGroupOrder('staged-first')
SOURCE_CONTROL_GROUP_ORDER
)
expect(getSourceControlSectionViewAction(sections[0]!)).toEqual({
@@ -1,5 +1,4 @@
import { normalizeSourceControlGroupOrder } from '../../../../shared/source-control-group-order'
import type { GitStatusEntry, SourceControlGroupOrder } from '../../../../shared/types'
import type { GitStatusEntry } from '../../../../shared/types'
export const SOURCE_CONTROL_AREAS = ['unstaged', 'staged', 'untracked'] as const
export type SourceControlSectionArea = (typeof SOURCE_CONTROL_AREAS)[number]
@@ -22,16 +21,23 @@ export type SourceControlSectionViewAction =
| { kind: 'conflict-review'; entries: SourceControlConflictReviewEntry[] }
| { kind: 'combined-diff'; area?: SourceControlSectionArea; entries: GitStatusEntry[] }
const ORDER_BY_PRESET: Record<SourceControlGroupOrder, readonly SourceControlSectionArea[]> = {
'changes-first': ['unstaged', 'staged', 'untracked'],
'staged-first': ['staged', 'unstaged', 'untracked'],
'untracked-first': ['untracked', 'unstaged', 'staged']
}
export const SOURCE_CONTROL_GROUP_ORDER: readonly SourceControlSectionArea[] = [
'unstaged',
'staged',
'untracked'
]
export function resolveSourceControlGroupOrder(
value: SourceControlGroupOrder | null | undefined
): readonly SourceControlSectionArea[] {
return ORDER_BY_PRESET[normalizeSourceControlGroupOrder(value)]
export function mergeUntrackedIntoChanges(
groups: SourceControlEntryGroups
): SourceControlEntryGroups {
if (groups.untracked.length === 0) {
return groups
}
return {
staged: groups.staged,
unstaged: [...groups.unstaged, ...groups.untracked],
untracked: []
}
}
export function isPinnedConflictEntry(entry: GitStatusEntry): boolean {
@@ -6,12 +6,7 @@ import { getDefaultSettings } from '../../../../shared/constants'
import { translate } from '../../i18n/i18n'
import { useAppStore } from '../../store'
import { shouldOpenAutoRenameBranchAdvanced } from './AutoRenameBranchFromWorkSetting'
import {
GitPane,
SourceControlGroupOrderSetting,
getGitPaneSearchEntries,
shouldShowAutoRenameBranchSetting
} from './GitPane'
import { GitPane, getGitPaneSearchEntries, shouldShowAutoRenameBranchSetting } from './GitPane'
import { TooltipProvider } from '../ui/tooltip'
import { matchesSettingsSearch } from './settings-search'
import { SettingsSegmentedControl } from './SettingsFormControls'
@@ -40,23 +35,6 @@ function visit(node: unknown, cb: (node: ReactElementLike) => void): void {
}
}
function findSegmentedControl(node: unknown): ReactElementLike {
let found: ReactElementLike | null = null
const label = translate(
'auto.components.settings.GitPane.sourceControlGroupOrderTitle',
'Source Control Group Order'
)
visit(node, (entry) => {
if (entry.type === SettingsSegmentedControl && entry.props.ariaLabel === label) {
found = entry
}
})
if (!found) {
throw new Error('segmented control not found')
}
return found
}
function findCompareBaseSegmentedControl(node: unknown): ReactElementLike {
let found: ReactElementLike | null = null
const label = translate(
@@ -136,52 +114,6 @@ describe('GitPane', () => {
expect(markup).not.toContain('Refresh Local Base Ref')
})
it('renders Source Control group order in Git settings', () => {
const markup = renderGitPane('group order')
expect(markup).toContain(
translate(
'auto.components.settings.GitPane.sourceControlGroupOrderTitle',
'Source Control Group Order'
)
)
expect(markup).toContain(
translate('auto.components.settings.GitPane.changesFirst', 'Changes first')
)
expect(markup).toContain(
translate('auto.components.settings.GitPane.stagedFirst', 'Staged first')
)
expect(markup).toContain(
translate('auto.components.settings.GitPane.untrackedFirst', 'Untracked first')
)
})
it('updates Source Control group order only when the selected option changes', () => {
const updateSettings = vi.fn()
const element = SourceControlGroupOrderSetting({
settings: {
...getDefaultSettings(os.homedir()),
sourceControlGroupOrder: 'changes-first'
},
updateSettings
})
const control = findSegmentedControl(element)
const onChange = control.props.onChange as (value: string) => void
onChange('staged-first')
expect(updateSettings).toHaveBeenCalledWith({ sourceControlGroupOrder: 'staged-first' })
updateSettings.mockClear()
onChange('changes-first')
expect(updateSettings).not.toHaveBeenCalled()
})
it('includes Source Control group order search metadata', () => {
expect(matchesSettingsSearch('staged', getGitPaneSearchEntries())).toBe(true)
expect(matchesSettingsSearch('group order', getGitPaneSearchEntries())).toBe(true)
})
it('renders the default compare base setting in Git settings', () => {
const markup = renderGitPane('compare base')
@@ -1,7 +1,6 @@
import { useEffect, useRef, useState } from 'react'
import type { GlobalSettings, SourceControlGroupOrder } from '../../../../shared/types'
import type { GlobalSettings } from '../../../../shared/types'
import type { SourceControlAiSettingsPatch } from '../../../../shared/source-control-ai-types'
import { DEFAULT_SOURCE_CONTROL_GROUP_ORDER } from '../../../../shared/source-control-group-order'
import { Input } from '../ui/input'
import { Label } from '../ui/label'
import { useAppStore } from '../../store'
@@ -20,7 +19,6 @@ import {
getKeepLocalMainUpToDateTitle
} from './keep-local-main-up-to-date-setting'
import { translate } from '@/i18n/i18n'
import { SettingsRow, SettingsSegmentedControl } from './SettingsFormControls'
export { getGitPaneSearchEntries }
@@ -40,14 +38,6 @@ const KEEP_LOCAL_MAIN_UP_TO_DATE_KEYWORDS = [
'safely',
'worktree'
]
const SOURCE_CONTROL_GROUP_ORDER_KEYWORDS = [
'group order',
'changes first',
'staged first',
'untracked first',
'source control',
'git changes'
]
export function shouldShowAutoRenameBranchSetting(
searchQuery: string,
@@ -70,68 +60,6 @@ type GitPaneProps = {
settingsSearchQuery?: string
}
export function SourceControlGroupOrderSetting({
settings,
updateSettings
}: {
settings: GlobalSettings
updateSettings: (updates: Partial<GlobalSettings>) => void | Promise<void>
}): React.JSX.Element {
const value = settings.sourceControlGroupOrder ?? DEFAULT_SOURCE_CONTROL_GROUP_ORDER
const title = translate(
'auto.components.settings.GitPane.sourceControlGroupOrderTitle',
'Source Control Group Order'
)
const description = translate(
'auto.components.settings.GitPane.sourceControlGroupOrderDescription',
'Choose whether Changes, Staged Changes, or Untracked Files appear first in Source Control.'
)
return (
<SearchableSetting
title={title}
description={description}
keywords={SOURCE_CONTROL_GROUP_ORDER_KEYWORDS}
className="max-w-none"
>
<SettingsRow
label={title}
description={description}
alignTop
control={
<SettingsSegmentedControl<SourceControlGroupOrder>
value={value}
onChange={(nextValue) => {
if (nextValue !== value) {
void updateSettings({ sourceControlGroupOrder: nextValue })
}
}}
ariaLabel={title}
size="sm"
options={[
{
value: 'changes-first',
label: translate('auto.components.settings.GitPane.changesFirst', 'Changes first')
},
{
value: 'staged-first',
label: translate('auto.components.settings.GitPane.stagedFirst', 'Staged first')
},
{
value: 'untracked-first',
label: translate(
'auto.components.settings.GitPane.untrackedFirst',
'Untracked first'
)
}
]}
/>
}
/>
</SearchableSetting>
)
}
export function GitPane({
settings,
updateSettings,
@@ -297,23 +225,6 @@ export function GitPane({
</button>
</SearchableSetting>
) : null,
matchesSettingsSearch(searchQuery, {
title: translate(
'auto.components.settings.GitPane.sourceControlGroupOrderTitle',
'Source Control Group Order'
),
description: translate(
'auto.components.settings.GitPane.sourceControlGroupOrderDescription',
'Choose whether Changes, Staged Changes, or Untracked Files appear first in Source Control.'
),
keywords: SOURCE_CONTROL_GROUP_ORDER_KEYWORDS
}) ? (
<SourceControlGroupOrderSetting
key="source-control-group-order"
settings={settings}
updateSettings={updateSettings}
/>
) : null,
compareAgainstUpstreamMatchesSearch(searchQuery) ? (
<CompareAgainstUpstreamSetting
key="compare-against-upstream"
@@ -43,33 +43,6 @@ export const getGitPaneSearchEntries = createLocalizedCatalog(() => [
...translateSearchKeyword('auto.components.settings.git.search.035134fcd9', 'worktree')
]
},
{
title: translate(
'auto.components.settings.git.search.sourceControlGroupOrderTitle',
'Source Control Group Order'
),
description: translate(
'auto.components.settings.git.search.sourceControlGroupOrderDescription',
'Choose whether Changes, Staged Changes, or Untracked Files appear first in Source Control.'
),
keywords: [
...translateSearchKeyword('auto.components.settings.git.search.groupOrder', 'group order'),
...translateSearchKeyword(
'auto.components.settings.git.search.changesFirst',
'changes first'
),
...translateSearchKeyword('auto.components.settings.git.search.stagedFirst', 'staged first'),
...translateSearchKeyword(
'auto.components.settings.git.search.untrackedFirst',
'untracked first'
),
...translateSearchKeyword(
'auto.components.settings.git.search.sourceControl',
'source control'
),
...translateSearchKeyword('auto.components.settings.git.search.gitChanges', 'git changes')
]
},
{
title: translate(
'auto.components.settings.git.search.compareAgainstUpstreamTitle',
+6 -13
View File
@@ -6183,11 +6183,6 @@
"3d172725cc": "None",
"1f32ba27a6": "Custom",
"a182c5125e": "Git Username",
"sourceControlGroupOrderTitle": "Source Control Group Order",
"sourceControlGroupOrderDescription": "Choose whether Changes, Staged Changes, or Untracked Files appear first in Source Control.",
"changesFirst": "Changes first",
"stagedFirst": "Staged first",
"untrackedFirst": "Untracked first",
"compareAgainstUpstreamTitle": "Default Compare Base",
"compareAgainstUpstreamDescription": "Choose which base Source Control uses by default for committed-change comparisons. Branch upstream follows the current branch automatically and falls back to the repository default branch when no upstream exists. You can still change the compare base per worktree from that worktree's Git panel. Pull Request and rebase targets don't change.",
"compareBaseRepositoryDefault": "Repository default",
@@ -8463,14 +8458,7 @@
"f83c8937c4": "branch naming",
"5ecd91c5ef": "Prefix added to branch names when creating worktrees.",
"68bd65fdb8": "Branch Prefix",
"sourceControlGroupOrderTitle": "Source Control Group Order",
"sourceControlGroupOrderDescription": "Choose whether Changes, Staged Changes, or Untracked Files appear first in Source Control.",
"groupOrder": "group order",
"changesFirst": "changes first",
"stagedFirst": "staged first",
"untrackedFirst": "untracked first",
"sourceControl": "source control",
"gitChanges": "git changes",
"compareAgainstUpstreamTitle": "Default Compare Base",
"compareAgainstUpstreamDescription": "Choose which base Source Control uses by default for committed-change comparisons. Branch upstream follows the current branch automatically and falls back to the repository default branch when no upstream exists. You can still change the compare base per worktree from that worktree's Git panel. Pull Request and rebase targets don't change.",
"compareBase": "compare base",
@@ -10805,6 +10793,8 @@
"a4e93c21d7": "Current branch: {{value0}}",
"c7d4e2f801": "Change base ref: {{value0}}",
"f3a1b8c204": "upstream",
"discardAllMixedUntracked": "Discard changes and delete untracked files",
"discardFolderMixedUntracked": "Discard changes and delete untracked files in folder",
"createPrIntentEmptyGeneratedBody": "Generated review details did not include a description. Retry Create PR.",
"createPrIntentGenerateDetailsFailed": "Could not generate review details. Retry Create PR."
},
@@ -11230,7 +11220,10 @@
"40e9357b2a": "This will restore the file from HEAD and discard the deletion. This cannot be undone.",
"5c0bdbc4cb": "Restore \"{{value0}}\"?",
"d97bf697c9": "This will permanently delete this file. This cannot be undone.",
"96c772bee9": "Delete \"{{value0}}\"?"
"96c772bee9": "Delete \"{{value0}}\"?",
"mixedTitle": "Discard changes and delete untracked files?",
"mixedDescription": "Tracked changes will be reverted and untracked files will be permanently deleted. This cannot be undone.",
"mixedConfirm": "Delete and discard"
},
"dialog": {
"3bc61dc989": "Cancel",
-12
View File
@@ -5966,11 +5966,6 @@
"3d172725cc": "Ninguno",
"1f32ba27a6": "Personalizado",
"a182c5125e": "Usuario de Git",
"sourceControlGroupOrderTitle": "Orden de grupos de control de código fuente",
"sourceControlGroupOrderDescription": "Elige si Cambios, Cambios preparados o Archivos sin seguimiento aparecen primero en Control de código fuente.",
"changesFirst": "Cambios primero",
"stagedFirst": "Cambios preparados primero",
"untrackedFirst": "Archivos sin seguimiento primero",
"compareAgainstUpstreamTitle": "Base de comparación predeterminada",
"compareAgainstUpstreamDescription": "Elige qué base usa Control de código fuente de forma predeterminada para las comparaciones de cambios confirmados. El upstream de la rama sigue automáticamente la rama actual y recurre a la rama predeterminada del repositorio cuando no hay upstream. Aun así, puedes cambiar la base de comparación por worktree desde el panel Git de ese worktree. Los objetivos de Pull Request y rebase no cambian.",
"compareBaseRepositoryDefault": "Predeterminada del repositorio",
@@ -8236,14 +8231,7 @@
"f83c8937c4": "nombres de ramas",
"5ecd91c5ef": "Prefijo agregado a los nombres de ramas al crear worktrees.",
"68bd65fdb8": "Prefijo de rama",
"sourceControlGroupOrderTitle": "Orden de grupos de control de código fuente",
"sourceControlGroupOrderDescription": "Elige si Cambios, Cambios preparados o Archivos sin seguimiento aparecen primero en Control de código fuente.",
"groupOrder": "orden de grupos",
"changesFirst": "cambios primero",
"stagedFirst": "cambios preparados primero",
"untrackedFirst": "archivos sin seguimiento primero",
"sourceControl": "control de código fuente",
"gitChanges": "cambios de git",
"compareAgainstUpstreamTitle": "Base de comparación predeterminada",
"compareAgainstUpstreamDescription": "Elige qué base usa Control de código fuente de forma predeterminada para las comparaciones de cambios confirmados. El upstream de la rama sigue automáticamente la rama actual y recurre a la rama predeterminada del repositorio cuando no hay upstream. Aun así, puedes cambiar la base de comparación por worktree desde el panel Git de ese worktree. Los objetivos de Pull Request y rebase no cambian.",
"compareBase": "base de comparación",
-12
View File
@@ -5988,11 +5988,6 @@
"3d172725cc": "なし",
"1f32ba27a6": "カスタム",
"a182c5125e": "Gitのユーザー名",
"sourceControlGroupOrderTitle": "ソース管理のグループ順序",
"sourceControlGroupOrderDescription": "ソース管理で「変更」「ステージ済みの変更」「未追跡ファイル」のどれを先に表示するかを選択します。",
"changesFirst": "変更を先頭",
"stagedFirst": "ステージ済みを先頭",
"untrackedFirst": "未追跡を先頭",
"compareAgainstUpstreamTitle": "既定の比較ベース",
"compareAgainstUpstreamDescription": "ソース管理がコミット済み変更の比較に既定で使うベースを選択します。ブランチの上流は現在のブランチに自動的に追従し、上流がない場合はリポジトリの既定ブランチにフォールバックします。比較ベースは各 worktree の Git パネルから worktree ごとに変更できます。Pull Request やリベースの対象は変更されません。",
"compareBaseRepositoryDefault": "リポジトリ既定",
@@ -8258,14 +8253,7 @@
"f83c8937c4": "ブランチの命名",
"5ecd91c5ef": "ワークツリーの作成時にブランチ名に追加されるプレフィックス。",
"68bd65fdb8": "ブランチプレフィックス",
"sourceControlGroupOrderTitle": "ソース管理のグループ順序",
"sourceControlGroupOrderDescription": "ソース管理で「変更」「ステージ済みの変更」「未追跡ファイル」のどれを先に表示するかを選択します。",
"groupOrder": "グループ順序",
"changesFirst": "変更を先頭",
"stagedFirst": "ステージ済みを先頭",
"untrackedFirst": "未追跡を先頭",
"sourceControl": "ソース管理",
"gitChanges": "git の変更",
"compareAgainstUpstreamTitle": "既定の比較ベース",
"compareAgainstUpstreamDescription": "ソース管理がコミット済み変更の比較に既定で使うベースを選択します。ブランチの上流は現在のブランチに自動的に追従し、上流がない場合はリポジトリの既定ブランチにフォールバックします。比較ベースは各 worktree の Git パネルから worktree ごとに変更できます。Pull Request やリベースの対象は変更されません。",
"compareBase": "比較基準",
-12
View File
@@ -5951,11 +5951,6 @@
"3d172725cc": "없음",
"1f32ba27a6": "사용자 정의",
"a182c5125e": "Git 사용자 이름",
"sourceControlGroupOrderTitle": "소스 제어 그룹 순서",
"sourceControlGroupOrderDescription": "소스 제어에서 변경 사항, 스테이징된 변경 사항 또는 추적되지 않는 파일 중 무엇을 먼저 표시할지 선택합니다.",
"changesFirst": "변경 사항 먼저",
"stagedFirst": "스테이징된 항목 먼저",
"untrackedFirst": "추적되지 않는 파일 먼저",
"compareAgainstUpstreamTitle": "기본 비교 기준",
"compareAgainstUpstreamDescription": "소스 제어가 커밋된 변경 사항 비교에 기본으로 사용할 기준을 선택합니다. 브랜치 업스트림은 현재 브랜치를 자동으로 따라가며, 업스트림이 없으면 저장소 기본 브랜치로 돌아갑니다. 비교 기준은 해당 worktree의 Git 패널에서 worktree별로 변경할 수 있습니다. Pull Request 및 리베이스 대상은 변경되지 않습니다.",
"compareBaseRepositoryDefault": "저장소 기본값",
@@ -8221,14 +8216,7 @@
"f83c8937c4": "브랜치 이름 지정",
"5ecd91c5ef": "작업 트리를 생성할 때 브랜치 이름에 접두사가 추가됩니다.",
"68bd65fdb8": "브랜치 접두사",
"sourceControlGroupOrderTitle": "소스 제어 그룹 순서",
"sourceControlGroupOrderDescription": "소스 제어에서 변경 사항, 스테이징된 변경 사항 또는 추적되지 않는 파일 중 무엇을 먼저 표시할지 선택합니다.",
"groupOrder": "그룹 순서",
"changesFirst": "변경 사항 먼저",
"stagedFirst": "스테이징된 항목 먼저",
"untrackedFirst": "추적되지 않는 파일 먼저",
"sourceControl": "소스 제어",
"gitChanges": "git 변경 사항",
"compareAgainstUpstreamTitle": "기본 비교 기준",
"compareAgainstUpstreamDescription": "소스 제어가 커밋된 변경 사항 비교에 기본으로 사용할 기준을 선택합니다. 브랜치 업스트림은 현재 브랜치를 자동으로 따라가며, 업스트림이 없으면 저장소 기본 브랜치로 돌아갑니다. 비교 기준은 해당 worktree의 Git 패널에서 worktree별로 변경할 수 있습니다. Pull Request 및 리베이스 대상은 변경되지 않습니다.",
"compareBase": "비교 기준",
-12
View File
@@ -5963,11 +5963,6 @@
"3d172725cc": "没有任何",
"1f32ba27a6": "自定义",
"a182c5125e": "git用户名",
"sourceControlGroupOrderTitle": "源代码管理分组顺序",
"sourceControlGroupOrderDescription": "选择在源代码管理中优先显示“更改”、“已暂存更改”还是“未跟踪文件”。",
"changesFirst": "更改优先",
"stagedFirst": "已暂存优先",
"untrackedFirst": "未跟踪优先",
"compareAgainstUpstreamTitle": "默认对比基准",
"compareAgainstUpstreamDescription": "选择源代码管理在比较已提交变更时默认使用的基准。分支上游会自动跟随当前分支;如果没有上游,则回退到仓库默认分支。你仍然可以在对应 worktree 的 Git 面板中按 worktree 更改对比基准。Pull Request 和变基目标不会改变。",
"compareBaseRepositoryDefault": "仓库默认",
@@ -8233,14 +8228,7 @@
"f83c8937c4": "分支命名",
"5ecd91c5ef": "创建工作树时添加到分支名称的前缀。",
"68bd65fdb8": "分支前缀",
"sourceControlGroupOrderTitle": "源代码管理分组顺序",
"sourceControlGroupOrderDescription": "选择在源代码管理中优先显示“更改”、“已暂存更改”还是“未跟踪文件”。",
"groupOrder": "分组顺序",
"changesFirst": "更改优先",
"stagedFirst": "已暂存优先",
"untrackedFirst": "未跟踪优先",
"sourceControl": "源代码管理",
"gitChanges": "git 更改",
"compareAgainstUpstreamTitle": "默认对比基准",
"compareAgainstUpstreamDescription": "选择源代码管理在比较已提交变更时默认使用的基准。分支上游会自动跟随当前分支;如果没有上游,则回退到仓库默认分支。你仍然可以在对应 worktree 的 Git 面板中按 worktree 更改对比基准。Pull Request 和变基目标不会改变。",
"compareBase": "对比基准",
+1 -3
View File
@@ -23,9 +23,7 @@ describe('getDefaultSettings', () => {
expect(getDefaultSettings('/tmp').sourceControlViewMode).toBe('list')
})
it('keeps Source Control changes first by default', () => {
expect(getDefaultSettings('/tmp').sourceControlGroupOrder).toBe('changes-first')
})
it('uses a commit-oriented Source Control layout by default', () => {})
it('defaults mobile pairing to discovered network addresses', () => {
expect(getDefaultSettings('/tmp').mobilePairingCustomAddress).toBeNull()
-2
View File
@@ -29,7 +29,6 @@ import {
DEFAULT_LEFT_SIDEBAR_TINT_COLOR,
DEFAULT_LEFT_SIDEBAR_TINT_OPACITY
} from './left-sidebar-appearance'
import { DEFAULT_SOURCE_CONTROL_GROUP_ORDER } from './source-control-group-order'
import { DEFAULT_SETUP_AGENT_STARTUP_POLICY } from './setup-agent-startup-policy'
import { DESKTOP_TERMINAL_SCROLLBACK_ROWS_DEFAULT } from './terminal-scrollback-policy'
import { DEFAULT_USAGE_PERCENTAGE_DISPLAY } from './usage-percentage-display'
@@ -272,7 +271,6 @@ export function getDefaultSettings(homedir: string): GlobalSettings {
rightSidebarOpenByDefault: true,
showGitIgnoredFiles: true,
sourceControlViewMode: 'list',
sourceControlGroupOrder: DEFAULT_SOURCE_CONTROL_GROUP_ORDER,
sourceControlCompareAgainstUpstream: false,
showTitlebarAppName: true,
showTasksButton: true,
@@ -1,20 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
DEFAULT_SOURCE_CONTROL_GROUP_ORDER,
normalizeSourceControlGroupOrder
} from './source-control-group-order'
describe('normalizeSourceControlGroupOrder', () => {
it('keeps supported source control group orders', () => {
expect(normalizeSourceControlGroupOrder('changes-first')).toBe('changes-first')
expect(normalizeSourceControlGroupOrder('staged-first')).toBe('staged-first')
expect(normalizeSourceControlGroupOrder('untracked-first')).toBe('untracked-first')
})
it('falls back to the default for malformed values', () => {
expect(normalizeSourceControlGroupOrder('tracked-first')).toBe(
DEFAULT_SOURCE_CONTROL_GROUP_ORDER
)
expect(normalizeSourceControlGroupOrder(undefined)).toBe(DEFAULT_SOURCE_CONTROL_GROUP_ORDER)
})
})
-9
View File
@@ -1,9 +0,0 @@
import type { SourceControlGroupOrder } from './types'
export const DEFAULT_SOURCE_CONTROL_GROUP_ORDER: SourceControlGroupOrder = 'changes-first'
export function normalizeSourceControlGroupOrder(value: unknown): SourceControlGroupOrder {
return value === 'changes-first' || value === 'staged-first' || value === 'untracked-first'
? value
: DEFAULT_SOURCE_CONTROL_GROUP_ORDER
}
-3
View File
@@ -2699,7 +2699,6 @@ export type OpenInApplication = {
}
export type SourceControlViewMode = 'list' | 'tree'
export type SourceControlGroupOrder = 'changes-first' | 'staged-first' | 'untracked-first'
export type LeftSidebarAppearanceMode = 'default' | 'match-terminal' | 'tinted'
@@ -2885,8 +2884,6 @@ export type GlobalSettings = {
showGitIgnoredFiles?: boolean
/** Preferred Source Control changes layout. Per-user, not per-workspace. */
sourceControlViewMode: SourceControlViewMode
/** Preferred Source Control group order. Per-user, not per-workspace. */
sourceControlGroupOrder: SourceControlGroupOrder
/** Compare base defaults to the branch upstream instead of the repo default; affects only the compare/diff view, not the PR/rebase target. Per-user. */
sourceControlCompareAgainstUpstream: boolean
/** Whether to show the Orca app name in the titlebar. */
@@ -65,8 +65,9 @@ async function installUnreadableOrcaYamlFault(electronApp: ElectronApplication):
/** orca.yaml becomes readable again — every later check runs the production handler. */
async function healOrcaYamlRead(electronApp: ElectronApplication): Promise<void> {
await electronApp.evaluate(() => {
;(globalThis as typeof globalThis & { __orcaE2eOrcaYamlUnreadable?: boolean })
.__orcaE2eOrcaYamlUnreadable = false
;(
globalThis as typeof globalThis & { __orcaE2eOrcaYamlUnreadable?: boolean }
).__orcaE2eOrcaYamlUnreadable = false
})
}