@@ -274,6 +296,8 @@ export function NewSession(props: {
selectedIndex={selectedIndex}
isDisabled={isFormDisabled}
recentPaths={recentPaths}
+ statusMessage={directoryStatusMessage}
+ statusTone={directoryStatusTone}
onDirectoryChange={handleDirectoryChange}
onDirectoryFocus={handleDirectoryFocus}
onDirectoryBlur={handleDirectoryBlur}
@@ -322,6 +346,7 @@ export function NewSession(props: {
isPending={isPending}
canCreate={canCreate}
isDisabled={isFormDisabled}
+ createLabel={createLabel}
onCancel={props.onCancel}
onCreate={handleCreate}
/>
diff --git a/web/src/components/SpawnSession.tsx b/web/src/components/SpawnSession.tsx
index 2b215a3d..407ed62d 100644
--- a/web/src/components/SpawnSession.tsx
+++ b/web/src/components/SpawnSession.tsx
@@ -1,8 +1,9 @@
-import { useMemo, useState } from 'react'
+import { useDeferredValue, useEffect, useMemo, useState } from 'react'
import type { ApiClient } from '@/api/client'
import type { Machine } from '@/types/api'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
+import { useMachinePathsExists } from '@/hooks/useMachinePathsExists'
import { usePlatform } from '@/hooks/usePlatform'
import { useSpawnSession } from '@/hooks/mutations/useSpawnSession'
import { formatRunnerSpawnError } from '@/utils/formatRunnerSpawnError'
@@ -29,6 +30,7 @@ export function SpawnSession(props: {
const [directory, setDirectory] = useState('')
const [sessionType, setSessionType] = useState
('simple')
const [worktreeName, setWorktreeName] = useState('')
+ const [directoryCreationConfirmed, setDirectoryCreationConfirmed] = useState(false)
const [error, setError] = useState(null)
const { spawnSession, isPending, error: spawnError } = useSpawnSession(props.api)
@@ -37,16 +39,59 @@ export function SpawnSession(props: {
() => formatRunnerSpawnError(props.machine),
[props.machine?.runnerState?.lastSpawnError]
)
+ const trimmedDirectory = directory.trim()
+ const deferredDirectory = useDeferredValue(trimmedDirectory)
+ const pathsToCheck = useMemo(
+ () => deferredDirectory ? [deferredDirectory] : [],
+ [deferredDirectory]
+ )
+ const { pathExistence, checkPathsExists } = useMachinePathsExists(
+ props.api,
+ props.machineId,
+ pathsToCheck
+ )
+ const currentDirectoryExists = trimmedDirectory ? pathExistence[trimmedDirectory] : undefined
+ const needsDirectoryCreationWarning = sessionType === 'simple' && trimmedDirectory !== '' && currentDirectoryExists === false
+ const missingWorktreeDirectory = sessionType === 'worktree' && trimmedDirectory !== '' && currentDirectoryExists === false
+ const directoryStatusMessage = missingWorktreeDirectory
+ ? t('session.directoryMissingWorktree')
+ : needsDirectoryCreationWarning
+ ? (
+ directoryCreationConfirmed
+ ? t('session.directoryMissingSimpleConfirm')
+ : t('session.directoryMissingSimple')
+ )
+ : null
+ const createLabel = needsDirectoryCreationWarning && directoryCreationConfirmed
+ ? t('session.createAndCreateDirectory')
+ : t('spawn.create')
+
+ useEffect(() => {
+ setDirectoryCreationConfirmed(false)
+ }, [props.machineId, sessionType, trimmedDirectory])
async function spawn() {
- const trimmed = directory.trim()
- if (!trimmed) return
+ if (!trimmedDirectory) return
setError(null)
try {
+ const existsResult = await checkPathsExists([trimmedDirectory])
+ const directoryExists = existsResult[trimmedDirectory]
+
+ if (sessionType === 'worktree' && directoryExists === false) {
+ haptic.notification('error')
+ setError(t('session.directoryMissingWorktree'))
+ return
+ }
+
+ if (sessionType === 'simple' && directoryExists === false && !directoryCreationConfirmed) {
+ setDirectoryCreationConfirmed(true)
+ return
+ }
+
const result = await spawnSession({
machineId: props.machineId,
- directory: trimmed,
+ directory: trimmedDirectory,
sessionType,
worktreeName: sessionType === 'worktree' ? (worktreeName.trim() || undefined) : undefined
})
@@ -82,6 +127,16 @@ export function SpawnSession(props: {
className="w-full rounded-md border border-[var(--app-border)] bg-[var(--app-bg)] p-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--app-link)]"
/>
+ {directoryStatusMessage ? (
+
+ {directoryStatusMessage}
+
+ ) : null}
+
diff --git a/web/src/hooks/useMachinePathsExists.ts b/web/src/hooks/useMachinePathsExists.ts
new file mode 100644
index 00000000..2ee4781a
--- /dev/null
+++ b/web/src/hooks/useMachinePathsExists.ts
@@ -0,0 +1,58 @@
+import { useCallback, useEffect, useState } from 'react'
+import type { ApiClient } from '@/api/client'
+
+export function useMachinePathsExists(
+ api: ApiClient,
+ machineId: string | null,
+ paths: string[]
+): {
+ pathExistence: Record