reigncode-app 1.3.2

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 (300) hide show
  1. package/AGENTS.md +30 -0
  2. package/Dockerfile +21 -0
  3. package/README.md +51 -0
  4. package/bunfig.toml +3 -0
  5. package/create-effect-simplification-spec.md +515 -0
  6. package/e2e/AGENTS.md +226 -0
  7. package/e2e/actions.ts +1018 -0
  8. package/e2e/app/home.spec.ts +24 -0
  9. package/e2e/app/navigation.spec.ts +10 -0
  10. package/e2e/app/palette.spec.ts +20 -0
  11. package/e2e/app/server-default.spec.ts +58 -0
  12. package/e2e/app/session.spec.ts +16 -0
  13. package/e2e/app/titlebar-history.spec.ts +120 -0
  14. package/e2e/commands/input-focus.spec.ts +15 -0
  15. package/e2e/commands/panels.spec.ts +33 -0
  16. package/e2e/commands/tab-close.spec.ts +32 -0
  17. package/e2e/files/file-open.spec.ts +31 -0
  18. package/e2e/files/file-tree.spec.ts +56 -0
  19. package/e2e/files/file-viewer.spec.ts +156 -0
  20. package/e2e/fixtures.ts +154 -0
  21. package/e2e/models/model-picker.spec.ts +48 -0
  22. package/e2e/models/models-visibility.spec.ts +61 -0
  23. package/e2e/projects/project-edit.spec.ts +43 -0
  24. package/e2e/projects/projects-close.spec.ts +54 -0
  25. package/e2e/projects/projects-switch.spec.ts +116 -0
  26. package/e2e/projects/workspace-new-session.spec.ts +94 -0
  27. package/e2e/projects/workspaces.spec.ts +375 -0
  28. package/e2e/prompt/context.spec.ts +95 -0
  29. package/e2e/prompt/prompt-async.spec.ts +76 -0
  30. package/e2e/prompt/prompt-drop-file-uri.spec.ts +22 -0
  31. package/e2e/prompt/prompt-drop-file.spec.ts +30 -0
  32. package/e2e/prompt/prompt-history.spec.ts +184 -0
  33. package/e2e/prompt/prompt-mention.spec.ts +26 -0
  34. package/e2e/prompt/prompt-multiline.spec.ts +24 -0
  35. package/e2e/prompt/prompt-shell.spec.ts +62 -0
  36. package/e2e/prompt/prompt-slash-open.spec.ts +22 -0
  37. package/e2e/prompt/prompt-slash-share.spec.ts +64 -0
  38. package/e2e/prompt/prompt-slash-terminal.spec.ts +18 -0
  39. package/e2e/prompt/prompt.spec.ts +55 -0
  40. package/e2e/selectors.ts +75 -0
  41. package/e2e/session/session-child-navigation.spec.ts +37 -0
  42. package/e2e/session/session-composer-dock.spec.ts +530 -0
  43. package/e2e/session/session-model-persistence.spec.ts +359 -0
  44. package/e2e/session/session-review.spec.ts +426 -0
  45. package/e2e/session/session-undo-redo.spec.ts +233 -0
  46. package/e2e/session/session.spec.ts +174 -0
  47. package/e2e/settings/settings-keybinds.spec.ts +389 -0
  48. package/e2e/settings/settings-models.spec.ts +122 -0
  49. package/e2e/settings/settings-providers.spec.ts +136 -0
  50. package/e2e/settings/settings.spec.ts +519 -0
  51. package/e2e/sidebar/sidebar-popover-actions.spec.ts +118 -0
  52. package/e2e/sidebar/sidebar-session-links.spec.ts +30 -0
  53. package/e2e/sidebar/sidebar.spec.ts +40 -0
  54. package/e2e/status/status-popover.spec.ts +94 -0
  55. package/e2e/terminal/terminal-init.spec.ts +28 -0
  56. package/e2e/terminal/terminal-reconnect.spec.ts +46 -0
  57. package/e2e/terminal/terminal-tabs.spec.ts +168 -0
  58. package/e2e/terminal/terminal.spec.ts +18 -0
  59. package/e2e/thinking-level.spec.ts +25 -0
  60. package/e2e/tsconfig.json +9 -0
  61. package/e2e/utils.ts +63 -0
  62. package/happydom.ts +75 -0
  63. package/index.html +23 -0
  64. package/package.json +77 -0
  65. package/playwright.config.ts +45 -0
  66. package/public/_headers +17 -0
  67. package/public/oc-theme-preload.js +35 -0
  68. package/script/e2e-local.ts +180 -0
  69. package/src/addons/serialize.test.ts +319 -0
  70. package/src/addons/serialize.ts +634 -0
  71. package/src/app.tsx +308 -0
  72. package/src/components/debug-bar.tsx +443 -0
  73. package/src/components/dialog-connect-provider.tsx +617 -0
  74. package/src/components/dialog-custom-provider-form.ts +158 -0
  75. package/src/components/dialog-custom-provider.test.ts +80 -0
  76. package/src/components/dialog-custom-provider.tsx +329 -0
  77. package/src/components/dialog-edit-project.tsx +255 -0
  78. package/src/components/dialog-fork.tsx +108 -0
  79. package/src/components/dialog-manage-models.tsx +101 -0
  80. package/src/components/dialog-release-notes.tsx +144 -0
  81. package/src/components/dialog-select-directory.tsx +392 -0
  82. package/src/components/dialog-select-file.tsx +466 -0
  83. package/src/components/dialog-select-mcp.tsx +107 -0
  84. package/src/components/dialog-select-model-unpaid.tsx +137 -0
  85. package/src/components/dialog-select-model.tsx +220 -0
  86. package/src/components/dialog-select-provider.tsx +86 -0
  87. package/src/components/dialog-select-server.tsx +649 -0
  88. package/src/components/dialog-settings.tsx +73 -0
  89. package/src/components/file-tree.test.ts +78 -0
  90. package/src/components/file-tree.tsx +507 -0
  91. package/src/components/link.tsx +26 -0
  92. package/src/components/model-tooltip.tsx +91 -0
  93. package/src/components/prompt-input/attachments.test.ts +44 -0
  94. package/src/components/prompt-input/attachments.ts +201 -0
  95. package/src/components/prompt-input/build-request-parts.test.ts +312 -0
  96. package/src/components/prompt-input/build-request-parts.ts +175 -0
  97. package/src/components/prompt-input/context-items.tsx +88 -0
  98. package/src/components/prompt-input/drag-overlay.tsx +25 -0
  99. package/src/components/prompt-input/editor-dom.test.ts +99 -0
  100. package/src/components/prompt-input/editor-dom.ts +148 -0
  101. package/src/components/prompt-input/files.ts +66 -0
  102. package/src/components/prompt-input/history.test.ts +153 -0
  103. package/src/components/prompt-input/history.ts +256 -0
  104. package/src/components/prompt-input/image-attachments.tsx +58 -0
  105. package/src/components/prompt-input/paste.ts +24 -0
  106. package/src/components/prompt-input/placeholder.test.ts +48 -0
  107. package/src/components/prompt-input/placeholder.ts +15 -0
  108. package/src/components/prompt-input/slash-popover.tsx +141 -0
  109. package/src/components/prompt-input/submit.test.ts +346 -0
  110. package/src/components/prompt-input/submit.ts +579 -0
  111. package/src/components/prompt-input.tsx +1595 -0
  112. package/src/components/server/server-row.tsx +130 -0
  113. package/src/components/session/index.ts +5 -0
  114. package/src/components/session/session-context-breakdown.test.ts +61 -0
  115. package/src/components/session/session-context-breakdown.ts +132 -0
  116. package/src/components/session/session-context-format.ts +20 -0
  117. package/src/components/session/session-context-metrics.test.ts +101 -0
  118. package/src/components/session/session-context-metrics.ts +82 -0
  119. package/src/components/session/session-context-tab.tsx +339 -0
  120. package/src/components/session/session-header.tsx +486 -0
  121. package/src/components/session/session-new-view.tsx +91 -0
  122. package/src/components/session/session-sortable-tab.tsx +70 -0
  123. package/src/components/session/session-sortable-terminal-tab.tsx +193 -0
  124. package/src/components/session-context-usage.tsx +122 -0
  125. package/src/components/settings-general.tsx +585 -0
  126. package/src/components/settings-keybinds.tsx +453 -0
  127. package/src/components/settings-list.tsx +5 -0
  128. package/src/components/settings-models.tsx +137 -0
  129. package/src/components/settings-providers.tsx +251 -0
  130. package/src/components/status-popover.tsx +419 -0
  131. package/src/components/terminal.tsx +653 -0
  132. package/src/components/titlebar-history.test.ts +63 -0
  133. package/src/components/titlebar-history.ts +57 -0
  134. package/src/components/titlebar.tsx +312 -0
  135. package/src/constants/file-picker.ts +89 -0
  136. package/src/context/command-keybind.test.ts +69 -0
  137. package/src/context/command.test.ts +25 -0
  138. package/src/context/command.tsx +437 -0
  139. package/src/context/comments.test.ts +186 -0
  140. package/src/context/comments.tsx +243 -0
  141. package/src/context/file/content-cache.ts +88 -0
  142. package/src/context/file/path.test.ts +360 -0
  143. package/src/context/file/path.ts +151 -0
  144. package/src/context/file/tree-store.ts +170 -0
  145. package/src/context/file/types.ts +41 -0
  146. package/src/context/file/view-cache.ts +146 -0
  147. package/src/context/file/watcher.test.ts +149 -0
  148. package/src/context/file/watcher.ts +53 -0
  149. package/src/context/file-content-eviction-accounting.test.ts +65 -0
  150. package/src/context/file.tsx +280 -0
  151. package/src/context/global-sdk.tsx +232 -0
  152. package/src/context/global-sync/bootstrap.ts +206 -0
  153. package/src/context/global-sync/child-store.test.ts +38 -0
  154. package/src/context/global-sync/child-store.ts +281 -0
  155. package/src/context/global-sync/event-reducer.test.ts +552 -0
  156. package/src/context/global-sync/event-reducer.ts +359 -0
  157. package/src/context/global-sync/eviction.ts +28 -0
  158. package/src/context/global-sync/queue.ts +83 -0
  159. package/src/context/global-sync/session-cache.test.ts +102 -0
  160. package/src/context/global-sync/session-cache.ts +62 -0
  161. package/src/context/global-sync/session-load.ts +25 -0
  162. package/src/context/global-sync/session-prefetch.test.ts +96 -0
  163. package/src/context/global-sync/session-prefetch.ts +100 -0
  164. package/src/context/global-sync/session-trim.test.ts +59 -0
  165. package/src/context/global-sync/session-trim.ts +56 -0
  166. package/src/context/global-sync/types.ts +133 -0
  167. package/src/context/global-sync/utils.ts +25 -0
  168. package/src/context/global-sync.test.ts +122 -0
  169. package/src/context/global-sync.tsx +408 -0
  170. package/src/context/highlights.tsx +233 -0
  171. package/src/context/language.tsx +248 -0
  172. package/src/context/layout-scroll.test.ts +64 -0
  173. package/src/context/layout-scroll.ts +126 -0
  174. package/src/context/layout.test.ts +69 -0
  175. package/src/context/layout.tsx +937 -0
  176. package/src/context/local.tsx +422 -0
  177. package/src/context/model-variant.test.ts +86 -0
  178. package/src/context/model-variant.ts +52 -0
  179. package/src/context/models.tsx +163 -0
  180. package/src/context/notification.tsx +373 -0
  181. package/src/context/permission-auto-respond.test.ts +102 -0
  182. package/src/context/permission-auto-respond.ts +51 -0
  183. package/src/context/permission.tsx +277 -0
  184. package/src/context/platform.tsx +99 -0
  185. package/src/context/prompt.tsx +297 -0
  186. package/src/context/sdk.tsx +49 -0
  187. package/src/context/server.tsx +295 -0
  188. package/src/context/settings.tsx +241 -0
  189. package/src/context/sync-optimistic.test.ts +123 -0
  190. package/src/context/sync.tsx +618 -0
  191. package/src/context/terminal-title.ts +51 -0
  192. package/src/context/terminal.test.ts +82 -0
  193. package/src/context/terminal.tsx +437 -0
  194. package/src/entry.tsx +144 -0
  195. package/src/env.d.ts +18 -0
  196. package/src/hooks/use-providers.ts +44 -0
  197. package/src/i18n/ar.ts +855 -0
  198. package/src/i18n/br.ts +867 -0
  199. package/src/i18n/bs.ts +943 -0
  200. package/src/i18n/da.ts +937 -0
  201. package/src/i18n/de.ts +879 -0
  202. package/src/i18n/en.ts +948 -0
  203. package/src/i18n/es.ts +950 -0
  204. package/src/i18n/fr.ts +878 -0
  205. package/src/i18n/ja.ts +861 -0
  206. package/src/i18n/ko.ts +860 -0
  207. package/src/i18n/no.ts +944 -0
  208. package/src/i18n/parity.test.ts +32 -0
  209. package/src/i18n/pl.ts +865 -0
  210. package/src/i18n/ru.ts +946 -0
  211. package/src/i18n/th.ts +933 -0
  212. package/src/i18n/tr.ts +952 -0
  213. package/src/i18n/zh.ts +930 -0
  214. package/src/i18n/zht.ts +925 -0
  215. package/src/index.css +29 -0
  216. package/src/index.ts +6 -0
  217. package/src/pages/directory-layout.tsx +88 -0
  218. package/src/pages/error.tsx +327 -0
  219. package/src/pages/home.tsx +131 -0
  220. package/src/pages/layout/deep-links.ts +50 -0
  221. package/src/pages/layout/helpers.test.ts +211 -0
  222. package/src/pages/layout/helpers.ts +98 -0
  223. package/src/pages/layout/inline-editor.tsx +126 -0
  224. package/src/pages/layout/sidebar-items.tsx +437 -0
  225. package/src/pages/layout/sidebar-project.tsx +384 -0
  226. package/src/pages/layout/sidebar-shell.tsx +125 -0
  227. package/src/pages/layout/sidebar-workspace.tsx +504 -0
  228. package/src/pages/layout.tsx +2509 -0
  229. package/src/pages/session/composer/index.ts +2 -0
  230. package/src/pages/session/composer/session-composer-region.tsx +255 -0
  231. package/src/pages/session/composer/session-composer-state.test.ts +128 -0
  232. package/src/pages/session/composer/session-composer-state.ts +249 -0
  233. package/src/pages/session/composer/session-followup-dock.tsx +109 -0
  234. package/src/pages/session/composer/session-permission-dock.tsx +74 -0
  235. package/src/pages/session/composer/session-question-dock.tsx +449 -0
  236. package/src/pages/session/composer/session-request-tree.ts +52 -0
  237. package/src/pages/session/composer/session-revert-dock.tsx +99 -0
  238. package/src/pages/session/composer/session-todo-dock.tsx +330 -0
  239. package/src/pages/session/file-tab-scroll.test.ts +40 -0
  240. package/src/pages/session/file-tab-scroll.ts +67 -0
  241. package/src/pages/session/file-tabs.tsx +456 -0
  242. package/src/pages/session/handoff.ts +36 -0
  243. package/src/pages/session/helpers.test.ts +181 -0
  244. package/src/pages/session/helpers.ts +198 -0
  245. package/src/pages/session/message-gesture.test.ts +62 -0
  246. package/src/pages/session/message-gesture.ts +21 -0
  247. package/src/pages/session/message-id-from-hash.ts +6 -0
  248. package/src/pages/session/message-timeline.tsx +1013 -0
  249. package/src/pages/session/review-tab.tsx +170 -0
  250. package/src/pages/session/session-layout.ts +20 -0
  251. package/src/pages/session/session-model-helpers.test.ts +51 -0
  252. package/src/pages/session/session-model-helpers.ts +16 -0
  253. package/src/pages/session/session-side-panel.tsx +453 -0
  254. package/src/pages/session/terminal-label.ts +16 -0
  255. package/src/pages/session/terminal-panel.test.ts +25 -0
  256. package/src/pages/session/terminal-panel.tsx +326 -0
  257. package/src/pages/session/use-session-commands.tsx +495 -0
  258. package/src/pages/session/use-session-hash-scroll.test.ts +16 -0
  259. package/src/pages/session/use-session-hash-scroll.ts +197 -0
  260. package/src/pages/session.tsx +1841 -0
  261. package/src/sst-env.d.ts +12 -0
  262. package/src/testing/model-selection.ts +80 -0
  263. package/src/testing/prompt.ts +56 -0
  264. package/src/testing/session-composer.ts +84 -0
  265. package/src/testing/terminal.ts +118 -0
  266. package/src/theme-preload.test.ts +46 -0
  267. package/src/utils/agent.ts +23 -0
  268. package/src/utils/aim.ts +138 -0
  269. package/src/utils/base64.ts +10 -0
  270. package/src/utils/comment-note.ts +88 -0
  271. package/src/utils/id.ts +99 -0
  272. package/src/utils/notification-click.test.ts +27 -0
  273. package/src/utils/notification-click.ts +13 -0
  274. package/src/utils/persist.test.ts +115 -0
  275. package/src/utils/persist.ts +476 -0
  276. package/src/utils/prompt.test.ts +44 -0
  277. package/src/utils/prompt.ts +203 -0
  278. package/src/utils/runtime-adapters.test.ts +62 -0
  279. package/src/utils/runtime-adapters.ts +39 -0
  280. package/src/utils/same.ts +6 -0
  281. package/src/utils/scoped-cache.test.ts +69 -0
  282. package/src/utils/scoped-cache.ts +104 -0
  283. package/src/utils/server-errors.test.ts +131 -0
  284. package/src/utils/server-errors.ts +80 -0
  285. package/src/utils/server-health.test.ts +123 -0
  286. package/src/utils/server-health.ts +91 -0
  287. package/src/utils/server.ts +22 -0
  288. package/src/utils/solid-dnd.tsx +49 -0
  289. package/src/utils/sound.ts +117 -0
  290. package/src/utils/terminal-writer.test.ts +64 -0
  291. package/src/utils/terminal-writer.ts +65 -0
  292. package/src/utils/time.ts +22 -0
  293. package/src/utils/uuid.test.ts +78 -0
  294. package/src/utils/uuid.ts +12 -0
  295. package/src/utils/worktree.test.ts +46 -0
  296. package/src/utils/worktree.ts +73 -0
  297. package/sst-env.d.ts +10 -0
  298. package/tsconfig.json +26 -0
  299. package/vite.config.ts +15 -0
  300. package/vite.js +26 -0
@@ -0,0 +1,392 @@
1
+ import { useDialog } from "@reign-labs/ui/context/dialog"
2
+ import { Dialog } from "@reign-labs/ui/dialog"
3
+ import { FileIcon } from "@reign-labs/ui/file-icon"
4
+ import { List } from "@reign-labs/ui/list"
5
+ import type { ListRef } from "@reign-labs/ui/list"
6
+ import { getDirectory, getFilename } from "@reign-labs/util/path"
7
+ import fuzzysort from "fuzzysort"
8
+ import { createMemo, createResource, createSignal } from "solid-js"
9
+ import { useGlobalSDK } from "@/context/global-sdk"
10
+ import { useGlobalSync } from "@/context/global-sync"
11
+ import { useLayout } from "@/context/layout"
12
+ import { useLanguage } from "@/context/language"
13
+
14
+ interface DialogSelectDirectoryProps {
15
+ title?: string
16
+ multiple?: boolean
17
+ onSelect: (result: string | string[] | null) => void
18
+ }
19
+
20
+ type Row = {
21
+ absolute: string
22
+ search: string
23
+ group: "recent" | "folders"
24
+ }
25
+
26
+ function cleanInput(value: string) {
27
+ const first = (value ?? "").split(/\r?\n/)[0] ?? ""
28
+ return first.replace(/[\u0000-\u001F\u007F]/g, "").trim()
29
+ }
30
+
31
+ function normalizePath(input: string) {
32
+ const v = input.replaceAll("\\", "/")
33
+ if (v.startsWith("//") && !v.startsWith("///")) return "//" + v.slice(2).replace(/\/+/g, "/")
34
+ return v.replace(/\/+/g, "/")
35
+ }
36
+
37
+ function normalizeDriveRoot(input: string) {
38
+ const v = normalizePath(input)
39
+ if (/^[A-Za-z]:$/.test(v)) return v + "/"
40
+ return v
41
+ }
42
+
43
+ function trimTrailing(input: string) {
44
+ const v = normalizeDriveRoot(input)
45
+ if (v === "/") return v
46
+ if (v === "//") return v
47
+ if (/^[A-Za-z]:\/$/.test(v)) return v
48
+ return v.replace(/\/+$/, "")
49
+ }
50
+
51
+ function joinPath(base: string | undefined, rel: string) {
52
+ const b = trimTrailing(base ?? "")
53
+ const r = trimTrailing(rel).replace(/^\/+/, "")
54
+ if (!b) return r
55
+ if (!r) return b
56
+ if (b.endsWith("/")) return b + r
57
+ return b + "/" + r
58
+ }
59
+
60
+ function rootOf(input: string) {
61
+ const v = normalizeDriveRoot(input)
62
+ if (v.startsWith("//")) return "//"
63
+ if (v.startsWith("/")) return "/"
64
+ if (/^[A-Za-z]:\//.test(v)) return v.slice(0, 3)
65
+ return ""
66
+ }
67
+
68
+ function parentOf(input: string) {
69
+ const v = trimTrailing(input)
70
+ if (v === "/") return v
71
+ if (v === "//") return v
72
+ if (/^[A-Za-z]:\/$/.test(v)) return v
73
+
74
+ const i = v.lastIndexOf("/")
75
+ if (i <= 0) return "/"
76
+ if (i === 2 && /^[A-Za-z]:/.test(v)) return v.slice(0, 3)
77
+ return v.slice(0, i)
78
+ }
79
+
80
+ function modeOf(input: string) {
81
+ const raw = normalizeDriveRoot(input.trim())
82
+ if (!raw) return "relative" as const
83
+ if (raw.startsWith("~")) return "tilde" as const
84
+ if (rootOf(raw)) return "absolute" as const
85
+ return "relative" as const
86
+ }
87
+
88
+ function tildeOf(absolute: string, home: string) {
89
+ const full = trimTrailing(absolute)
90
+ if (!home) return ""
91
+
92
+ const hn = trimTrailing(home)
93
+ const lc = full.toLowerCase()
94
+ const hc = hn.toLowerCase()
95
+ if (lc === hc) return "~"
96
+ if (lc.startsWith(hc + "/")) return "~" + full.slice(hn.length)
97
+ return ""
98
+ }
99
+
100
+ function displayPath(path: string, input: string, home: string) {
101
+ const full = trimTrailing(path)
102
+ if (modeOf(input) === "absolute") return full
103
+ return tildeOf(full, home) || full
104
+ }
105
+
106
+ function toRow(absolute: string, home: string, group: Row["group"]): Row {
107
+ const full = trimTrailing(absolute)
108
+ const tilde = tildeOf(full, home)
109
+ const withSlash = (value: string) => {
110
+ if (!value) return ""
111
+ if (value.endsWith("/")) return value
112
+ return value + "/"
113
+ }
114
+
115
+ const search = Array.from(
116
+ new Set([full, withSlash(full), tilde, withSlash(tilde), getFilename(full)].filter(Boolean)),
117
+ ).join("\n")
118
+ return { absolute: full, search, group }
119
+ }
120
+
121
+ function uniqueRows(rows: Row[]) {
122
+ const seen = new Set<string>()
123
+ return rows.filter((row) => {
124
+ if (seen.has(row.absolute)) return false
125
+ seen.add(row.absolute)
126
+ return true
127
+ })
128
+ }
129
+
130
+ function useDirectorySearch(args: {
131
+ sdk: ReturnType<typeof useGlobalSDK>
132
+ start: () => string | undefined
133
+ home: () => string
134
+ }) {
135
+ const cache = new Map<string, Promise<Array<{ name: string; absolute: string }>>>()
136
+ let current = 0
137
+
138
+ const scoped = (value: string) => {
139
+ const base = args.start()
140
+ if (!base) return
141
+
142
+ const raw = normalizeDriveRoot(value)
143
+ if (!raw) return { directory: trimTrailing(base), path: "" }
144
+
145
+ const h = args.home()
146
+ if (raw === "~") return { directory: trimTrailing(h || base), path: "" }
147
+ if (raw.startsWith("~/")) return { directory: trimTrailing(h || base), path: raw.slice(2) }
148
+
149
+ const root = rootOf(raw)
150
+ if (root) return { directory: trimTrailing(root), path: raw.slice(root.length) }
151
+ return { directory: trimTrailing(base), path: raw }
152
+ }
153
+
154
+ const dirs = async (dir: string) => {
155
+ const key = trimTrailing(dir)
156
+ const existing = cache.get(key)
157
+ if (existing) return existing
158
+
159
+ const request = args.sdk.client.file
160
+ .list({ directory: key, path: "" })
161
+ .then((x) => x.data ?? [])
162
+ .catch(() => [])
163
+ .then((nodes) =>
164
+ nodes
165
+ .filter((n) => n.type === "directory")
166
+ .map((n) => ({
167
+ name: n.name,
168
+ absolute: trimTrailing(normalizeDriveRoot(n.absolute)),
169
+ })),
170
+ )
171
+
172
+ cache.set(key, request)
173
+ return request
174
+ }
175
+
176
+ const match = async (dir: string, query: string, limit: number) => {
177
+ const items = await dirs(dir)
178
+ if (!query) return items.slice(0, limit).map((x) => x.absolute)
179
+ return fuzzysort.go(query, items, { key: "name", limit }).map((x) => x.obj.absolute)
180
+ }
181
+
182
+ return async (filter: string) => {
183
+ const token = ++current
184
+ const active = () => token === current
185
+
186
+ const value = cleanInput(filter)
187
+ const scopedInput = scoped(value)
188
+ if (!scopedInput) return [] as string[]
189
+
190
+ const raw = normalizeDriveRoot(value)
191
+ const isPath = raw.startsWith("~") || !!rootOf(raw) || raw.includes("/")
192
+ const query = normalizeDriveRoot(scopedInput.path)
193
+
194
+ const find = () =>
195
+ args.sdk.client.find
196
+ .files({ directory: scopedInput.directory, query, type: "directory", limit: 50 })
197
+ .then((x) => x.data ?? [])
198
+ .catch(() => [])
199
+
200
+ if (!isPath) {
201
+ const results = await find()
202
+ if (!active()) return []
203
+ return results.map((rel) => joinPath(scopedInput.directory, rel)).slice(0, 50)
204
+ }
205
+
206
+ const segments = query.replace(/^\/+/, "").split("/")
207
+ const head = segments.slice(0, segments.length - 1).filter((x) => x && x !== ".")
208
+ const tail = segments[segments.length - 1] ?? ""
209
+
210
+ const cap = 12
211
+ const branch = 4
212
+ let paths = [scopedInput.directory]
213
+ for (const part of head) {
214
+ if (!active()) return []
215
+ if (part === "..") {
216
+ paths = paths.map(parentOf)
217
+ continue
218
+ }
219
+
220
+ const next = (await Promise.all(paths.map((p) => match(p, part, branch)))).flat()
221
+ if (!active()) return []
222
+ paths = Array.from(new Set(next)).slice(0, cap)
223
+ if (paths.length === 0) return [] as string[]
224
+ }
225
+
226
+ const out = (await Promise.all(paths.map((p) => match(p, tail, 50)))).flat()
227
+ if (!active()) return []
228
+ const deduped = Array.from(new Set(out))
229
+ const base = raw.startsWith("~") ? trimTrailing(scopedInput.directory) : ""
230
+ const expand = !raw.endsWith("/")
231
+ if (!expand || !tail) {
232
+ const items = base ? Array.from(new Set([base, ...deduped])) : deduped
233
+ return items.slice(0, 50)
234
+ }
235
+
236
+ const needle = tail.toLowerCase()
237
+ const exact = deduped.filter((p) => getFilename(p).toLowerCase() === needle)
238
+ const target = exact[0]
239
+ if (!target) return deduped.slice(0, 50)
240
+
241
+ const children = await match(target, "", 30)
242
+ if (!active()) return []
243
+ const items = Array.from(new Set([...deduped, ...children]))
244
+ return (base ? Array.from(new Set([base, ...items])) : items).slice(0, 50)
245
+ }
246
+ }
247
+
248
+ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
249
+ const sync = useGlobalSync()
250
+ const sdk = useGlobalSDK()
251
+ const layout = useLayout()
252
+ const dialog = useDialog()
253
+ const language = useLanguage()
254
+
255
+ const [filter, setFilter] = createSignal("")
256
+ let list: ListRef | undefined
257
+
258
+ const missingBase = createMemo(() => !(sync.data.path.home || sync.data.path.directory))
259
+ const [fallbackPath] = createResource(
260
+ () => (missingBase() ? true : undefined),
261
+ async () => {
262
+ return sdk.client.path
263
+ .get()
264
+ .then((x) => x.data)
265
+ .catch(() => undefined)
266
+ },
267
+ { initialValue: undefined },
268
+ )
269
+
270
+ const home = createMemo(() => sync.data.path.home || fallbackPath()?.home || "")
271
+ const start = createMemo(
272
+ () => sync.data.path.home || sync.data.path.directory || fallbackPath()?.home || fallbackPath()?.directory,
273
+ )
274
+
275
+ const directories = useDirectorySearch({
276
+ sdk,
277
+ home,
278
+ start,
279
+ })
280
+
281
+ const recentProjects = createMemo(() => {
282
+ const projects = layout.projects.list()
283
+ const byProject = new Map<string, number>()
284
+
285
+ for (const project of projects) {
286
+ let at = 0
287
+ const dirs = [project.worktree, ...(project.sandboxes ?? [])]
288
+ for (const directory of dirs) {
289
+ const sessions = sync.child(directory, { bootstrap: false })[0].session
290
+ for (const session of sessions) {
291
+ if (session.time.archived) continue
292
+ const updated = session.time.updated ?? session.time.created
293
+ if (updated > at) at = updated
294
+ }
295
+ }
296
+ byProject.set(project.worktree, at)
297
+ }
298
+
299
+ return projects
300
+ .map((project, index) => ({ project, at: byProject.get(project.worktree) ?? 0, index }))
301
+ .sort((a, b) => b.at - a.at || a.index - b.index)
302
+ .slice(0, 5)
303
+ .map(({ project }) => {
304
+ const row = toRow(project.worktree, home(), "recent")
305
+ const name = project.name || getFilename(project.worktree)
306
+ return {
307
+ ...row,
308
+ search: `${row.search}\n${name}`,
309
+ }
310
+ })
311
+ })
312
+
313
+ const items = async (value: string) => {
314
+ const results = await directories(value)
315
+ const directoryRows = results.map((absolute) => toRow(absolute, home(), "folders"))
316
+ return uniqueRows([...recentProjects(), ...directoryRows])
317
+ }
318
+
319
+ function resolve(absolute: string) {
320
+ props.onSelect(props.multiple ? [absolute] : absolute)
321
+ dialog.close()
322
+ }
323
+
324
+ return (
325
+ <Dialog title={props.title ?? language.t("command.project.open")}>
326
+ <List
327
+ search={{ placeholder: language.t("dialog.directory.search.placeholder"), autofocus: true }}
328
+ emptyMessage={language.t("dialog.directory.empty")}
329
+ loadingMessage={language.t("common.loading")}
330
+ items={items}
331
+ key={(x) => x.absolute}
332
+ filterKeys={["search"]}
333
+ groupBy={(item) => item.group}
334
+ sortGroupsBy={(a, b) => {
335
+ if (a.category === b.category) return 0
336
+ return a.category === "recent" ? -1 : 1
337
+ }}
338
+ groupHeader={(group) =>
339
+ group.category === "recent" ? language.t("home.recentProjects") : language.t("command.project.open")
340
+ }
341
+ ref={(r) => (list = r)}
342
+ onFilter={(value) => setFilter(cleanInput(value))}
343
+ onKeyEvent={(e, item) => {
344
+ if (e.key !== "Tab") return
345
+ if (e.shiftKey) return
346
+ if (!item) return
347
+
348
+ e.preventDefault()
349
+ e.stopPropagation()
350
+
351
+ const value = displayPath(item.absolute, filter(), home())
352
+ list?.setFilter(value.endsWith("/") ? value : value + "/")
353
+ }}
354
+ onSelect={(path) => {
355
+ if (!path) return
356
+ resolve(path.absolute)
357
+ }}
358
+ >
359
+ {(item) => {
360
+ const path = displayPath(item.absolute, filter(), home())
361
+ if (path === "~") {
362
+ return (
363
+ <div class="w-full flex items-center justify-between rounded-md">
364
+ <div class="flex items-center gap-x-3 grow min-w-0">
365
+ <FileIcon node={{ path: item.absolute, type: "directory" }} class="shrink-0 size-4" />
366
+ <div class="flex items-center text-14-regular min-w-0">
367
+ <span class="text-text-strong whitespace-nowrap">~</span>
368
+ <span class="text-text-weak whitespace-nowrap">/</span>
369
+ </div>
370
+ </div>
371
+ </div>
372
+ )
373
+ }
374
+ return (
375
+ <div class="w-full flex items-center justify-between rounded-md">
376
+ <div class="flex items-center gap-x-3 grow min-w-0">
377
+ <FileIcon node={{ path: item.absolute, type: "directory" }} class="shrink-0 size-4" />
378
+ <div class="flex items-center text-14-regular min-w-0">
379
+ <span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
380
+ {getDirectory(path)}
381
+ </span>
382
+ <span class="text-text-strong whitespace-nowrap">{getFilename(path)}</span>
383
+ <span class="text-text-weak whitespace-nowrap">/</span>
384
+ </div>
385
+ </div>
386
+ </div>
387
+ )
388
+ }}
389
+ </List>
390
+ </Dialog>
391
+ )
392
+ }