From 57e56469cdd70c3356cbd7bf4be7f891797bf3ec Mon Sep 17 00:00:00 2001 From: huanghuifeng Date: Thu, 5 Mar 2026 19:04:52 +0800 Subject: [PATCH 1/2] feat: add confirmation dialog before creating new session --- src/tui/component/dialog-new.tsx | 33 +++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/tui/component/dialog-new.tsx b/src/tui/component/dialog-new.tsx index 0ad0dda..556b658 100644 --- a/src/tui/component/dialog-new.tsx +++ b/src/tui/component/dialog-new.tsx @@ -10,6 +10,7 @@ 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" @@ -192,7 +193,7 @@ export function DialogNew() { return fields } - async function handleCreate() { + async function doCreate() { if (creating()) return setCreating(true) setStatusMessage("Preparing...") @@ -295,6 +296,36 @@ export function DialogNew() { } } + function handleCreate() { + // 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)"}`) + + } + + dialog.push(() => ( + { + dialog.pop() + if (opt.value === "confirm") { + doCreate() + } + }} + /> + )) + } + useKeyboard((evt) => { if (evt.name === "escape") { evt.preventDefault() From 32c2e6d5c8da8737cc5ff66a82d8902deec95514 Mon Sep 17 00:00:00 2001 From: huanghuifeng Date: Mon, 9 Mar 2026 16:39:39 +0800 Subject: [PATCH 2/2] fix: preserve form state and add Esc-to-back in confirmation dialog Two improvements to the new session confirmation dialog: 1. Esc on the confirmation screen now returns to the form instead of closing the entire dialog. Previously, DialogProvider's keyboard handler would intercept Esc and pop DialogNew off the stack entirely. 2. Form state is no longer lost when navigating back from confirmation. Previously, dialog.push() caused DialogNew to unmount, so all signal values were reset on re-mount. Now all form signals are saved to a module-level variable before pushing the confirmation dialog, and restored when DialogNew re-mounts after dialog.pop(). Co-Authored-By: Claude Sonnet 4.6 --- src/tui/component/dialog-new.tsx | 115 ++++++++++++++++++++++++++----- 1 file changed, 97 insertions(+), 18 deletions(-) diff --git a/src/tui/component/dialog-new.tsx b/src/tui/component/dialog-new.tsx index 556b658..85d5fbf 100644 --- a/src/tui/component/dialog-new.tsx +++ b/src/tui/component/dialog-new.tsx @@ -17,7 +17,7 @@ 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" @@ -53,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" }, @@ -62,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() @@ -73,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) @@ -96,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 @@ -122,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) } }) @@ -188,6 +214,9 @@ export function DialogNew() { fields.push("worktree") if (useWorktree()) { fields.push("branch") + if (claudeDirExists()) { + fields.push("copyClaudeDir") + } } } return fields @@ -245,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 @@ -297,6 +329,20 @@ 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()}`) @@ -306,9 +352,13 @@ export function DialogNew() { 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(() => ( { - dialog.pop() if (opt.value === "confirm") { - doCreate() + _savedFormState = null + dialog.clear() + capturedDoCreate() + } else { + dialog.pop() } }} /> @@ -411,6 +464,12 @@ export function DialogNew() { setSkipPermissions(!skipPermissions()) return } + + if (focusedField() === "copyClaudeDir" && evt.name === "space") { + evt.preventDefault() + setDoCopyClaudeDir(!doCopyClaudeDir()) + return + } }) return ( @@ -615,6 +674,26 @@ export function DialogNew() { Base on develop + + {/* Copy .claude directory toggle */} + + { + setFocusedField("copyClaudeDir") + setDoCopyClaudeDir(!doCopyClaudeDir()) + }} + > + + {doCopyClaudeDir() ? "[x]" : "[ ]"} + + + Copy .claude directory + + + @@ -641,7 +720,7 @@ export function DialogNew() { onAction={handleCreate} /> - + ) }