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.
This commit is contained in:
@@ -64,6 +64,8 @@ export interface WorktreeDescriptor {
|
|||||||
|
|
||||||
export interface WorktreeListResponse {
|
export interface WorktreeListResponse {
|
||||||
worktrees: WorktreeDescriptor[]
|
worktrees: WorktreeDescriptor[]
|
||||||
|
/** True when the workspace folder resolves to a Git repository. */
|
||||||
|
isGitRepo?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorktreeCreateRequest {
|
export interface WorktreeCreateRequest {
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ export function registerWorktreeRoutes(app: FastifyInstance, deps: RouteDeps) {
|
|||||||
return { error: "Workspace not found" }
|
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 worktrees = await listWorktrees({ repoRoot, workspaceFolder: workspace.path, logger: request.log })
|
||||||
const response: WorktreeListResponse = { worktrees }
|
const response: WorktreeListResponse = { worktrees, isGitRepo }
|
||||||
return response
|
return response
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
createWorktree,
|
createWorktree,
|
||||||
deleteWorktree,
|
deleteWorktree,
|
||||||
getParentSessionId,
|
getParentSessionId,
|
||||||
|
getGitRepoStatus,
|
||||||
getWorktreeSlugForParentSession,
|
getWorktreeSlugForParentSession,
|
||||||
getWorktrees,
|
getWorktrees,
|
||||||
reloadWorktreeMap,
|
reloadWorktreeMap,
|
||||||
@@ -85,6 +86,10 @@ export default function WorktreeSelector(props: WorktreeSelectorProps) {
|
|||||||
const parentId = createMemo(() => getParentSessionId(props.instanceId, props.sessionId))
|
const parentId = createMemo(() => getParentSessionId(props.instanceId, props.sessionId))
|
||||||
const currentSlug = createMemo(() => getWorktreeSlugForParentSession(props.instanceId, parentId()))
|
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<WorktreeOption[]>(() => {
|
const worktreeOptions = createMemo<WorktreeOption[]>(() => {
|
||||||
const list = getWorktrees(props.instanceId)
|
const list = getWorktrees(props.instanceId)
|
||||||
const mapped: WorktreeOption[] = list.map((wt) => ({
|
const mapped: WorktreeOption[] = list.map((wt) => ({
|
||||||
@@ -134,6 +139,7 @@ export default function WorktreeSelector(props: WorktreeSelectorProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleChange = async (value: WorktreeOption | null) => {
|
const handleChange = async (value: WorktreeOption | null) => {
|
||||||
|
if (worktreesUnavailable()) return
|
||||||
if (!value) return
|
if (!value) return
|
||||||
if (value.kind === "action") {
|
if (value.kind === "action") {
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
@@ -157,7 +163,7 @@ export default function WorktreeSelector(props: WorktreeSelectorProps) {
|
|||||||
optionValue="key"
|
optionValue="key"
|
||||||
optionTextValue={(opt) => (opt.kind === "action" ? opt.label : opt.slug)}
|
optionTextValue={(opt) => (opt.kind === "action" ? opt.label : opt.slug)}
|
||||||
placeholder="Worktree"
|
placeholder="Worktree"
|
||||||
disabled={isChildSession()}
|
disabled={dropdownDisabled()}
|
||||||
itemComponent={(itemProps) => {
|
itemComponent={(itemProps) => {
|
||||||
const opt = itemProps.item.rawValue
|
const opt = itemProps.item.rawValue
|
||||||
if (opt.kind === "action") {
|
if (opt.kind === "action") {
|
||||||
@@ -234,6 +240,14 @@ export default function WorktreeSelector(props: WorktreeSelectorProps) {
|
|||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<Select.Value<WorktreeOption>>
|
<Select.Value<WorktreeOption>>
|
||||||
{(state) => {
|
{(state) => {
|
||||||
|
if (worktreesUnavailable()) {
|
||||||
|
return (
|
||||||
|
<div class="selector-trigger-label selector-trigger-label--stacked">
|
||||||
|
<span class="selector-trigger-primary selector-trigger-primary--align-left">Worktree: Unavailable</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const value = state.selectedOption()
|
const value = state.selectedOption()
|
||||||
const label = value && value.kind === "worktree" ? (value.slug === "root" ? "root" : value.slug) : "root"
|
const label = value && value.kind === "worktree" ? (value.slug === "root" ? "root" : value.slug) : "root"
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const log = getLogger("api")
|
|||||||
|
|
||||||
const [worktreesByInstance, setWorktreesByInstance] = createSignal<Map<string, WorktreeDescriptor[]>>(new Map())
|
const [worktreesByInstance, setWorktreesByInstance] = createSignal<Map<string, WorktreeDescriptor[]>>(new Map())
|
||||||
const [worktreeMapByInstance, setWorktreeMapByInstance] = createSignal<Map<string, WorktreeMap>>(new Map())
|
const [worktreeMapByInstance, setWorktreeMapByInstance] = createSignal<Map<string, WorktreeMap>>(new Map())
|
||||||
|
const [gitRepoStatusByInstance, setGitRepoStatusByInstance] = createSignal<Map<string, boolean | null>>(new Map())
|
||||||
|
|
||||||
const worktreeLoads = new Map<string, Promise<void>>()
|
const worktreeLoads = new Map<string, Promise<void>>()
|
||||||
const mapLoads = new Map<string, Promise<void>>()
|
const mapLoads = new Map<string, Promise<void>>()
|
||||||
@@ -26,7 +27,7 @@ function normalizeMap(input?: WorktreeMap | null): WorktreeMap {
|
|||||||
|
|
||||||
async function ensureWorktreesLoaded(instanceId: string): Promise<void> {
|
async function ensureWorktreesLoaded(instanceId: string): Promise<void> {
|
||||||
if (!instanceId) return
|
if (!instanceId) return
|
||||||
if (worktreesByInstance().has(instanceId)) return
|
if (worktreesByInstance().has(instanceId) && gitRepoStatusByInstance().has(instanceId)) return
|
||||||
const existing = worktreeLoads.get(instanceId)
|
const existing = worktreeLoads.get(instanceId)
|
||||||
if (existing) return existing
|
if (existing) return existing
|
||||||
|
|
||||||
@@ -38,6 +39,12 @@ async function ensureWorktreesLoaded(instanceId: string): Promise<void> {
|
|||||||
next.set(instanceId, response.worktrees ?? [])
|
next.set(instanceId, response.worktrees ?? [])
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
|
|
||||||
|
setGitRepoStatusByInstance((prev) => {
|
||||||
|
const next = new Map(prev)
|
||||||
|
next.set(instanceId, typeof response.isGitRepo === "boolean" ? response.isGitRepo : null)
|
||||||
|
return next
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
log.warn("Failed to load worktrees", { instanceId, error })
|
log.warn("Failed to load worktrees", { instanceId, error })
|
||||||
@@ -46,6 +53,14 @@ async function ensureWorktreesLoaded(instanceId: string): Promise<void> {
|
|||||||
next.set(instanceId, [])
|
next.set(instanceId, [])
|
||||||
return next
|
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(() => {
|
.finally(() => {
|
||||||
worktreeLoads.delete(instanceId)
|
worktreeLoads.delete(instanceId)
|
||||||
@@ -65,12 +80,22 @@ async function reloadWorktrees(instanceId: string): Promise<void> {
|
|||||||
next.set(instanceId, response.worktrees ?? [])
|
next.set(instanceId, response.worktrees ?? [])
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
|
|
||||||
|
setGitRepoStatusByInstance((prev) => {
|
||||||
|
const next = new Map(prev)
|
||||||
|
next.set(instanceId, typeof response.isGitRepo === "boolean" ? response.isGitRepo : null)
|
||||||
|
return next
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
log.warn("Failed to reload worktrees", { instanceId, 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 }> {
|
async function createWorktree(instanceId: string, slug: string): Promise<{ slug: string; directory: string; branch?: string }> {
|
||||||
if (!instanceId) {
|
if (!instanceId) {
|
||||||
throw new Error("Missing instanceId")
|
throw new Error("Missing instanceId")
|
||||||
@@ -242,10 +267,12 @@ function getRootClient(instanceId: string): OpencodeClient {
|
|||||||
export {
|
export {
|
||||||
worktreesByInstance,
|
worktreesByInstance,
|
||||||
worktreeMapByInstance,
|
worktreeMapByInstance,
|
||||||
|
gitRepoStatusByInstance,
|
||||||
ensureWorktreesLoaded,
|
ensureWorktreesLoaded,
|
||||||
reloadWorktrees,
|
reloadWorktrees,
|
||||||
reloadWorktreeMap,
|
reloadWorktreeMap,
|
||||||
ensureWorktreeMapLoaded,
|
ensureWorktreeMapLoaded,
|
||||||
|
getGitRepoStatus,
|
||||||
getWorktrees,
|
getWorktrees,
|
||||||
getWorktreeMap,
|
getWorktreeMap,
|
||||||
getDefaultWorktreeSlug,
|
getDefaultWorktreeSlug,
|
||||||
|
|||||||
Reference in New Issue
Block a user