import { Select } from "@kobalte/core/select" import { Dialog } from "@kobalte/core/dialog" import { For, Show, createMemo, createSignal } from "solid-js" import { ChevronDown, Copy, Trash2 } from "lucide-solid" import type { WorktreeDescriptor } from "../../../server/src/api-types" import { getLogger } from "../lib/logger" import { copyToClipboard } from "../lib/clipboard" import { showToastNotification } from "../lib/notifications" import { createWorktree, deleteWorktree, getParentSessionId, getGitRepoStatus, getWorktreeSlugForParentSession, getWorktrees, reloadWorktreeMap, reloadWorktrees, setWorktreeSlugForParentSession, } from "../stores/worktrees" import { sessions } from "../stores/sessions" const log = getLogger("session") type WorktreeOption = | { kind: "action"; key: "__create__"; label: string } | { kind: "worktree"; key: string; slug: string; directory: string; raw: WorktreeDescriptor } const CREATE_OPTION: WorktreeOption = { kind: "action", key: "__create__", label: "+ Create worktree" } function preventSelectPress(event: PointerEvent | MouseEvent) { // Prevent Select.Item from treating this as a selection. // We intentionally prevent default to stop Kobalte's internal press handling. event.preventDefault() event.stopImmediatePropagation?.() event.stopPropagation() } function normalizePath(input: string): string { return (input ?? "").replace(/\\/g, "/").replace(/\/+$/, "") } function relativePath(fromDir: string, toDir: string): string { const from = normalizePath(fromDir) const to = normalizePath(toDir) if (!from || !to) return to || from || "" if (from === to) return "." const fromParts = from.split("/").filter(Boolean) const toParts = to.split("/").filter(Boolean) let i = 0 while (i < fromParts.length && i < toParts.length) { const a = fromParts[i] const b = toParts[i] if (!a || !b) break if (a.toLowerCase() !== b.toLowerCase()) break i++ } const up = fromParts.length - i const down = toParts.slice(i) const relParts: string[] = [] for (let j = 0; j < up; j++) relParts.push("..") relParts.push(...down) return relParts.join("/") || "." } interface WorktreeSelectorProps { instanceId: string sessionId: string } export default function WorktreeSelector(props: WorktreeSelectorProps) { const [isOpen, setIsOpen] = createSignal(false) const [createOpen, setCreateOpen] = createSignal(false) const [createSlug, setCreateSlug] = createSignal("") const [isCreating, setIsCreating] = createSignal(false) const [deleteOpen, setDeleteOpen] = createSignal(false) const [deleteTarget, setDeleteTarget] = createSignal(null) const [forceDelete, setForceDelete] = createSignal(false) const [isDeleting, setIsDeleting] = createSignal(false) const session = createMemo(() => sessions().get(props.instanceId)?.get(props.sessionId)) const isChildSession = createMemo(() => Boolean(session()?.parentId)) 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) => ({ kind: "worktree", key: wt.slug, slug: wt.slug, directory: wt.directory, raw: wt, })) return [CREATE_OPTION, ...mapped] }) const selectedOption = createMemo(() => { const slug = currentSlug() const match = worktreeOptions().find((opt) => opt.kind === "worktree" && opt.slug === slug) if (match) return match // Fallback to root if mapped slug is missing. return worktreeOptions().find((opt) => opt.kind === "worktree" && opt.slug === "root") }) const openDeleteDialog = (opt: WorktreeOption & { kind: "worktree" }) => { if (opt.slug === "root") return setForceDelete(false) setDeleteTarget(opt) setDeleteOpen(true) } const repoRoot = createMemo(() => { const list = getWorktrees(props.instanceId) return list.find((wt) => wt.slug === "root")?.directory ?? "" }) const displayPathFor = (directory: string) => { const base = repoRoot() if (!base) return directory return relativePath(base, directory) } const handleCopyPath = async (directory: string) => { try { const ok = await copyToClipboard(directory) showToastNotification({ message: ok ? "Copied worktree path" : "Failed to copy path", variant: ok ? "success" : "error" }) } catch (error) { log.error("Failed to copy worktree path", error) showToastNotification({ message: "Failed to copy path", variant: "error" }) } } const handleChange = async (value: WorktreeOption | null) => { if (worktreesUnavailable()) return if (!value) return if (value.kind === "action") { setIsOpen(false) setCreateSlug("") setCreateOpen(true) return } await setWorktreeSlugForParentSession(props.instanceId, parentId(), value.slug) } return ( ) }