From 3cfaf689e79f248853bb4c65fdff05bd106b8870 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Sat, 7 Feb 2026 15:23:27 +0000 Subject: [PATCH] fix(worktrees): disable selector outside git repos Expose isGitRepo on worktree listing and show Worktree: Unavailable while disabling the dropdown when a workspace folder is not a Git repository. --- packages/server/src/api-types.ts | 2 ++ .../server/src/server/routes/worktrees.ts | 4 +-- .../ui/src/components/worktree-selector.tsx | 16 +++++++++- packages/ui/src/stores/worktrees.ts | 29 ++++++++++++++++++- 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/packages/server/src/api-types.ts b/packages/server/src/api-types.ts index 33765f0c..477207e2 100644 --- a/packages/server/src/api-types.ts +++ b/packages/server/src/api-types.ts @@ -64,6 +64,8 @@ export interface WorktreeDescriptor { export interface WorktreeListResponse { worktrees: WorktreeDescriptor[] + /** True when the workspace folder resolves to a Git repository. */ + isGitRepo?: boolean } export interface WorktreeCreateRequest { diff --git a/packages/server/src/server/routes/worktrees.ts b/packages/server/src/server/routes/worktrees.ts index d60308db..9e76319b 100644 --- a/packages/server/src/server/routes/worktrees.ts +++ b/packages/server/src/server/routes/worktrees.ts @@ -34,9 +34,9 @@ export function registerWorktreeRoutes(app: FastifyInstance, deps: RouteDeps) { return { error: "Workspace not found" } } - const { repoRoot } = await resolveRepoRoot(workspace.path, request.log) + const { repoRoot, isGitRepo } = await resolveRepoRoot(workspace.path, request.log) const worktrees = await listWorktrees({ repoRoot, workspaceFolder: workspace.path, logger: request.log }) - const response: WorktreeListResponse = { worktrees } + const response: WorktreeListResponse = { worktrees, isGitRepo } return response }) diff --git a/packages/ui/src/components/worktree-selector.tsx b/packages/ui/src/components/worktree-selector.tsx index d97ae53e..9d288d29 100644 --- a/packages/ui/src/components/worktree-selector.tsx +++ b/packages/ui/src/components/worktree-selector.tsx @@ -10,6 +10,7 @@ import { createWorktree, deleteWorktree, getParentSessionId, + getGitRepoStatus, getWorktreeSlugForParentSession, getWorktrees, reloadWorktreeMap, @@ -85,6 +86,10 @@ export default function WorktreeSelector(props: WorktreeSelectorProps) { const parentId = createMemo(() => getParentSessionId(props.instanceId, props.sessionId)) const currentSlug = createMemo(() => getWorktreeSlugForParentSession(props.instanceId, parentId())) + const gitRepoStatus = createMemo(() => getGitRepoStatus(props.instanceId)) + const worktreesUnavailable = createMemo(() => gitRepoStatus() === false) + const dropdownDisabled = createMemo(() => isChildSession() || worktreesUnavailable()) + const worktreeOptions = createMemo(() => { const list = getWorktrees(props.instanceId) const mapped: WorktreeOption[] = list.map((wt) => ({ @@ -134,6 +139,7 @@ export default function WorktreeSelector(props: WorktreeSelectorProps) { } const handleChange = async (value: WorktreeOption | null) => { + if (worktreesUnavailable()) return if (!value) return if (value.kind === "action") { setIsOpen(false) @@ -157,7 +163,7 @@ export default function WorktreeSelector(props: WorktreeSelectorProps) { optionValue="key" optionTextValue={(opt) => (opt.kind === "action" ? opt.label : opt.slug)} placeholder="Worktree" - disabled={isChildSession()} + disabled={dropdownDisabled()} itemComponent={(itemProps) => { const opt = itemProps.item.rawValue if (opt.kind === "action") { @@ -234,6 +240,14 @@ export default function WorktreeSelector(props: WorktreeSelectorProps) {
> {(state) => { + if (worktreesUnavailable()) { + return ( +
+ Worktree: Unavailable +
+ ) + } + const value = state.selectedOption() const label = value && value.kind === "worktree" ? (value.slug === "root" ? "root" : value.slug) : "root" return ( diff --git a/packages/ui/src/stores/worktrees.ts b/packages/ui/src/stores/worktrees.ts index 5fa283ae..4aef984c 100644 --- a/packages/ui/src/stores/worktrees.ts +++ b/packages/ui/src/stores/worktrees.ts @@ -9,6 +9,7 @@ const log = getLogger("api") const [worktreesByInstance, setWorktreesByInstance] = createSignal>(new Map()) const [worktreeMapByInstance, setWorktreeMapByInstance] = createSignal>(new Map()) +const [gitRepoStatusByInstance, setGitRepoStatusByInstance] = createSignal>(new Map()) const worktreeLoads = new Map>() const mapLoads = new Map>() @@ -26,7 +27,7 @@ function normalizeMap(input?: WorktreeMap | null): WorktreeMap { async function ensureWorktreesLoaded(instanceId: string): Promise { if (!instanceId) return - if (worktreesByInstance().has(instanceId)) return + if (worktreesByInstance().has(instanceId) && gitRepoStatusByInstance().has(instanceId)) return const existing = worktreeLoads.get(instanceId) if (existing) return existing @@ -38,6 +39,12 @@ async function ensureWorktreesLoaded(instanceId: string): Promise { next.set(instanceId, response.worktrees ?? []) return next }) + + setGitRepoStatusByInstance((prev) => { + const next = new Map(prev) + next.set(instanceId, typeof response.isGitRepo === "boolean" ? response.isGitRepo : null) + return next + }) }) .catch((error) => { log.warn("Failed to load worktrees", { instanceId, error }) @@ -46,6 +53,14 @@ async function ensureWorktreesLoaded(instanceId: string): Promise { next.set(instanceId, []) return next }) + + // Preserve any previous value; if unknown, keep it unknown. + setGitRepoStatusByInstance((prev) => { + if (prev.has(instanceId)) return prev + const next = new Map(prev) + next.set(instanceId, null) + return next + }) }) .finally(() => { worktreeLoads.delete(instanceId) @@ -65,12 +80,22 @@ async function reloadWorktrees(instanceId: string): Promise { next.set(instanceId, response.worktrees ?? []) return next }) + + setGitRepoStatusByInstance((prev) => { + const next = new Map(prev) + next.set(instanceId, typeof response.isGitRepo === "boolean" ? response.isGitRepo : null) + return next + }) }) .catch((error) => { log.warn("Failed to reload worktrees", { instanceId, error }) }) } +function getGitRepoStatus(instanceId: string): boolean | null { + return gitRepoStatusByInstance().get(instanceId) ?? null +} + async function createWorktree(instanceId: string, slug: string): Promise<{ slug: string; directory: string; branch?: string }> { if (!instanceId) { throw new Error("Missing instanceId") @@ -242,10 +267,12 @@ function getRootClient(instanceId: string): OpencodeClient { export { worktreesByInstance, worktreeMapByInstance, + gitRepoStatusByInstance, ensureWorktreesLoaded, reloadWorktrees, reloadWorktreeMap, ensureWorktreeMapLoaded, + getGitRepoStatus, getWorktrees, getWorktreeMap, getDefaultWorktreeSlug,