Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 76 additions & 10 deletions src/tui/component/dialog-new.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -52,6 +53,20 @@ async function commandExists(cmd: string, cwd?: string): Promise<boolean> {
const projectPathHistory = new HistoryManager("dialog-new:project-paths", 15)
const branchNameHistory = new HistoryManager("dialog-new:branch-names", 15)

// Persists form state across dialog.push/pop cycles (confirmation dialog)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool! wdyt about moving the dialog to a multi step dialog in the same way shift+N (remote session) dialog works?

I think it will be much more convenient and in this way users won't create session accidently.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion! I considered the multi-step dialog approach (like the Shift+N remote session flow), but the local session form has highly interdependent fields that make a linear wizard awkward:

  • The worktree checkbox only appears when the project path is a git repo
  • The branch input only appears when worktree is checked
  • Resume/Skip Permissions options only appear when Claude is selected as the tool

For the remote session dialog, each step is independent (host → av path → tool → path → title), so the wizard pattern fits naturally. But for local sessions, users benefit from seeing all relevant options at once and navigating with Tab — it's more of
a configuration panel than a sequential flow.

The current implementation uses a module-level variable to preserve form state across the dialog.push()/dialog.pop() cycle, so state preservation works correctly without restructuring the UX.

That said, happy to reconsider if you feel strongly about consistency with the remote session pattern!

interface SavedFormState {
title: string
selectedTool: Tool
toolIndex: number
claudeSessionMode: ClaudeSessionMode
skipPermissions: boolean
customCommand: string
projectPath: string
useWorktree: boolean
worktreeBranch: string
}
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" },
Expand All @@ -72,13 +87,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<Tool>(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)
Expand All @@ -95,19 +114,19 @@ export function DialogNew() {
}
})

const [claudeSessionMode, setClaudeSessionMode] = createSignal<ClaudeSessionMode>("new")
const [skipPermissions, setSkipPermissions] = createSignal(false)
const [claudeSessionMode, setClaudeSessionMode] = createSignal<ClaudeSessionMode>(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 storage = getStorage()

const [focusedField, setFocusedField] = createSignal<FocusField>("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
Expand Down Expand Up @@ -192,7 +211,7 @@ export function DialogNew() {
return fields
}

async function handleCreate() {
async function doCreate() {
if (creating()) return
setCreating(true)
setStatusMessage("Preparing...")
Expand Down Expand Up @@ -295,6 +314,53 @@ 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(),
}

// 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)"}`)
}

// Capture doCreate in closure before DialogNew is unmounted by dialog.push
const capturedDoCreate = doCreate
dialog.push(() => (
<DialogSelect
title={`Create session?\n\n${lines.join("\n")}`}
options={[
{ title: "✅ Confirm", value: "confirm" },
{ title: "❌ Back", value: "back" },
]}
onSelect={(opt) => {
if (opt.value === "confirm") {
_savedFormState = null
dialog.clear()
capturedDoCreate()
} else {
dialog.pop()
}
}}
/>
))
}

useKeyboard((evt) => {
if (evt.name === "escape") {
evt.preventDefault()
Expand Down