@brimveyn/aimux 1.22.2 → 1.22.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/app-runtime/side-effects.ts +35 -8
- package/src/app-runtime/workspace-actions.ts +32 -2
- package/src/cli/commands/workspace/create-core.ts +6 -2
- package/src/git/worktree.ts +52 -4
- package/src/input/modes/types.ts +1 -0
- package/src/settings/flags.ts +15 -0
- package/src/settings/sections/git.ts +12 -0
- package/src/settings/sections/index.ts +2 -0
- package/src/settings/sections/workspace.ts +44 -0
- package/src/state/types.ts +8 -0
- package/src/state/validation.ts +2 -1
- package/src/ui/components/git/pane/git-pane-header.tsx +18 -18
- package/src/ui/components/git/pane/git-pane-widget.tsx +2 -2
- package/src/ui/components/git/pane/pr-checks-panel.tsx +17 -3
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@ import { loadConfig, saveConfig } from '../config'
|
|
|
8
8
|
import { logInputDebug } from '../debug/input-log'
|
|
9
9
|
import { enqueueGitOp } from '../git/command-queue'
|
|
10
10
|
import { countDirtyFiles } from '../git/move-workspace'
|
|
11
|
-
import { getDefaultBranch, listLocalBranches } from '../git/worktree'
|
|
11
|
+
import { getCurrentBranch, getDefaultBranch, listLocalBranches } from '../git/worktree'
|
|
12
12
|
import { assistantAcceptsPromptArg } from '../pty/command-registry'
|
|
13
13
|
import { allLeafIds, getGroupIdForTab } from '../state/layout-tree'
|
|
14
14
|
import { saveCurrentProject } from '../state/project-save'
|
|
@@ -72,6 +72,7 @@ import {
|
|
|
72
72
|
isForceableWorkspaceDeleteError,
|
|
73
73
|
runDeleteWorkspace,
|
|
74
74
|
runMoveWorkspace,
|
|
75
|
+
setProjectDefaultBaseRef,
|
|
75
76
|
} from './workspace-actions'
|
|
76
77
|
import { placeholderWorkspaceName, renameWorkspaceFromPrompt } from './workspace-naming'
|
|
77
78
|
|
|
@@ -207,6 +208,12 @@ function applyThemeEffect(
|
|
|
207
208
|
}
|
|
208
209
|
}
|
|
209
210
|
|
|
211
|
+
/** The given branch first, the rest in the order they came. */
|
|
212
|
+
function hoistBranch(branches: string[], first: string | undefined): string[] {
|
|
213
|
+
if (first == null || !branches.includes(first)) return branches
|
|
214
|
+
return [first, ...branches.filter((branch) => branch !== first)]
|
|
215
|
+
}
|
|
216
|
+
|
|
210
217
|
/**
|
|
211
218
|
* Setup runs concurrently with the agent by design, so say so rather than
|
|
212
219
|
* letting the agent run tests against a half-installed tree and draw the wrong
|
|
@@ -359,12 +366,21 @@ export function executeSideEffect(effect: SideEffect, ctx: SideEffectContext): v
|
|
|
359
366
|
const project = state.projects.find((entry) => entry.id === state.currentProjectId)
|
|
360
367
|
const sourcePath = getActiveWorkspace(project)?.path ?? getActiveWorkspacePath(project)
|
|
361
368
|
if (!(sourcePath != null && sourcePath !== '')) return
|
|
362
|
-
const [branches, defaultBranch] = await Promise.all([
|
|
369
|
+
const [branches, defaultBranch, currentBranch] = await Promise.all([
|
|
363
370
|
listLocalBranches(sourcePath),
|
|
364
371
|
getDefaultBranch(sourcePath),
|
|
372
|
+
getCurrentBranch(sourcePath),
|
|
365
373
|
])
|
|
366
374
|
if (ctx.getState().modal.type !== 'create-workspace') return
|
|
367
|
-
ctx.dispatch({
|
|
375
|
+
ctx.dispatch({
|
|
376
|
+
// Stacking on the branch you are already on is the other common base,
|
|
377
|
+
// and committer date buries it once anyone else pushes.
|
|
378
|
+
branches: hoistBranch(branches, currentBranch),
|
|
379
|
+
// The project's convention wins over what the repo declares: a gitflow
|
|
380
|
+
// repo still says `main` while everyone branches off `develop`.
|
|
381
|
+
defaultBranch: project?.defaultBaseRef ?? defaultBranch,
|
|
382
|
+
type: 'set-create-workspace-base-branches',
|
|
383
|
+
})
|
|
368
384
|
})()
|
|
369
385
|
return
|
|
370
386
|
}
|
|
@@ -678,6 +694,10 @@ export function executeSideEffect(effect: SideEffect, ctx: SideEffectContext): v
|
|
|
678
694
|
handleConfigureSetupScriptEffect(ctx, effect.projectId)
|
|
679
695
|
return
|
|
680
696
|
}
|
|
697
|
+
case 'set-project-default-base-ref': {
|
|
698
|
+
setProjectDefaultBaseRef(ctx, effect.projectId, effect.baseRef)
|
|
699
|
+
return
|
|
700
|
+
}
|
|
681
701
|
case 'ask-agent-for-setup-script': {
|
|
682
702
|
handleAskAgentForSetupScriptEffect(ctx)
|
|
683
703
|
return
|
|
@@ -729,11 +749,18 @@ function runUpdateFromTui(ctx: SideEffectContext, latestVersion: string): void {
|
|
|
729
749
|
void ctx.backend.destroy(true)
|
|
730
750
|
ctx.renderer.destroy()
|
|
731
751
|
process.stdout.write(`\nUpdating aimux to ${latestVersion}...\n`)
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
752
|
+
// `bun update -g` is a no-op when the global entry pins an exact version
|
|
753
|
+
// (`"@brimveyn/aimux": "1.22.2"`), which is what `install -g pkg@version`
|
|
754
|
+
// writes — the update "succeeded", the version never moved, and the modal
|
|
755
|
+
// came back every launch. Install the resolved version explicitly instead.
|
|
756
|
+
const proc = Bun.spawn(
|
|
757
|
+
['bun', 'install', '-g', `@brimveyn/aimux@${latestVersion}`, '@brimveyn/aimux-config@latest'],
|
|
758
|
+
{
|
|
759
|
+
stderr: 'inherit',
|
|
760
|
+
stdin: 'inherit',
|
|
761
|
+
stdout: 'inherit',
|
|
762
|
+
}
|
|
763
|
+
)
|
|
737
764
|
void (async () => {
|
|
738
765
|
const code = await proc.exited
|
|
739
766
|
if (code === 0) {
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
makeWorktreePath,
|
|
24
24
|
sanitizePathSegment,
|
|
25
25
|
} from '../platform/worktree-paths'
|
|
26
|
+
import { shouldRefreshBase } from '../settings/flags'
|
|
26
27
|
import { saveProjectCatalog } from '../state/project-catalog'
|
|
27
28
|
import { pruneSnapshotOfWorkspace } from '../state/project-persistence'
|
|
28
29
|
import { getActiveWorkspace, getActiveWorkspacePath } from '../state/project-workspaces'
|
|
@@ -64,6 +65,27 @@ export function handleSwitchWorkspace(
|
|
|
64
65
|
ctx.dispatch({ projects, type: 'set-projects' })
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Set (or clear, when blank) the branch this project's new workspaces fork from.
|
|
70
|
+
* Here rather than with the project actions: the value is workspace policy that
|
|
71
|
+
* happens to be stored on the project, and this is where the record-update
|
|
72
|
+
* helper and the rest of that policy already live.
|
|
73
|
+
*/
|
|
74
|
+
export function setProjectDefaultBaseRef(
|
|
75
|
+
ctx: SideEffectContext,
|
|
76
|
+
projectId: string,
|
|
77
|
+
baseRef: string
|
|
78
|
+
): void {
|
|
79
|
+
const trimmed = baseRef.trim()
|
|
80
|
+
const projects = replaceProject(ctx, projectId, (entry) => ({
|
|
81
|
+
...entry,
|
|
82
|
+
defaultBaseRef: trimmed === '' ? undefined : trimmed,
|
|
83
|
+
updatedAt: new Date().toISOString(),
|
|
84
|
+
}))
|
|
85
|
+
saveProjectCatalog(projects)
|
|
86
|
+
ctx.dispatch({ projects, type: 'set-projects' })
|
|
87
|
+
}
|
|
88
|
+
|
|
67
89
|
function normalizeBranchName(branch: string | undefined): string | undefined {
|
|
68
90
|
return branch?.replace(/^refs\/heads\//, '').trim()
|
|
69
91
|
}
|
|
@@ -116,11 +138,19 @@ export async function createAimuxTempWorkspace(
|
|
|
116
138
|
|
|
117
139
|
await mkdir(dirname(targetPath), { recursive: true })
|
|
118
140
|
await assertSafeAimuxWorktreePath(targetPath)
|
|
119
|
-
|
|
141
|
+
// What it forked from, not what was asked for: the two differ whenever the
|
|
142
|
+
// base was refreshed from origin, and the record is what "based on" reads.
|
|
143
|
+
const forkRef = await createGitWorktree({
|
|
144
|
+
baseRef,
|
|
145
|
+
branchName,
|
|
146
|
+
refreshBase: shouldRefreshBase(),
|
|
147
|
+
repoPath: repoRoot,
|
|
148
|
+
targetPath,
|
|
149
|
+
})
|
|
120
150
|
const now = new Date().toISOString()
|
|
121
151
|
|
|
122
152
|
const workspace: WorkspaceRecord = {
|
|
123
|
-
baseRef,
|
|
153
|
+
baseRef: forkRef,
|
|
124
154
|
branch: branchName,
|
|
125
155
|
commitSha: await getHeadSha(targetPath),
|
|
126
156
|
createdAt: now,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
makeWorktreePath,
|
|
11
11
|
pruneEmptyWorktreeParent,
|
|
12
12
|
} from '../../../platform/worktree-paths'
|
|
13
|
+
import { shouldRefreshBase } from '../../../settings/flags'
|
|
13
14
|
|
|
14
15
|
export interface CreateWorkspaceParams {
|
|
15
16
|
/** Base ref for the branch (callers default to 'HEAD'). */
|
|
@@ -68,10 +69,12 @@ export async function createProjectWorkspace(
|
|
|
68
69
|
await ensureAimuxWorktreeRoot()
|
|
69
70
|
await assertSafeAimuxWorktreePath(targetPath)
|
|
70
71
|
|
|
72
|
+
let forkRef: string
|
|
71
73
|
try {
|
|
72
|
-
await createGitWorktree({
|
|
74
|
+
forkRef = await createGitWorktree({
|
|
73
75
|
baseRef: base,
|
|
74
76
|
branchName: branch,
|
|
77
|
+
refreshBase: shouldRefreshBase(),
|
|
75
78
|
repoPath: primary.repoRoot,
|
|
76
79
|
targetPath,
|
|
77
80
|
})
|
|
@@ -84,7 +87,8 @@ export async function createProjectWorkspace(
|
|
|
84
87
|
|
|
85
88
|
const now = new Date().toISOString()
|
|
86
89
|
const record: WorkspaceRecord = {
|
|
87
|
-
|
|
90
|
+
// What it forked from: `origin/<base>` when the base was refreshed.
|
|
91
|
+
baseRef: forkRef,
|
|
88
92
|
branch,
|
|
89
93
|
createdAt: now,
|
|
90
94
|
createdByAimux: true,
|
package/src/git/worktree.ts
CHANGED
|
@@ -98,9 +98,48 @@ export async function renameGitBranch(
|
|
|
98
98
|
return result.exitCode === 0
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* The ref a new workspace should actually fork from.
|
|
103
|
+
*
|
|
104
|
+
* "Base it on main" means the main the team pushed, not whatever the local main
|
|
105
|
+
* happened to be at the last pull — a workspace started ten commits behind says
|
|
106
|
+
* nothing at the time and shows up as conflicts at merge. So fetch the base's
|
|
107
|
+
* counterpart on origin and fork from that when there is one.
|
|
108
|
+
*
|
|
109
|
+
* Every step is best-effort on purpose. The fetch is `nothrow`, so a machine
|
|
110
|
+
* with no network still gets a workspace off the local ref; and a base with no
|
|
111
|
+
* counterpart on origin — a local-only branch, a SHA, `HEAD`, or an
|
|
112
|
+
* already-qualified `origin/x` — resolves to nothing and is used as given, which
|
|
113
|
+
* is why no ref-shape guessing is needed here.
|
|
114
|
+
*
|
|
115
|
+
* The refspec is spelled out rather than left to `remote.origin.fetch`: a bare
|
|
116
|
+
* `fetch origin main` on a remote with no configured refspec updates only
|
|
117
|
+
* FETCH_HEAD, leaving a stale `origin/main` that resolves perfectly well — so
|
|
118
|
+
* the fork would silently land on exactly the old commit this exists to avoid.
|
|
119
|
+
*
|
|
120
|
+
* ponytail: unpushed commits on the local base are unreachable from the picker
|
|
121
|
+
* once this is on — the remote ref wins whenever it exists, and the toggle is
|
|
122
|
+
* the only way back. Listing `main` and `origin/main` as separate base options
|
|
123
|
+
* is the upgrade path.
|
|
124
|
+
*/
|
|
125
|
+
export async function resolveWorktreeBaseRef(repoPath: string, baseRef: string): Promise<string> {
|
|
126
|
+
const remoteRef = `origin/${baseRef}`
|
|
127
|
+
await $`git -C ${repoPath} fetch origin +${baseRef}:refs/remotes/${remoteRef}`.quiet().nothrow()
|
|
128
|
+
return (await resolveGitRef(repoPath, remoteRef)) !== undefined ? remoteRef : baseRef
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Returns the ref it forked from, which is not always the one it was handed.
|
|
133
|
+
*
|
|
134
|
+
* `refreshBase` is required rather than read from the user's config here: this
|
|
135
|
+
* module wraps git and nothing else, and a caller that has to name the policy
|
|
136
|
+
* cannot forget it exists — which a settings lookup hidden in here would let
|
|
137
|
+
* them do. `shouldRefreshBase` is what both callers name it with.
|
|
138
|
+
*/
|
|
101
139
|
export async function createGitWorktree({
|
|
102
140
|
baseRef,
|
|
103
141
|
branchName,
|
|
142
|
+
refreshBase,
|
|
104
143
|
repoPath,
|
|
105
144
|
targetPath,
|
|
106
145
|
}: {
|
|
@@ -108,10 +147,18 @@ export async function createGitWorktree({
|
|
|
108
147
|
targetPath: string
|
|
109
148
|
branchName: string
|
|
110
149
|
baseRef: string
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
150
|
+
refreshBase: boolean
|
|
151
|
+
}): Promise<string> {
|
|
152
|
+
const forkRef = refreshBase ? await resolveWorktreeBaseRef(repoPath, baseRef) : baseRef
|
|
153
|
+
// `--no-track`: forking from `origin/main` would otherwise set that as the new
|
|
154
|
+
// branch's upstream, and a workspace branch tracking main is not a workspace
|
|
155
|
+
// branch — `git push` either refuses it (push.default=simple) or, worse,
|
|
156
|
+
// pushes the branch onto main. Forking from a local branch never set tracking,
|
|
157
|
+
// so this only pins down what refreshing the base would have changed.
|
|
158
|
+
const result =
|
|
159
|
+
await $`git -C ${repoPath} worktree add --no-track -b ${branchName} ${targetPath} ${forkRef}`
|
|
160
|
+
.quiet()
|
|
161
|
+
.nothrow()
|
|
115
162
|
if (result.exitCode !== 0) {
|
|
116
163
|
// Always name the repository. A bare "fatal: not a valid object name: 'X'"
|
|
117
164
|
// reads as a ref-resolution bug when the real cause is that git ran in a
|
|
@@ -119,6 +166,7 @@ export async function createGitWorktree({
|
|
|
119
166
|
const stderr = result.stderr.toString().trim()
|
|
120
167
|
throw new Error(`${stderr || 'failed to create git worktree'} (in ${repoPath})`)
|
|
121
168
|
}
|
|
169
|
+
return forkRef
|
|
122
170
|
}
|
|
123
171
|
|
|
124
172
|
// Force-delete a local branch. Returns false (without throwing) when git
|
package/src/input/modes/types.ts
CHANGED
|
@@ -114,6 +114,7 @@ export type SideEffect =
|
|
|
114
114
|
| { type: 'run-setup' }
|
|
115
115
|
| { type: 'stop-setup' }
|
|
116
116
|
| { type: 'configure-setup-script'; projectId?: string }
|
|
117
|
+
| { type: 'set-project-default-base-ref'; projectId: string; baseRef: string }
|
|
117
118
|
| { type: 'ask-agent-for-setup-script' }
|
|
118
119
|
| { type: 'promote-setup-tab' }
|
|
119
120
|
/** Toggle a checkbox, cycle an enum, run a row's action — whatever the row is. */
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { loadConfig } from '../config'
|
|
2
|
+
import { FETCH_BASE } from './sections/git'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Settings read outside the screen, off the file rather than the store.
|
|
6
|
+
*
|
|
7
|
+
* `live.ts` is the same read side for the running TUI, through hooks over the
|
|
8
|
+
* hydrated store. These have to answer where there is no store: the CLI never
|
|
9
|
+
* hydrates one, and a setting only the TUI honours is a setting that lies.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Whether a new workspace forks from the base as pushed, or as last pulled. */
|
|
13
|
+
export function shouldRefreshBase(): boolean {
|
|
14
|
+
return loadConfig().settings?.[FETCH_BASE] !== false
|
|
15
|
+
}
|
|
@@ -5,6 +5,8 @@ import type { SettingSection, SettingValue } from '../types'
|
|
|
5
5
|
|
|
6
6
|
import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
|
|
7
7
|
|
|
8
|
+
export const FETCH_BASE = 'git.fetchBase'
|
|
9
|
+
|
|
8
10
|
function isFileListMode(value: SettingValue): value is GitFileListMode {
|
|
9
11
|
return value === 'tree' || value === 'flat'
|
|
10
12
|
}
|
|
@@ -81,6 +83,16 @@ export const GIT_SECTION: SettingSection = {
|
|
|
81
83
|
step: 1,
|
|
82
84
|
storage: 'settings',
|
|
83
85
|
},
|
|
86
|
+
{
|
|
87
|
+
// No `apply`: `shouldRefreshBase` reads this off the file, so the CLI —
|
|
88
|
+
// where this screen never hydrates — honours it too.
|
|
89
|
+
description: 'Fetch the base branch before forking from it. Off forks from your local copy.',
|
|
90
|
+
fallback: true,
|
|
91
|
+
id: FETCH_BASE,
|
|
92
|
+
kind: 'toggle',
|
|
93
|
+
label: 'Refresh the base branch',
|
|
94
|
+
storage: 'settings',
|
|
95
|
+
},
|
|
84
96
|
{
|
|
85
97
|
apply: (value) => setMultiRepoConfig({ ...getMultiRepoConfig(), enabled: value === true }),
|
|
86
98
|
description: 'Aggregate the status of git repos nested under the project.',
|
|
@@ -13,6 +13,7 @@ import { LAYOUT_SECTION } from './layout'
|
|
|
13
13
|
import { NOTIFICATIONS_SECTION } from './notifications'
|
|
14
14
|
import { SETUP_SECTION } from './setup'
|
|
15
15
|
import { STATUS_BAR_SECTION } from './status-bar'
|
|
16
|
+
import { WORKSPACE_SECTION } from './workspace'
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Every section, in the order the screen lists them. Adding a setting is one
|
|
@@ -24,6 +25,7 @@ export const SETTING_SECTIONS: readonly SettingSection[] = [
|
|
|
24
25
|
AUTOMATION_SECTION,
|
|
25
26
|
NOTIFICATIONS_SECTION,
|
|
26
27
|
SETUP_SECTION,
|
|
28
|
+
WORKSPACE_SECTION,
|
|
27
29
|
GIT_SECTION,
|
|
28
30
|
COMMANDS_SECTION,
|
|
29
31
|
EDITOR_SECTION,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ProjectRecord } from '../../state/types'
|
|
2
|
+
import type { SettingRow, SettingSection } from '../types'
|
|
3
|
+
|
|
4
|
+
import { runSideEffectGlobal } from '../../state/dispatch-ref'
|
|
5
|
+
|
|
6
|
+
const BASE_REF_ROW_PREFIX = 'workspace.defaultBaseRef.'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* One row per project, over `defaultBaseRef` on its catalog record.
|
|
10
|
+
*
|
|
11
|
+
* Per project rather than global because the convention belongs to the repo:
|
|
12
|
+
* one clone branches off `develop`, the next off `main`, and a single value
|
|
13
|
+
* would be wrong for every repo but one. Left empty it follows whatever the
|
|
14
|
+
* repo declares as its default branch, which is the right answer most of the
|
|
15
|
+
* time and the reason this is a row and not a prompt.
|
|
16
|
+
*/
|
|
17
|
+
function defaultBaseRefRow(project: ProjectRecord): SettingRow {
|
|
18
|
+
return {
|
|
19
|
+
description: "Branch new workspaces fork from. Empty follows the repo's own default.",
|
|
20
|
+
id: `${BASE_REF_ROW_PREFIX}${project.id}`,
|
|
21
|
+
kind: 'text',
|
|
22
|
+
label: project.name,
|
|
23
|
+
placeholder: "the repo's default",
|
|
24
|
+
// Read through the live state, not the record this row was built from: the
|
|
25
|
+
// screen rebuilds its rows from the same list it just wrote to.
|
|
26
|
+
read: (ctx) =>
|
|
27
|
+
ctx.state.projects.find((entry) => entry.id === project.id)?.defaultBaseRef ?? '',
|
|
28
|
+
storage: 'app',
|
|
29
|
+
write: (value) =>
|
|
30
|
+
runSideEffectGlobal({
|
|
31
|
+
baseRef: String(value),
|
|
32
|
+
projectId: project.id,
|
|
33
|
+
type: 'set-project-default-base-ref',
|
|
34
|
+
}),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const WORKSPACE_SECTION: SettingSection = {
|
|
39
|
+
id: 'workspace',
|
|
40
|
+
label: 'Workspaces',
|
|
41
|
+
rowCount: (projects) => projects.length,
|
|
42
|
+
rows: (projects: readonly ProjectRecord[]) =>
|
|
43
|
+
projects.map((project) => defaultBaseRefRow(project)),
|
|
44
|
+
}
|
package/src/state/types.ts
CHANGED
|
@@ -217,6 +217,14 @@ export interface ProjectRecord {
|
|
|
217
217
|
projectSnapshot?: ProjectSnapshotV1
|
|
218
218
|
workspaces?: WorkspaceRecord[]
|
|
219
219
|
activeWorkspaceId?: string
|
|
220
|
+
/**
|
|
221
|
+
* The branch new workspaces fork from, when the repo's own default is the
|
|
222
|
+
* wrong answer — a gitflow repo branches off `develop` while still declaring
|
|
223
|
+
* `main` as its default, and nothing about the repo says so. Per project
|
|
224
|
+
* because the convention is the team's, not the user's: three repos, three
|
|
225
|
+
* answers. Unset means the repo's default branch.
|
|
226
|
+
*/
|
|
227
|
+
defaultBaseRef?: string
|
|
220
228
|
}
|
|
221
229
|
|
|
222
230
|
export interface ProjectBarState {
|
package/src/state/validation.ts
CHANGED
|
@@ -180,7 +180,8 @@ export function isProjectRecord(value: unknown): value is ProjectRecord {
|
|
|
180
180
|
(value.projectSnapshot === undefined || isProjectSnapshotV1(value.projectSnapshot)) &&
|
|
181
181
|
(value.workspaces === undefined ||
|
|
182
182
|
(Array.isArray(value.workspaces) && value.workspaces.every(isWorkspaceRecord))) &&
|
|
183
|
-
(value.activeWorkspaceId === undefined || isString(value.activeWorkspaceId))
|
|
183
|
+
(value.activeWorkspaceId === undefined || isString(value.activeWorkspaceId)) &&
|
|
184
|
+
(value.defaultBaseRef === undefined || isString(value.defaultBaseRef))
|
|
184
185
|
)
|
|
185
186
|
}
|
|
186
187
|
|
|
@@ -8,7 +8,7 @@ import { selectPrRowVisible, usePrStatusStore } from '../../../../state/pr-statu
|
|
|
8
8
|
import { useTheme, useTransparent } from '../../../theme'
|
|
9
9
|
import { PrStateRow } from './pr-state-row'
|
|
10
10
|
|
|
11
|
-
export type GitPaneTab = '
|
|
11
|
+
export type GitPaneTab = 'diff' | 'github'
|
|
12
12
|
|
|
13
13
|
interface GitPaneHeaderProps {
|
|
14
14
|
gitPanel: GitPanelState
|
|
@@ -40,14 +40,14 @@ export const GitPaneHeader = memo(function GitPaneHeader({
|
|
|
40
40
|
dispatchGlobal({ type: 'git-mode-toggle-file-list-mode' })
|
|
41
41
|
runSideEffectGlobal({ mode: nextFileListMode, type: 'persist-git-file-list-mode' })
|
|
42
42
|
}, [nextFileListMode])
|
|
43
|
-
const
|
|
44
|
-
const
|
|
43
|
+
const showDiff = useCallback(() => onTabChange?.('diff'), [onTabChange])
|
|
44
|
+
const showGithub = useCallback(() => onTabChange?.('github'), [onTabChange])
|
|
45
45
|
|
|
46
46
|
const hasTabs = tab !== undefined && onTabChange !== undefined
|
|
47
47
|
const hasProject = projectPath != null && projectPath !== ''
|
|
48
48
|
if (!hasProject) return null
|
|
49
49
|
// A git error must not hide the tab row, otherwise there's no way back to
|
|
50
|
-
// `
|
|
50
|
+
// `diff` (and the PR checks are still worth showing outside a repo).
|
|
51
51
|
if (gitPanel.error !== null && !hasTabs) return null
|
|
52
52
|
|
|
53
53
|
const branch = gitPanel.branch
|
|
@@ -60,17 +60,17 @@ export const GitPaneHeader = memo(function GitPaneHeader({
|
|
|
60
60
|
const showBehind = behind > 0
|
|
61
61
|
const showTracking = showAhead || showBehind
|
|
62
62
|
|
|
63
|
-
// The PR row already carries the branch identity
|
|
64
|
-
//
|
|
63
|
+
// The PR row already carries the branch identity, so the branch row is only
|
|
64
|
+
// worth a line without a PR.
|
|
65
65
|
const showPrRow = hasTabs && prRowVisible
|
|
66
66
|
const showBranchRow = !showPrRow && gitPanel.error === null
|
|
67
|
-
const showToggle = tab !== '
|
|
67
|
+
const showToggle = tab !== 'github' && gitPanel.files.length > 0
|
|
68
68
|
const showHistorical = headOffset > 0
|
|
69
69
|
const showReviewBase = baseLabel != null && baseLabel !== ''
|
|
70
70
|
const showScope = showHistorical || showReviewBase
|
|
71
71
|
const trackingAndToggle = (
|
|
72
72
|
<box flexDirection="row" flexShrink={0} gap={2}>
|
|
73
|
-
{showTracking && tab !== '
|
|
73
|
+
{showTracking && tab !== 'github' ? (
|
|
74
74
|
<box flexDirection="row" gap={1}>
|
|
75
75
|
{showAhead ? (
|
|
76
76
|
<text selectable={false} fg={t.textMuted} wrapMode="none">
|
|
@@ -117,31 +117,31 @@ export const GitPaneHeader = memo(function GitPaneHeader({
|
|
|
117
117
|
<box
|
|
118
118
|
paddingLeft={1}
|
|
119
119
|
paddingRight={1}
|
|
120
|
-
backgroundColor={tab === '
|
|
121
|
-
onMouseDown={
|
|
120
|
+
backgroundColor={tab === 'diff' ? activeTabBg : undefined}
|
|
121
|
+
onMouseDown={showDiff}
|
|
122
122
|
>
|
|
123
123
|
<text
|
|
124
124
|
selectable={false}
|
|
125
|
-
fg={tab === '
|
|
126
|
-
bg={tab === '
|
|
125
|
+
fg={tab === 'diff' ? t.text : t.textMuted}
|
|
126
|
+
bg={tab === 'diff' ? activeTabBg : undefined}
|
|
127
127
|
wrapMode="none"
|
|
128
128
|
>
|
|
129
|
-
|
|
129
|
+
diff
|
|
130
130
|
</text>
|
|
131
131
|
</box>
|
|
132
132
|
<box
|
|
133
133
|
paddingLeft={1}
|
|
134
134
|
paddingRight={1}
|
|
135
|
-
backgroundColor={tab === '
|
|
136
|
-
onMouseDown={
|
|
135
|
+
backgroundColor={tab === 'github' ? activeTabBg : undefined}
|
|
136
|
+
onMouseDown={showGithub}
|
|
137
137
|
>
|
|
138
138
|
<text
|
|
139
139
|
selectable={false}
|
|
140
|
-
fg={tab === '
|
|
141
|
-
bg={tab === '
|
|
140
|
+
fg={tab === 'github' ? t.text : t.textMuted}
|
|
141
|
+
bg={tab === 'github' ? activeTabBg : undefined}
|
|
142
142
|
wrapMode="none"
|
|
143
143
|
>
|
|
144
|
-
|
|
144
|
+
github
|
|
145
145
|
</text>
|
|
146
146
|
</box>
|
|
147
147
|
</box>
|
|
@@ -34,7 +34,7 @@ export const GitPaneWidget = memo(function GitPaneWidget({
|
|
|
34
34
|
: undefined
|
|
35
35
|
const projectPath = getActiveWorkspacePath(currentProject)
|
|
36
36
|
|
|
37
|
-
const [tab, setTab] = useState<GitPaneTab>('
|
|
37
|
+
const [tab, setTab] = useState<GitPaneTab>('diff')
|
|
38
38
|
|
|
39
39
|
useRepoDiscovery(projectPath)
|
|
40
40
|
useGitPanelPolling({ enabled: pollingEnabled, headOffset: 0, projectPath })
|
|
@@ -56,7 +56,7 @@ export const GitPaneWidget = memo(function GitPaneWidget({
|
|
|
56
56
|
return (
|
|
57
57
|
<box flexDirection="column" flexGrow={1} flexShrink={1} flexBasis={0} overflow="hidden">
|
|
58
58
|
<GitPaneHeader gitPanel={display} onTabChange={setTab} projectPath={projectPath} tab={tab} />
|
|
59
|
-
{tab === '
|
|
59
|
+
{tab === 'github' ? (
|
|
60
60
|
<PrChecksPanel contentWidth={contentWidth} />
|
|
61
61
|
) : (
|
|
62
62
|
<GitPanel
|
|
@@ -156,9 +156,23 @@ export const PrChecksPanel = memo(function PrChecksPanel({
|
|
|
156
156
|
scrollbarOptions={HIDDEN_SCROLLBAR_OPTIONS}
|
|
157
157
|
contentOptions={AIRY_CONTENT_OPTIONS}
|
|
158
158
|
>
|
|
159
|
-
<
|
|
160
|
-
<
|
|
161
|
-
|
|
159
|
+
<box flexDirection="column">
|
|
160
|
+
<text selectable={false} fg={stale ? t.textMuted : t.text} bg={bg}>
|
|
161
|
+
<strong>{pr.title}</strong>
|
|
162
|
+
</text>
|
|
163
|
+
{/* The PR diffstat (base..head), not the working tree the files tab shows. */}
|
|
164
|
+
<box flexDirection="row" gap={1}>
|
|
165
|
+
<text selectable={false} fg={t.success} bg={bg} wrapMode="none">
|
|
166
|
+
+{pr.additions}
|
|
167
|
+
</text>
|
|
168
|
+
<text selectable={false} fg={t.error} bg={bg} wrapMode="none">
|
|
169
|
+
-{pr.deletions}
|
|
170
|
+
</text>
|
|
171
|
+
<text selectable={false} fg={t.textMuted} bg={bg} wrapMode="none">
|
|
172
|
+
{pr.changedFiles === 1 ? '1 file' : `${pr.changedFiles} files`}
|
|
173
|
+
</text>
|
|
174
|
+
</box>
|
|
175
|
+
</box>
|
|
162
176
|
{body !== '' ? (
|
|
163
177
|
<box flexDirection="column">
|
|
164
178
|
<text selectable={false} fg={t.textMuted} bg={bg}>
|