@neurocode-ai/tui 1.18.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. package/bunfig.toml +4 -0
  2. package/package.json +72 -0
  3. package/src/app.tsx +1134 -0
  4. package/src/attention.ts +260 -0
  5. package/src/audio.d.ts +9 -0
  6. package/src/audio.ts +53 -0
  7. package/src/clipboard.ts +124 -0
  8. package/src/component/bg-pulse-render.ts +436 -0
  9. package/src/component/bg-pulse.tsx +99 -0
  10. package/src/component/command-palette.tsx +79 -0
  11. package/src/component/dialog-agent.tsx +31 -0
  12. package/src/component/dialog-console-org.tsx +135 -0
  13. package/src/component/dialog-debug.tsx +90 -0
  14. package/src/component/dialog-mcp.tsx +85 -0
  15. package/src/component/dialog-model.tsx +197 -0
  16. package/src/component/dialog-move-session.tsx +353 -0
  17. package/src/component/dialog-provider.tsx +469 -0
  18. package/src/component/dialog-retry-action.tsx +160 -0
  19. package/src/component/dialog-session-delete-failed.tsx +99 -0
  20. package/src/component/dialog-session-list.tsx +364 -0
  21. package/src/component/dialog-session-rename.tsx +31 -0
  22. package/src/component/dialog-skill.tsx +70 -0
  23. package/src/component/dialog-stash.tsx +87 -0
  24. package/src/component/dialog-status.tsx +168 -0
  25. package/src/component/dialog-tag.tsx +47 -0
  26. package/src/component/dialog-theme-list.tsx +50 -0
  27. package/src/component/dialog-variant.tsx +39 -0
  28. package/src/component/dialog-workspace-create.tsx +308 -0
  29. package/src/component/dialog-workspace-file-changes.tsx +144 -0
  30. package/src/component/dialog-workspace-list.tsx +112 -0
  31. package/src/component/dialog-workspace-unavailable.tsx +69 -0
  32. package/src/component/error-component.tsx +240 -0
  33. package/src/component/logo.tsx +61 -0
  34. package/src/component/plugin-route-missing.tsx +14 -0
  35. package/src/component/prompt/autocomplete.tsx +781 -0
  36. package/src/component/prompt/cwd.ts +0 -0
  37. package/src/component/prompt/frecency.tsx +1 -0
  38. package/src/component/prompt/history.tsx +1 -0
  39. package/src/component/prompt/index.tsx +1713 -0
  40. package/src/component/prompt/local-attachment.ts +48 -0
  41. package/src/component/prompt/move.tsx +205 -0
  42. package/src/component/prompt/stash.tsx +1 -0
  43. package/src/component/prompt/workspace.tsx +137 -0
  44. package/src/component/register-spinner.ts +6 -0
  45. package/src/component/spinner.tsx +26 -0
  46. package/src/component/startup-loading.tsx +63 -0
  47. package/src/component/todo-item.tsx +32 -0
  48. package/src/component/use-connected.tsx +12 -0
  49. package/src/component/workspace-label.tsx +19 -0
  50. package/src/config/index.tsx +129 -0
  51. package/src/config/keybind.ts +471 -0
  52. package/src/context/args.tsx +16 -0
  53. package/src/context/clipboard.tsx +18 -0
  54. package/src/context/data.tsx +569 -0
  55. package/src/context/directory.ts +17 -0
  56. package/src/context/editor.ts +408 -0
  57. package/src/context/epilogue.tsx +6 -0
  58. package/src/context/event.ts +36 -0
  59. package/src/context/exit.tsx +8 -0
  60. package/src/context/helper.tsx +26 -0
  61. package/src/context/kv.tsx +66 -0
  62. package/src/context/local.tsx +542 -0
  63. package/src/context/location.tsx +14 -0
  64. package/src/context/path-format.tsx +24 -0
  65. package/src/context/permission.tsx +26 -0
  66. package/src/context/project.tsx +115 -0
  67. package/src/context/prompt.tsx +18 -0
  68. package/src/context/route.tsx +60 -0
  69. package/src/context/runtime.tsx +62 -0
  70. package/src/context/sdk.tsx +151 -0
  71. package/src/context/sync.tsx +666 -0
  72. package/src/context/theme.tsx +332 -0
  73. package/src/context/thinking.ts +67 -0
  74. package/src/editor-zed.ts +286 -0
  75. package/src/editor.ts +101 -0
  76. package/src/feature-plugins/builtins.ts +36 -0
  77. package/src/feature-plugins/home/footer.tsx +100 -0
  78. package/src/feature-plugins/home/tips-view.tsx +287 -0
  79. package/src/feature-plugins/home/tips.tsx +59 -0
  80. package/src/feature-plugins/sidebar/context.tsx +65 -0
  81. package/src/feature-plugins/sidebar/files.tsx +70 -0
  82. package/src/feature-plugins/sidebar/footer.tsx +98 -0
  83. package/src/feature-plugins/sidebar/lsp.tsx +65 -0
  84. package/src/feature-plugins/sidebar/mcp.tsx +97 -0
  85. package/src/feature-plugins/sidebar/todo.tsx +49 -0
  86. package/src/feature-plugins/system/diff-viewer-file-tree-utils.ts +232 -0
  87. package/src/feature-plugins/system/diff-viewer-file-tree.tsx +162 -0
  88. package/src/feature-plugins/system/diff-viewer-ui.tsx +103 -0
  89. package/src/feature-plugins/system/diff-viewer.tsx +1077 -0
  90. package/src/feature-plugins/system/notifications.ts +94 -0
  91. package/src/feature-plugins/system/plugins.tsx +269 -0
  92. package/src/feature-plugins/system/which-key.tsx +608 -0
  93. package/src/index.tsx +1 -0
  94. package/src/keymap.tsx +290 -0
  95. package/src/logo.ts +11 -0
  96. package/src/parsers-config.ts +386 -0
  97. package/src/plugin/adapters.tsx +355 -0
  98. package/src/plugin/api.ts +52 -0
  99. package/src/plugin/command-shim.ts +109 -0
  100. package/src/plugin/runtime.tsx +81 -0
  101. package/src/plugin/slots.tsx +65 -0
  102. package/src/prompt/display.ts +48 -0
  103. package/src/prompt/frecency.tsx +80 -0
  104. package/src/prompt/history.tsx +111 -0
  105. package/src/prompt/part.ts +29 -0
  106. package/src/prompt/stash.tsx +89 -0
  107. package/src/prompt/traits.ts +29 -0
  108. package/src/routes/home/session-destination.tsx +41 -0
  109. package/src/routes/home.tsx +95 -0
  110. package/src/routes/session/dialog-fork-from-timeline.tsx +76 -0
  111. package/src/routes/session/dialog-message.tsx +109 -0
  112. package/src/routes/session/dialog-subagent.tsx +26 -0
  113. package/src/routes/session/dialog-timeline.tsx +47 -0
  114. package/src/routes/session/footer.tsx +91 -0
  115. package/src/routes/session/index.tsx +2710 -0
  116. package/src/routes/session/permission.tsx +718 -0
  117. package/src/routes/session/question.tsx +514 -0
  118. package/src/routes/session/sidebar.tsx +103 -0
  119. package/src/routes/session/subagent-footer.tsx +132 -0
  120. package/src/runtime.tsx +9 -0
  121. package/src/terminal-win32.ts +130 -0
  122. package/src/theme/assets/aura.json +69 -0
  123. package/src/theme/assets/ayu.json +80 -0
  124. package/src/theme/assets/carbonfox.json +248 -0
  125. package/src/theme/assets/catppuccin-frappe.json +230 -0
  126. package/src/theme/assets/catppuccin-macchiato.json +230 -0
  127. package/src/theme/assets/catppuccin.json +112 -0
  128. package/src/theme/assets/cobalt2.json +225 -0
  129. package/src/theme/assets/cursor.json +249 -0
  130. package/src/theme/assets/dracula.json +219 -0
  131. package/src/theme/assets/everforest.json +241 -0
  132. package/src/theme/assets/flexoki.json +237 -0
  133. package/src/theme/assets/github.json +233 -0
  134. package/src/theme/assets/gruvbox.json +242 -0
  135. package/src/theme/assets/kanagawa.json +77 -0
  136. package/src/theme/assets/lucent-orng.json +234 -0
  137. package/src/theme/assets/material.json +235 -0
  138. package/src/theme/assets/matrix.json +77 -0
  139. package/src/theme/assets/mercury.json +252 -0
  140. package/src/theme/assets/monokai.json +221 -0
  141. package/src/theme/assets/nightowl.json +221 -0
  142. package/src/theme/assets/nord.json +223 -0
  143. package/src/theme/assets/one-dark.json +84 -0
  144. package/src/theme/assets/opencode.json +245 -0
  145. package/src/theme/assets/orng.json +249 -0
  146. package/src/theme/assets/osaka-jade.json +93 -0
  147. package/src/theme/assets/palenight.json +222 -0
  148. package/src/theme/assets/rosepine.json +234 -0
  149. package/src/theme/assets/solarized.json +223 -0
  150. package/src/theme/assets/synthwave84.json +226 -0
  151. package/src/theme/assets/tokyonight.json +243 -0
  152. package/src/theme/assets/vercel.json +245 -0
  153. package/src/theme/assets/vesper.json +218 -0
  154. package/src/theme/assets/zenburn.json +223 -0
  155. package/src/theme/index.ts +1089 -0
  156. package/src/ui/border.ts +21 -0
  157. package/src/ui/dialog-alert.tsx +66 -0
  158. package/src/ui/dialog-confirm.tsx +108 -0
  159. package/src/ui/dialog-export-options.tsx +217 -0
  160. package/src/ui/dialog-help.tsx +40 -0
  161. package/src/ui/dialog-prompt.tsx +126 -0
  162. package/src/ui/dialog-select.tsx +790 -0
  163. package/src/ui/dialog.tsx +231 -0
  164. package/src/ui/link.tsx +34 -0
  165. package/src/ui/spinner.ts +368 -0
  166. package/src/ui/toast.tsx +102 -0
  167. package/src/util/collapse-tool-output.ts +19 -0
  168. package/src/util/error.ts +182 -0
  169. package/src/util/filetype.ts +130 -0
  170. package/src/util/format.ts +20 -0
  171. package/src/util/layout.ts +25 -0
  172. package/src/util/locale.ts +86 -0
  173. package/src/util/model.ts +28 -0
  174. package/src/util/path.ts +12 -0
  175. package/src/util/persistence.ts +33 -0
  176. package/src/util/presentation.ts +38 -0
  177. package/src/util/provider-origin.ts +7 -0
  178. package/src/util/record.ts +3 -0
  179. package/src/util/renderer.ts +7 -0
  180. package/src/util/revert-diff.ts +18 -0
  181. package/src/util/scroll.ts +27 -0
  182. package/src/util/selection.ts +79 -0
  183. package/src/util/session.ts +3 -0
  184. package/src/util/signal.ts +51 -0
  185. package/src/util/system.ts +20 -0
  186. package/src/util/tool-display.ts +13 -0
  187. package/src/util/transcript.ts +112 -0
  188. package/sst-env.d.ts +10 -0
  189. package/test/app-lifecycle.test.tsx +128 -0
  190. package/test/cli/cmd/tui/dialog-workspace-create.test.ts +28 -0
  191. package/test/cli/cmd/tui/model-options.test.ts +32 -0
  192. package/test/cli/cmd/tui/notifications.test.ts +267 -0
  193. package/test/cli/cmd/tui/provider-options.test.ts +41 -0
  194. package/test/cli/cmd/tui/sync-fixture.tsx +70 -0
  195. package/test/cli/cmd/tui/sync-live-hydration.test.tsx +262 -0
  196. package/test/cli/cmd/tui/sync-undefined-messages.test.tsx +43 -0
  197. package/test/cli/cmd/tui/sync.test.tsx +65 -0
  198. package/test/cli/tui/__snapshots__/inline-tool-wrap-snapshot.test.tsx.snap +92 -0
  199. package/test/cli/tui/data.test.tsx +486 -0
  200. package/test/cli/tui/dialog-prompt.test.tsx +147 -0
  201. package/test/cli/tui/diff-viewer-file-tree.test.tsx +200 -0
  202. package/test/cli/tui/diff-viewer.test.tsx +268 -0
  203. package/test/cli/tui/inline-tool-wrap-snapshot.test.tsx +351 -0
  204. package/test/cli/tui/prompt-submit-race.test.ts +98 -0
  205. package/test/cli/tui/thinking.test.ts +36 -0
  206. package/test/cli/tui/use-event.test.tsx +148 -0
  207. package/test/clipboard.test.ts +19 -0
  208. package/test/component/dialog-session-list.test.ts +46 -0
  209. package/test/config.test.tsx +127 -0
  210. package/test/context/local.test.ts +22 -0
  211. package/test/editor.test.ts +32 -0
  212. package/test/feature-plugins/diff-viewer-file-tree-utils.test.ts +323 -0
  213. package/test/fixture/fixture.ts +13 -0
  214. package/test/fixture/tui-environment.tsx +32 -0
  215. package/test/fixture/tui-plugin.ts +36 -0
  216. package/test/fixture/tui-runtime.ts +12 -0
  217. package/test/fixture/tui-sdk.ts +109 -0
  218. package/test/index.test.tsx +6 -0
  219. package/test/keymap.test.tsx +141 -0
  220. package/test/plugin/runtime.test.ts +50 -0
  221. package/test/plugin/slots.test.tsx +38 -0
  222. package/test/prompt/display.test.ts +33 -0
  223. package/test/prompt/history.test.ts +39 -0
  224. package/test/prompt/jsonl.test.ts +24 -0
  225. package/test/prompt/local-attachment.test.ts +43 -0
  226. package/test/prompt/part.test.ts +53 -0
  227. package/test/prompt/persistence.test.ts +23 -0
  228. package/test/prompt/traits.test.ts +25 -0
  229. package/test/runtime.test.tsx +37 -0
  230. package/test/theme.test.ts +81 -0
  231. package/test/util/error.test.ts +49 -0
  232. package/test/util/filetype.test.ts +16 -0
  233. package/test/util/format.test.ts +59 -0
  234. package/test/util/model.test.ts +9 -0
  235. package/test/util/presentation.test.ts +8 -0
  236. package/test/util/renderer.test.ts +30 -0
  237. package/test/util/revert-diff.test.ts +35 -0
  238. package/test/util/session.test.ts +10 -0
  239. package/test/util/tool-display.test.ts +40 -0
  240. package/test/util/transcript.test.ts +421 -0
  241. package/tsconfig.json +10 -0
@@ -0,0 +1,353 @@
1
+ import { useTerminalDimensions } from "@opentui/solid"
2
+ import { TextAttributes } from "@opentui/core"
3
+ import { createMemo, createResource, createSignal, onMount, Show } from "solid-js"
4
+ import path from "path"
5
+ import { DialogSelect, type DialogSelectOption } from "../ui/dialog-select"
6
+ import { useDialog } from "../ui/dialog"
7
+ import { useSDK } from "../context/sdk"
8
+ import { useTheme } from "../context/theme"
9
+ import { useSync } from "../context/sync"
10
+ import { abbreviateHome } from "../runtime"
11
+ import { useTuiPaths } from "../context/runtime"
12
+ import { Locale } from "../util/locale"
13
+ import { errorMessage } from "../util/error"
14
+ import { useToast } from "../ui/toast"
15
+ import { useCommandShortcut } from "../keymap"
16
+ import { useProject } from "../context/project"
17
+ import { Spinner } from "./spinner"
18
+ import { DialogWorkspaceFileChanges } from "./dialog-workspace-file-changes"
19
+ import type { ProjectDirectories } from "@neurocode-ai/sdk/v2"
20
+ import { useRoute } from "../context/route"
21
+
22
+ export type MoveSessionSelection = { type: "directory"; directory: string; subdirectory: boolean } | { type: "new" }
23
+ type ProjectDirectory = ProjectDirectories[number]
24
+
25
+ type DialogMoveSessionProps = {
26
+ projectID: string
27
+ current?: MoveSessionSelection
28
+ onSelect: (selection: MoveSessionSelection) => void
29
+ onCurrentChange?: (selection: MoveSessionSelection) => void
30
+ initialDirectories?: ProjectDirectory[]
31
+ initialRemoving?: string
32
+ }
33
+
34
+ export function DialogMoveSession(props: DialogMoveSessionProps) {
35
+ const dialog = useDialog()
36
+ const sdk = useSDK()
37
+ const dimensions = useTerminalDimensions()
38
+ const { theme } = useTheme()
39
+ const sync = useSync()
40
+ const projectContext = useProject()
41
+ const route = useRoute()
42
+ const toast = useToast()
43
+ const paths = useTuiPaths()
44
+ const [working, setWorking] = createSignal(Boolean(props.initialRemoving))
45
+ const [toDelete, setToDelete] = createSignal<string>()
46
+ const [removing, setRemoving] = createSignal(props.initialRemoving)
47
+ const [replacementCurrent, setReplacementCurrent] = createSignal<string>()
48
+ const [loadError, setLoadError] = createSignal<unknown>()
49
+ const deleteHint = useCommandShortcut("dialog.move_session.delete")
50
+ onMount(() => dialog.setSize("xlarge"))
51
+
52
+ function reopen(initialRemoving?: string) {
53
+ dialog.replace(() => (
54
+ <DialogMoveSession {...props} initialDirectories={directoryData()} initialRemoving={initialRemoving} />
55
+ ))
56
+ }
57
+
58
+ // A failed current-checkout lookup only affects which row is highlighted, so
59
+ // swallow it and let the directory list render without a current marker.
60
+ const [loadedProject] = createResource(
61
+ () => (projectContext.project() === props.projectID ? undefined : props.projectID),
62
+ (projectID) =>
63
+ sdk.client.project
64
+ .current({}, { throwOnError: true })
65
+ .then((result) => (result.data?.id === projectID ? result.data.worktree : undefined))
66
+ .catch(() => undefined),
67
+ )
68
+ const currentCheckout = createMemo(() => {
69
+ if (projectContext.project() === props.projectID) return projectContext.instance.path().worktree
70
+ return loadedProject()
71
+ })
72
+
73
+ const [directories, { refetch }] = createResource(
74
+ () => (props.initialRemoving ? undefined : props.projectID),
75
+ async (projectID, info): Promise<ProjectDirectory[] | undefined> => {
76
+ try {
77
+ await sdk.client.v2.projectCopy.refresh(
78
+ { projectID, location: { directory: sdk.directory } },
79
+ { throwOnError: true },
80
+ )
81
+ const directories = await sdk.client.project.directories({ projectID }, { throwOnError: true })
82
+ setLoadError(undefined)
83
+ return directories.data ?? []
84
+ } catch (error) {
85
+ setLoadError(error)
86
+ // An initial load with no data surfaces the inline error view below. A
87
+ // failed refresh intentionally stays quiet and keeps the already-shown
88
+ // list interactive; reopening the dialog retries the load.
89
+ return info.value
90
+ }
91
+ },
92
+ )
93
+ const directoryData = createMemo(() => directories() ?? props.initialDirectories)
94
+ // Show the locked error view only when we have nothing to display. A refresh
95
+ // that fails after the list rendered keeps the list and its actions.
96
+ const showError = createMemo(() => Boolean(loadError()) && !directoryData())
97
+
98
+ const currentDirectory = createMemo(
99
+ () => replacementCurrent() ?? (props.current?.type === "directory" ? props.current.directory : currentCheckout()),
100
+ )
101
+ const currentRoot = createMemo<ProjectDirectory | undefined>(() => {
102
+ if (showError()) return
103
+ const directory = currentDirectory()
104
+ if (!directory) return
105
+ return (
106
+ directoryData()
107
+ ?.filter((root) => contains(root.directory, directory))
108
+ .toSorted((a, b) => b.directory.length - a.directory.length)[0] ?? { directory }
109
+ )
110
+ })
111
+
112
+ const options = createMemo<DialogSelectOption<MoveSessionSelection | undefined>[]>(() => {
113
+ if (showError()) return []
114
+ const data = directoryData()
115
+ const current = currentRoot()?.directory
116
+ if (directories.loading && !data && !current) return [{ title: "Loading project directories...", value: undefined }]
117
+ const roots = [...(data ?? [])]
118
+ if (current && !roots.some((item) => item.directory === current)) roots.unshift({ directory: current })
119
+ roots.sort((a, b) => {
120
+ if (a.directory === current) return -1
121
+ if (b.directory === current) return 1
122
+ if (Boolean(a.strategy) !== Boolean(b.strategy)) return a.strategy ? 1 : -1
123
+ if (!a.strategy && !b.strategy) return a.directory.length - b.directory.length
124
+ return 0
125
+ })
126
+ if (roots.length === 0) return [{ title: "No project directories found", value: undefined }]
127
+
128
+ const subdirectories = sync.data.session
129
+ .filter((session) => session.projectID === props.projectID && session.path && ![".", "/"].includes(session.path))
130
+ .map((session) => session.directory)
131
+ .filter((directory) => !roots.some((root) => root.directory === directory))
132
+ .filter((directory, index, directories) => directories.indexOf(directory) === index)
133
+ .map((location) => ({
134
+ location,
135
+ root: roots
136
+ .filter((root) => {
137
+ const relative = path.relative(root.directory, location)
138
+ return relative && relative !== ".." && !relative.startsWith(".." + path.sep) && !path.isAbsolute(relative)
139
+ })
140
+ .toSorted((a, b) => b.directory.length - a.directory.length)[0],
141
+ }))
142
+ .filter((item): item is { location: string; root: ProjectDirectory } => item.root !== undefined)
143
+
144
+ const list = [...roots.map((root) => ({ location: root.directory, root })), ...subdirectories].toSorted((a, b) => {
145
+ const root = roots.indexOf(a.root) - roots.indexOf(b.root)
146
+ if (root !== 0) return root
147
+ if (a.location === a.root.directory) return -1
148
+ if (b.location === b.root.directory) return 1
149
+ return a.location.localeCompare(b.location)
150
+ })
151
+ const titleWidth = Math.max(1, Math.min(116, dimensions().width - 2) - 12)
152
+
153
+ return list.map((item) => {
154
+ const title = abbreviateHome(item.location, paths.home)
155
+ const suffix =
156
+ item.location === item.root.directory ? undefined : path.sep + path.relative(item.root.directory, item.location)
157
+ const visible = Locale.truncateLeft(title, titleWidth)
158
+ const split = suffix ? Math.max(0, visible.length - suffix.length) : visible.length
159
+ const deleting = toDelete() === item.location
160
+ const isRemoving = removing() === item.location
161
+ return {
162
+ title,
163
+ titleView: isRemoving ? (
164
+ <span style={{ fg: theme.error }}>Deleting {item.location}</span>
165
+ ) : deleting ? (
166
+ <span style={{ fg: theme.text }}>Press {deleteHint()} again to confirm</span>
167
+ ) : suffix ? (
168
+ <>
169
+ {visible.slice(0, split)}
170
+ <span style={{ fg: theme.textMuted }}>{visible.slice(split)}</span>
171
+ </>
172
+ ) : undefined,
173
+ bg: deleting ? theme.error : undefined,
174
+ value: {
175
+ type: "directory",
176
+ directory: item.location,
177
+ subdirectory: item.location !== item.root.directory,
178
+ } as const,
179
+ category: item.root.directory === current ? "Current" : "Other",
180
+ titleWidth,
181
+ truncateTitle: "left" as const,
182
+ }
183
+ })
184
+ })
185
+
186
+ const current = createMemo(() => {
187
+ if (directories.loading || loadedProject.loading) return
188
+ const replacement = replacementCurrent()
189
+ if (replacement) return { type: "directory", directory: replacement, subdirectory: false } as const
190
+ return props.current
191
+ })
192
+
193
+ async function removedCurrent(current: boolean) {
194
+ if (!current) return false
195
+ const fallback = projectContext.data.project.mainDir
196
+ if (fallback) setReplacementCurrent(fallback)
197
+ if (route.data.type === "session") {
198
+ route.navigate({ type: "home" })
199
+ dialog.clear()
200
+ return true
201
+ }
202
+ if (fallback) {
203
+ props.onCurrentChange?.({ type: "directory", directory: fallback, subdirectory: false })
204
+ return true
205
+ }
206
+ dialog.clear()
207
+ return true
208
+ }
209
+
210
+ async function remove(option: DialogSelectOption<MoveSessionSelection | undefined>) {
211
+ if (!option.value || option.value.type !== "directory" || option.value.subdirectory || removing()) return
212
+ const data = directoryData()
213
+ const selected = option.value
214
+ const root = data?.find((item) => item.directory === selected.directory)
215
+ if (!root?.strategy) return
216
+ const deletingCurrent = selected.directory === currentRoot()?.directory
217
+ if (toDelete() !== selected.directory) {
218
+ setToDelete(selected.directory)
219
+ return
220
+ }
221
+ setToDelete(undefined)
222
+ setRemoving(selected.directory)
223
+ setWorking(true)
224
+ const result = await sdk.client.v2.projectCopy
225
+ .remove({
226
+ projectID: props.projectID,
227
+ location: { directory: sdk.directory },
228
+ directory: selected.directory,
229
+ force: false,
230
+ })
231
+ .catch((error) => ({ error }))
232
+ if (result.error) {
233
+ setRemoving(undefined)
234
+ setWorking(false)
235
+ if ("data" in result.error && result.error.data.forceRequired) {
236
+ const status = await sdk.client.vcs.status({ directory: selected.directory }).catch(() => undefined)
237
+ const choice = await DialogWorkspaceFileChanges.show(dialog, status?.data ?? [], {
238
+ title: "Delete working copy?",
239
+ message: "This working copy has file changes. Do you want to delete it anyway?",
240
+ })
241
+ if (choice !== "yes") {
242
+ reopen()
243
+ return
244
+ }
245
+ reopen(selected.directory)
246
+ const forced = await sdk.client.v2.projectCopy
247
+ .remove({
248
+ projectID: props.projectID,
249
+ location: { directory: sdk.directory },
250
+ directory: selected.directory,
251
+ force: true,
252
+ })
253
+ .catch((error) => ({ error }))
254
+ if (forced.error) {
255
+ toast.show({
256
+ variant: "error",
257
+ title: "Failed to delete project copy",
258
+ message: errorMessage(forced.error),
259
+ })
260
+ reopen()
261
+ return
262
+ }
263
+ setRemoving(undefined)
264
+ setWorking(false)
265
+ if (await removedCurrent(deletingCurrent)) return
266
+ reopen()
267
+ return
268
+ }
269
+ toast.show({
270
+ variant: "error",
271
+ title: "Failed to delete project copy",
272
+ message: errorMessage(result.error),
273
+ })
274
+ return
275
+ }
276
+ await refetch()
277
+ setRemoving(undefined)
278
+ setWorking(false)
279
+ if (await removedCurrent(deletingCurrent)) return
280
+ }
281
+
282
+ const fullHeight = createMemo(() =>
283
+ Math.max(8, Math.min(16, dimensions().height - Math.floor(dimensions().height / 4) - 2)),
284
+ )
285
+
286
+ return (
287
+ <box minHeight={showError() ? 5 : fullHeight()}>
288
+ <DialogSelect
289
+ title="Move session"
290
+ titleView={
291
+ <box flexDirection="row" gap={1}>
292
+ <text fg={theme.text} attributes={TextAttributes.BOLD}>
293
+ Move session
294
+ </text>
295
+ <Show when={working() || directories.loading || loadedProject.loading}>
296
+ <Spinner />
297
+ </Show>
298
+ </box>
299
+ }
300
+ renderFilter={!showError()}
301
+ options={options()}
302
+ emptyView={
303
+ showError() ? (
304
+ <box paddingLeft={4} paddingRight={4}>
305
+ <text fg={theme.error} attributes={TextAttributes.BOLD}>
306
+ Could not load project directories
307
+ </text>
308
+ <text fg={theme.textMuted}>{errorMessage(loadError())}</text>
309
+ </box>
310
+ ) : undefined
311
+ }
312
+ locked={showError() || directories.loading || loadedProject.loading || Boolean(removing())}
313
+ current={current()}
314
+ onSelect={(option) => {
315
+ if (option.value) props.onSelect(option.value)
316
+ }}
317
+ onMove={() => setToDelete(undefined)}
318
+ actions={
319
+ showError()
320
+ ? []
321
+ : [
322
+ {
323
+ command: "dialog.move_session.new",
324
+ title: "new",
325
+ onTrigger: () => props.onSelect({ type: "new" }),
326
+ },
327
+ {
328
+ command: "dialog.move_session.delete",
329
+ title: "delete",
330
+ disabled: (option) => {
331
+ const value = option?.value
332
+ if (!value || value.type !== "directory" || value.subdirectory) return true
333
+ return !directoryData()?.find((item) => item.directory === value.directory)?.strategy
334
+ },
335
+ onTrigger: remove,
336
+ },
337
+ {
338
+ command: "dialog.move_session.refresh",
339
+ title: "refresh",
340
+ onTrigger: () => void refetch(),
341
+ },
342
+ ]
343
+ }
344
+ />
345
+ </box>
346
+ )
347
+ }
348
+
349
+ function contains(root: string, directory: string) {
350
+ if (root === directory) return true
351
+ const relative = path.relative(root, directory)
352
+ return relative && relative !== ".." && !relative.startsWith(".." + path.sep) && !path.isAbsolute(relative)
353
+ }