diff --git a/docs/screenshots/multi-repository-branches.png b/docs/screenshots/multi-repository-branches.png new file mode 100644 index 0000000..2ace154 Binary files /dev/null and b/docs/screenshots/multi-repository-branches.png differ diff --git a/docs/screenshots/multi-repository-git-status.png b/docs/screenshots/multi-repository-git-status.png new file mode 100644 index 0000000..6c0c62c Binary files /dev/null and b/docs/screenshots/multi-repository-git-status.png differ diff --git a/docs/screenshots/multi-repository-selector.png b/docs/screenshots/multi-repository-selector.png new file mode 100644 index 0000000..95c95d9 Binary files /dev/null and b/docs/screenshots/multi-repository-selector.png differ diff --git a/package.json b/package.json index b7c0ec8..ebd4f66 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "LICENSE" ], "scripts": { - "build": "rm -rf lib && tsc -p tsconfig.build.json && tsdown", + "build": "node -e \"require('node:fs').rmSync('lib',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && tsdown", "prepublishOnly": "pnpm build", "typecheck": "tsc --noEmit", "bundle": "tsdown", diff --git a/src/client/DiffTab.tsx b/src/client/DiffTab.tsx index ec66d4e..c96caf2 100644 --- a/src/client/DiffTab.tsx +++ b/src/client/DiffTab.tsx @@ -34,7 +34,7 @@ export function DiffTab(props: { sessionId: string; cwd: string | undefined; dif useEffect(() => { let cancelled = false - const scope: SessionScope = { sessionId, cwd } + const scope: SessionScope = { sessionId, cwd, ...(diff.repoRoot !== undefined ? { repoRoot: diff.repoRoot } : {}) } setLoading(true) setError(null) setData(null) diff --git a/src/client/GitView.tsx b/src/client/GitView.tsx index 0237988..168d121 100644 --- a/src/client/GitView.tsx +++ b/src/client/GitView.tsx @@ -93,6 +93,7 @@ export function GitView(props: { const [status, setStatus] = useState(null) const [worktrees, setWorktrees] = useState([]) const [selectedWorktree, setSelectedWorktree] = useState() + const [repoRoot, setRepoRoot] = useState(undefined) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [branchNames, setBranchNames] = useState([]) @@ -122,6 +123,8 @@ export function GitView(props: { const selectedRef = useRef(undefined) useEffect(() => { selectedRef.current = selectedWorktree }, [selectedWorktree]) + const gitScope: SessionScope = repoRoot === undefined ? scope : { ...scope, repoRoot } + /** Publish a complete checkout-derived view. Status, branch choices and * history are one consistency unit: never mix rows from two worktrees. */ const refreshTarget = useCallback(async ( @@ -132,12 +135,13 @@ export function GitView(props: { setError(null) try { const [statusResult, branchResult, logResult] = await Promise.all([ - api.gitStatus(scope, target), - api.gitBranch(scope, target).catch(() => ({ current: '', names: [] as string[] })), - api.gitLog(scope, LOG_BATCH, 0, target).catch(() => [] as GitLogEntry[]), + api.gitStatus(gitScope, target), + api.gitBranch(gitScope, target).catch(() => ({ current: '', names: [] as string[] })), + api.gitLog(gitScope, LOG_BATCH, 0, target).catch(() => [] as GitLogEntry[]), ]) if (options.generation !== refreshGeneration.current) return setStatus(statusResult) + if (statusResult.root !== undefined && statusResult.root !== repoRoot) setRepoRoot(statusResult.root) setBranchNames(branchResult.names) setLogEntries(logResult) setLogEnded(logResult.length < LOG_BATCH) @@ -148,7 +152,7 @@ export function GitView(props: { } finally { if (options.loading && options.generation === refreshGeneration.current) setLoading(false) } - }, [scope.sessionId, scope.cwd]) + }, [scope.sessionId, scope.cwd, repoRoot]) const refresh = useCallback(async (silent = false): Promise => { if (refreshInFlight.current) return @@ -189,7 +193,7 @@ export function GitView(props: { // A poll may update status alone only while staying on the same checkout. // Any automatic selection change refreshes the complete derived view. if (silent && !targetChanged) { - const statusResult = await api.gitStatus(scope, target) + const statusResult = await api.gitStatus(gitScope, target) if (generation === refreshGeneration.current) setStatus(statusResult) return } @@ -240,7 +244,7 @@ export function GitView(props: { const target = selectedRef.current setLogLoadingMore(true) try { - const next = await api.gitLog(scope, LOG_BATCH, logEntries.length, target) + const next = await api.gitLog(gitScope, LOG_BATCH, logEntries.length, target) // A worktree switch clears the old history and increments generation. // Never append a late page from that checkout into the new one. if (generation !== refreshGeneration.current || target !== selectedRef.current) return @@ -261,7 +265,7 @@ export function GitView(props: { id: `diff:w:${encodeURIComponent(selectedWorktree ?? '')}:${staged ? 's' : 'u'}:${entry.path}`, type: 'diff', title: baseName(entry.path), - diff: { kind: 'worktree', path: entry.path, staged, untracked: isUntracked(entry), worktree: selectedWorktree }, + diff: { kind: 'worktree', path: entry.path, staged, untracked: isUntracked(entry), worktree: selectedWorktree, repoRoot }, }) } @@ -271,15 +275,15 @@ export function GitView(props: { id: `diff:c:${encodeURIComponent(selectedWorktree ?? '')}:${entry.hashFull}`, type: 'diff', title: `${entry.hash} ${entry.subject}`, - diff: { kind: 'commit', hash: entry.hash, hashFull: entry.hashFull, subject: entry.subject, worktree: selectedWorktree }, + diff: { kind: 'commit', hash: entry.hash, hashFull: entry.hashFull, subject: entry.subject, worktree: selectedWorktree, repoRoot }, }) } const stageEntry = async (entry: GitStatusEntry, staged: boolean): Promise => { setBusy(true) try { - if (staged) await api.gitUnstage(scope, entry.path, selectedWorktree) - else await api.gitStage(scope, entry.path, selectedWorktree) + if (staged) await api.gitUnstage(gitScope, entry.path, selectedWorktree) + else await api.gitStage(gitScope, entry.path, selectedWorktree) await refresh() } finally { setBusy(false) @@ -289,8 +293,8 @@ export function GitView(props: { const stageAll = async (staged: boolean): Promise => { setBusy(true) try { - if (staged) await api.gitUnstage(scope, undefined, selectedWorktree) - else await api.gitStage(scope, undefined, selectedWorktree) + if (staged) await api.gitUnstage(gitScope, undefined, selectedWorktree) + else await api.gitStage(gitScope, undefined, selectedWorktree) await refresh() } finally { setBusy(false) @@ -303,7 +307,7 @@ export function GitView(props: { setBusy(true) setCommitError(null) try { - await api.gitCommit(scope, message, selectedWorktree) + await api.gitCommit(gitScope, message, selectedWorktree) setCommitMsg('') await refresh() } catch (reason) { @@ -318,7 +322,7 @@ export function GitView(props: { setBusy(true) setCommitError(null) try { - await api.gitCheckout(scope, branch, selectedWorktree) + await api.gitCheckout(gitScope, branch, selectedWorktree) await refresh() } catch (reason) { setCommitError(`${t('checkoutError')}: ${reason instanceof Error ? reason.message : String(reason)}`) @@ -411,6 +415,17 @@ export function GitView(props: { )}
+ {(status?.repositories?.length ?? 0) > 1 && ( + + )}