diff --git a/src/tui/component/dialog-new.tsx b/src/tui/component/dialog-new.tsx index 0ad0dda..85d5fbf 100644 --- a/src/tui/component/dialog-new.tsx +++ b/src/tui/component/dialog-new.tsx @@ -10,13 +10,14 @@ import { useSync } from "@tui/context/sync" import { useRoute } from "@tui/context/route" import { useConfig } from "@tui/context/config" import { useDialog, scrollDialogBy, scrollDialogTo } from "@tui/ui/dialog" +import { DialogSelect } from "@tui/ui/dialog-select" import { useToast } from "@tui/ui/toast" import { InputAutocomplete } from "@tui/ui/input-autocomplete" import { DialogHeader } from "@tui/ui/dialog-header" import { DialogFooter } from "@tui/ui/dialog-footer" import { ActionButton } from "@tui/ui/action-button" import { attachSessionSync } from "@/core/tmux" -import { isGitRepo, getRepoRoot, createWorktree, generateBranchName, generateWorktreePath, sanitizeBranchName, branchExists } from "@/core/git" +import { isGitRepo, getRepoRoot, createWorktree, generateBranchName, generateWorktreePath, sanitizeBranchName, branchExists, copyClaudeDir } from "@/core/git" import { HistoryManager } from "@/core/history" import { getStorage } from "@/core/storage" import type { Tool, ClaudeSessionMode } from "@/core/types" @@ -52,6 +53,21 @@ async function commandExists(cmd: string, cwd?: string): Promise { const projectPathHistory = new HistoryManager("dialog-new:project-paths", 30) const branchNameHistory = new HistoryManager("dialog-new:branch-names", 30) +// Persists form state across dialog.push/pop cycles (confirmation dialog) +interface SavedFormState { + title: string + selectedTool: Tool + toolIndex: number + claudeSessionMode: ClaudeSessionMode + skipPermissions: boolean + customCommand: string + projectPath: string + useWorktree: boolean + worktreeBranch: string + doCopyClaudeDir: boolean +} +let _savedFormState: SavedFormState | null = null + const TOOLS: { value: Tool; label: string; description: string }[] = [ { value: "claude", label: "Claude Code", description: "Anthropic's Claude CLI" }, { value: "opencode", label: "OpenCode", description: "OpenCode CLI" }, @@ -61,7 +77,7 @@ const TOOLS: { value: Tool; label: string; description: string }[] = [ { value: "shell", label: "Shell", description: "Plain terminal session" } ] -type FocusField = "title" | "tool" | "resumeSession" | "skipPermissions" | "customCommand" | "path" | "worktree" | "branch" +type FocusField = "title" | "tool" | "resumeSession" | "skipPermissions" | "customCommand" | "path" | "worktree" | "branch" | "copyClaudeDir" export function DialogNew() { const dialog = useDialog() @@ -72,13 +88,17 @@ export function DialogNew() { const renderer = useRenderer() const { config } = useConfig() - const defaultTool = config().defaultTool || "claude" + // Restore state saved before confirmation push, then clear it + const restore = _savedFormState + _savedFormState = null + + const defaultTool = restore?.selectedTool ?? (config().defaultTool || "claude") const defaultToolIndex = TOOLS.findIndex(t => t.value === defaultTool) - const [title, setTitle] = createSignal("") + const [title, setTitle] = createSignal(restore?.title ?? "") const [selectedTool, setSelectedTool] = createSignal(defaultTool) - const [customCommand, setCustomCommand] = createSignal("") - const [projectPath, setProjectPath] = createSignal(process.cwd()) + const [customCommand, setCustomCommand] = createSignal(restore?.customCommand ?? "") + const [projectPath, setProjectPath] = createSignal(restore?.projectPath ?? process.cwd()) const [creating, setCreating] = createSignal(false) const [statusMessage, setStatusMessage] = createSignal("") const [spinnerFrame, setSpinnerFrame] = createSignal(0) @@ -95,19 +115,21 @@ export function DialogNew() { } }) - const [claudeSessionMode, setClaudeSessionMode] = createSignal("new") - const [skipPermissions, setSkipPermissions] = createSignal(false) + const [claudeSessionMode, setClaudeSessionMode] = createSignal(restore?.claudeSessionMode ?? "new") + const [skipPermissions, setSkipPermissions] = createSignal(restore?.skipPermissions ?? false) - const [useWorktree, setUseWorktree] = createSignal(false) - const [worktreeBranch, setWorktreeBranch] = createSignal("") + const [useWorktree, setUseWorktree] = createSignal(restore?.useWorktree ?? false) + const [worktreeBranch, setWorktreeBranch] = createSignal(restore?.worktreeBranch ?? "") const [isInGitRepo, setIsInGitRepo] = createSignal(false) const [useBaseDevelop, setUseBaseDevelop] = createSignal(false) const [developExists, setDevelopExists] = createSignal(false) + const [claudeDirExists, setClaudeDirExists] = createSignal(false) + const [doCopyClaudeDir, setDoCopyClaudeDir] = createSignal(restore?.doCopyClaudeDir ?? true) const storage = getStorage() const [focusedField, setFocusedField] = createSignal("title") - const [toolIndex, setToolIndex] = createSignal(defaultToolIndex >= 0 ? defaultToolIndex : 0) + const [toolIndex, setToolIndex] = createSignal(restore?.toolIndex ?? (defaultToolIndex >= 0 ? defaultToolIndex : 0)) let titleInputRef: InputRenderable | undefined let customCommandInputRef: InputRenderable | undefined @@ -121,27 +143,32 @@ export function DialogNew() { }) createEffect(async () => { - const path = projectPath() + const dir = projectPath() try { - const result = await isGitRepo(path) + const result = await isGitRepo(dir) setIsInGitRepo(result) if (!result) { setUseWorktree(false) setDevelopExists(false) setUseBaseDevelop(false) + setClaudeDirExists(false) } else { - const repoRoot = await getRepoRoot(path) + const repoRoot = await getRepoRoot(dir) const hasDevelop = await branchExists(repoRoot, "develop") setDevelopExists(hasDevelop) if (!hasDevelop) { setUseBaseDevelop(false) } + const hasClaude = existsSync(path.join(repoRoot, ".claude")) + setClaudeDirExists(hasClaude) + if (!hasClaude) setDoCopyClaudeDir(false) } } catch { setIsInGitRepo(false) setUseWorktree(false) setDevelopExists(false) setUseBaseDevelop(false) + setClaudeDirExists(false) } }) @@ -187,12 +214,15 @@ export function DialogNew() { fields.push("worktree") if (useWorktree()) { fields.push("branch") + if (claudeDirExists()) { + fields.push("copyClaudeDir") + } } } return fields } - async function handleCreate() { + async function doCreate() { if (creating()) return setCreating(true) setStatusMessage("Preparing...") @@ -244,6 +274,9 @@ export function DialogNew() { const wtPath = generateWorktreePath(repoRoot, branchName) worktreePath = await createWorktree(repoRoot, branchName, wtPath, baseBranch) + if (doCopyClaudeDir() && claudeDirExists()) { + await copyClaudeDir(repoRoot, worktreePath) + } sessionProjectPath = worktreePath worktreeRepo = repoRoot worktreeBranchName = branchName @@ -295,6 +328,57 @@ export function DialogNew() { } } + function handleCreate() { + // Save form state so it survives dialog.push/pop cycle + _savedFormState = { + title: title(), + selectedTool: selectedTool(), + toolIndex: toolIndex(), + claudeSessionMode: claudeSessionMode(), + skipPermissions: skipPermissions(), + customCommand: customCommand(), + projectPath: projectPath(), + useWorktree: useWorktree(), + worktreeBranch: worktreeBranch(), + doCopyClaudeDir: doCopyClaudeDir(), + } + + // Build summary lines for the confirmation dialog + const lines: string[] = [] + lines.push(`Tool: ${selectedTool()}`) + const t = title().trim() + lines.push(`Title: ${t || "(auto-generated)"}`) + lines.push(`Path: ${projectPath() || process.cwd()}`) + if (useWorktree()) { + const branch = worktreeBranch().trim() + lines.push(`Branch: ${branch || "(auto-generated)"}`) + if (claudeDirExists() && doCopyClaudeDir()) { + lines.push(`.claude: will be copied`) + } + } + + // Capture doCreate in closure before DialogNew is unmounted by dialog.push + const capturedDoCreate = doCreate + dialog.push(() => ( + { + if (opt.value === "confirm") { + _savedFormState = null + dialog.clear() + capturedDoCreate() + } else { + dialog.pop() + } + }} + /> + )) + } + useKeyboard((evt) => { if (evt.name === "escape") { evt.preventDefault() @@ -380,6 +464,12 @@ export function DialogNew() { setSkipPermissions(!skipPermissions()) return } + + if (focusedField() === "copyClaudeDir" && evt.name === "space") { + evt.preventDefault() + setDoCopyClaudeDir(!doCopyClaudeDir()) + return + } }) return ( @@ -584,6 +674,26 @@ export function DialogNew() { Base on develop + + {/* Copy .claude directory toggle */} + + { + setFocusedField("copyClaudeDir") + setDoCopyClaudeDir(!doCopyClaudeDir()) + }} + > + + {doCopyClaudeDir() ? "[x]" : "[ ]"} + + + Copy .claude directory + + + @@ -610,7 +720,7 @@ export function DialogNew() { onAction={handleCreate} /> - + ) }