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,466 @@
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 { Icon } from "@reign-labs/ui/icon"
5
+ import { Keybind } from "@reign-labs/ui/keybind"
6
+ import { List } from "@reign-labs/ui/list"
7
+ import { base64Encode } from "@reign-labs/util/encode"
8
+ import { getDirectory, getFilename } from "@reign-labs/util/path"
9
+ import { useNavigate } from "@solidjs/router"
10
+ import { createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js"
11
+ import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
12
+ import { useGlobalSDK } from "@/context/global-sdk"
13
+ import { useGlobalSync } from "@/context/global-sync"
14
+ import { useLayout } from "@/context/layout"
15
+ import { useFile } from "@/context/file"
16
+ import { useLanguage } from "@/context/language"
17
+ import { useSessionLayout } from "@/pages/session/session-layout"
18
+ import { createSessionTabs } from "@/pages/session/helpers"
19
+ import { decode64 } from "@/utils/base64"
20
+ import { getRelativeTime } from "@/utils/time"
21
+
22
+ type EntryType = "command" | "file" | "session"
23
+
24
+ type Entry = {
25
+ id: string
26
+ type: EntryType
27
+ title: string
28
+ description?: string
29
+ keybind?: string
30
+ category: string
31
+ option?: CommandOption
32
+ path?: string
33
+ directory?: string
34
+ sessionID?: string
35
+ archived?: number
36
+ updated?: number
37
+ }
38
+
39
+ type DialogSelectFileMode = "all" | "files"
40
+
41
+ const ENTRY_LIMIT = 5
42
+ const COMMON_COMMAND_IDS = [
43
+ "session.new",
44
+ "workspace.new",
45
+ "session.previous",
46
+ "session.next",
47
+ "terminal.toggle",
48
+ "review.toggle",
49
+ ] as const
50
+
51
+ const uniqueEntries = (items: Entry[]) => {
52
+ const seen = new Set<string>()
53
+ const out: Entry[] = []
54
+ for (const item of items) {
55
+ if (seen.has(item.id)) continue
56
+ seen.add(item.id)
57
+ out.push(item)
58
+ }
59
+ return out
60
+ }
61
+
62
+ const createCommandEntry = (option: CommandOption, category: string): Entry => ({
63
+ id: "command:" + option.id,
64
+ type: "command",
65
+ title: option.title,
66
+ description: option.description,
67
+ keybind: option.keybind,
68
+ category,
69
+ option,
70
+ })
71
+
72
+ const createFileEntry = (path: string, category: string): Entry => ({
73
+ id: "file:" + path,
74
+ type: "file",
75
+ title: path,
76
+ category,
77
+ path,
78
+ })
79
+
80
+ const createSessionEntry = (
81
+ input: {
82
+ directory: string
83
+ id: string
84
+ title: string
85
+ description: string
86
+ archived?: number
87
+ updated?: number
88
+ },
89
+ category: string,
90
+ ): Entry => ({
91
+ id: `session:${input.directory}:${input.id}`,
92
+ type: "session",
93
+ title: input.title,
94
+ description: input.description,
95
+ category,
96
+ directory: input.directory,
97
+ sessionID: input.id,
98
+ archived: input.archived,
99
+ updated: input.updated,
100
+ })
101
+
102
+ function createCommandEntries(props: {
103
+ filesOnly: () => boolean
104
+ command: ReturnType<typeof useCommand>
105
+ language: ReturnType<typeof useLanguage>
106
+ }) {
107
+ const allowed = createMemo(() => {
108
+ if (props.filesOnly()) return []
109
+ return props.command.options.filter(
110
+ (option) => !option.disabled && !option.id.startsWith("suggested.") && option.id !== "file.open",
111
+ )
112
+ })
113
+
114
+ const list = createMemo(() => {
115
+ const category = props.language.t("palette.group.commands")
116
+ return allowed().map((option) => createCommandEntry(option, category))
117
+ })
118
+
119
+ const picks = createMemo(() => {
120
+ const all = allowed()
121
+ const order = new Map<string, number>(COMMON_COMMAND_IDS.map((id, index) => [id, index]))
122
+ const picked = all.filter((option) => order.has(option.id))
123
+ const base = picked.length ? picked : all.slice(0, ENTRY_LIMIT)
124
+ const sorted = picked.length ? [...base].sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0)) : base
125
+ const category = props.language.t("palette.group.commands")
126
+ return sorted.map((option) => createCommandEntry(option, category))
127
+ })
128
+
129
+ return { allowed, list, picks }
130
+ }
131
+
132
+ function createFileEntries(props: {
133
+ file: ReturnType<typeof useFile>
134
+ tabs: () => ReturnType<ReturnType<typeof useLayout>["tabs"]>
135
+ language: ReturnType<typeof useLanguage>
136
+ }) {
137
+ const tabState = createSessionTabs({
138
+ tabs: props.tabs,
139
+ pathFromTab: props.file.pathFromTab,
140
+ normalizeTab: (tab) => (tab.startsWith("file://") ? props.file.tab(tab) : tab),
141
+ })
142
+ const recent = createMemo(() => {
143
+ const all = tabState.openedTabs()
144
+ const active = tabState.activeFileTab()
145
+ const order = active ? [active, ...all.filter((item) => item !== active)] : all
146
+ const seen = new Set<string>()
147
+ const category = props.language.t("palette.group.files")
148
+ const items: Entry[] = []
149
+
150
+ for (const item of order) {
151
+ const path = props.file.pathFromTab(item)
152
+ if (!path) continue
153
+ if (seen.has(path)) continue
154
+ seen.add(path)
155
+ items.push(createFileEntry(path, category))
156
+ }
157
+
158
+ return items.slice(0, ENTRY_LIMIT)
159
+ })
160
+
161
+ const root = createMemo(() => {
162
+ const category = props.language.t("palette.group.files")
163
+ const nodes = props.file.tree.children("")
164
+ const paths = nodes
165
+ .filter((node) => node.type === "file")
166
+ .map((node) => node.path)
167
+ .sort((a, b) => a.localeCompare(b))
168
+ return paths.slice(0, ENTRY_LIMIT).map((path) => createFileEntry(path, category))
169
+ })
170
+
171
+ return { recent, root }
172
+ }
173
+
174
+ function createSessionEntries(props: {
175
+ workspaces: () => string[]
176
+ label: (directory: string) => string
177
+ globalSDK: ReturnType<typeof useGlobalSDK>
178
+ language: ReturnType<typeof useLanguage>
179
+ }) {
180
+ const state: {
181
+ token: number
182
+ inflight: Promise<Entry[]> | undefined
183
+ cached: Entry[] | undefined
184
+ } = {
185
+ token: 0,
186
+ inflight: undefined,
187
+ cached: undefined,
188
+ }
189
+
190
+ const sessions = (text: string) => {
191
+ const query = text.trim()
192
+ if (!query) {
193
+ state.token += 1
194
+ state.inflight = undefined
195
+ state.cached = undefined
196
+ return [] as Entry[]
197
+ }
198
+
199
+ if (state.cached) return state.cached
200
+ if (state.inflight) return state.inflight
201
+
202
+ const current = state.token
203
+ const dirs = props.workspaces()
204
+ if (dirs.length === 0) return [] as Entry[]
205
+
206
+ state.inflight = Promise.all(
207
+ dirs.map((directory) => {
208
+ const description = props.label(directory)
209
+ return props.globalSDK.client.session
210
+ .list({ directory, roots: true })
211
+ .then((x) =>
212
+ (x.data ?? [])
213
+ .filter((s) => !!s?.id)
214
+ .map((s) => ({
215
+ id: s.id,
216
+ title: s.title ?? props.language.t("command.session.new"),
217
+ description,
218
+ directory,
219
+ archived: s.time?.archived,
220
+ updated: s.time?.updated,
221
+ })),
222
+ )
223
+ .catch(
224
+ () =>
225
+ [] as {
226
+ id: string
227
+ title: string
228
+ description: string
229
+ directory: string
230
+ archived?: number
231
+ updated?: number
232
+ }[],
233
+ )
234
+ }),
235
+ )
236
+ .then((results) => {
237
+ if (state.token !== current) return [] as Entry[]
238
+ const seen = new Set<string>()
239
+ const category = props.language.t("command.category.session")
240
+ const next = results
241
+ .flat()
242
+ .filter((item) => {
243
+ const key = `${item.directory}:${item.id}`
244
+ if (seen.has(key)) return false
245
+ seen.add(key)
246
+ return true
247
+ })
248
+ .map((item) => createSessionEntry(item, category))
249
+ state.cached = next
250
+ return next
251
+ })
252
+ .catch(() => [] as Entry[])
253
+ .finally(() => {
254
+ state.inflight = undefined
255
+ })
256
+
257
+ return state.inflight
258
+ }
259
+
260
+ return { sessions }
261
+ }
262
+
263
+ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFile?: (path: string) => void }) {
264
+ const command = useCommand()
265
+ const language = useLanguage()
266
+ const layout = useLayout()
267
+ const file = useFile()
268
+ const dialog = useDialog()
269
+ const navigate = useNavigate()
270
+ const globalSDK = useGlobalSDK()
271
+ const globalSync = useGlobalSync()
272
+ const { params, tabs, view } = useSessionLayout()
273
+ const filesOnly = () => props.mode === "files"
274
+ const state = { cleanup: undefined as (() => void) | void, committed: false }
275
+ const [grouped, setGrouped] = createSignal(false)
276
+ const commandEntries = createCommandEntries({ filesOnly, command, language })
277
+ const fileEntries = createFileEntries({ file, tabs, language })
278
+
279
+ const projectDirectory = createMemo(() => decode64(params.dir) ?? "")
280
+ const project = createMemo(() => {
281
+ const directory = projectDirectory()
282
+ if (!directory) return
283
+ return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory))
284
+ })
285
+ const workspaces = createMemo(() => {
286
+ const directory = projectDirectory()
287
+ const current = project()
288
+ if (!current) return directory ? [directory] : []
289
+
290
+ const dirs = [current.worktree, ...(current.sandboxes ?? [])]
291
+ if (directory && !dirs.includes(directory)) return [...dirs, directory]
292
+ return dirs
293
+ })
294
+ const homedir = createMemo(() => globalSync.data.path.home)
295
+ const label = (directory: string) => {
296
+ const current = project()
297
+ const kind =
298
+ current && directory === current.worktree
299
+ ? language.t("workspace.type.local")
300
+ : language.t("workspace.type.sandbox")
301
+ const [store] = globalSync.child(directory, { bootstrap: false })
302
+ const home = homedir()
303
+ const path = home ? directory.replace(home, "~") : directory
304
+ const name = store.vcs?.branch ?? getFilename(directory)
305
+ return `${kind} : ${name || path}`
306
+ }
307
+
308
+ const { sessions } = createSessionEntries({ workspaces, label, globalSDK, language })
309
+
310
+ const items = async (text: string) => {
311
+ const query = text.trim()
312
+ setGrouped(query.length > 0)
313
+
314
+ if (!query && filesOnly()) {
315
+ const loaded = file.tree.state("")?.loaded
316
+ const pending = loaded ? Promise.resolve() : file.tree.list("")
317
+ const next = uniqueEntries([...fileEntries.recent(), ...fileEntries.root()])
318
+
319
+ if (loaded || next.length > 0) {
320
+ void pending
321
+ return next
322
+ }
323
+
324
+ await pending
325
+ return uniqueEntries([...fileEntries.recent(), ...fileEntries.root()])
326
+ }
327
+
328
+ if (!query) return [...commandEntries.picks(), ...fileEntries.recent()]
329
+
330
+ if (filesOnly()) {
331
+ const files = await file.searchFiles(query)
332
+ const category = language.t("palette.group.files")
333
+ return files.map((path) => createFileEntry(path, category))
334
+ }
335
+
336
+ const [files, nextSessions] = await Promise.all([file.searchFiles(query), Promise.resolve(sessions(query))])
337
+ const category = language.t("palette.group.files")
338
+ const entries = files.map((path) => createFileEntry(path, category))
339
+ return [...commandEntries.list(), ...nextSessions, ...entries]
340
+ }
341
+
342
+ const handleMove = (item: Entry | undefined) => {
343
+ state.cleanup?.()
344
+ if (!item) return
345
+ if (item.type !== "command") return
346
+ state.cleanup = item.option?.onHighlight?.()
347
+ }
348
+
349
+ const open = (path: string) => {
350
+ const value = file.tab(path)
351
+ tabs().open(value)
352
+ file.load(path)
353
+ if (!view().reviewPanel.opened()) view().reviewPanel.open()
354
+ layout.fileTree.setTab("all")
355
+ props.onOpenFile?.(path)
356
+ tabs().setActive(value)
357
+ }
358
+
359
+ const handleSelect = (item: Entry | undefined) => {
360
+ if (!item) return
361
+ state.committed = true
362
+ state.cleanup = undefined
363
+ dialog.close()
364
+
365
+ if (item.type === "command") {
366
+ item.option?.onSelect?.("palette")
367
+ return
368
+ }
369
+
370
+ if (item.type === "session") {
371
+ if (!item.directory || !item.sessionID) return
372
+ navigate(`/${base64Encode(item.directory)}/session/${item.sessionID}`)
373
+ return
374
+ }
375
+
376
+ if (!item.path) return
377
+ open(item.path)
378
+ }
379
+
380
+ onCleanup(() => {
381
+ if (state.committed) return
382
+ state.cleanup?.()
383
+ })
384
+
385
+ return (
386
+ <Dialog class="pt-3 pb-0 !max-h-[480px]" transition>
387
+ <List
388
+ search={{
389
+ placeholder: filesOnly()
390
+ ? language.t("session.header.searchFiles")
391
+ : language.t("palette.search.placeholder"),
392
+ autofocus: true,
393
+ hideIcon: true,
394
+ }}
395
+ emptyMessage={language.t("palette.empty")}
396
+ loadingMessage={language.t("common.loading")}
397
+ items={items}
398
+ key={(item) => item.id}
399
+ filterKeys={["title", "description", "category"]}
400
+ groupBy={grouped() ? (item) => item.category : () => ""}
401
+ onMove={handleMove}
402
+ onSelect={handleSelect}
403
+ >
404
+ {(item) => (
405
+ <Switch
406
+ fallback={
407
+ <div class="w-full flex items-center justify-between rounded-md pl-1">
408
+ <div class="flex items-center gap-x-3 grow min-w-0">
409
+ <FileIcon node={{ path: item.path ?? "", type: "file" }} class="shrink-0 size-4" />
410
+ <div class="flex items-center text-14-regular">
411
+ <span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
412
+ {getDirectory(item.path ?? "")}
413
+ </span>
414
+ <span class="text-text-strong whitespace-nowrap">{getFilename(item.path ?? "")}</span>
415
+ </div>
416
+ </div>
417
+ </div>
418
+ }
419
+ >
420
+ <Match when={item.type === "command"}>
421
+ <div class="w-full flex items-center justify-between gap-4">
422
+ <div class="flex items-center gap-2 min-w-0">
423
+ <span class="text-14-regular text-text-strong whitespace-nowrap">{item.title}</span>
424
+ <Show when={item.description}>
425
+ <span class="text-14-regular text-text-weak truncate">{item.description}</span>
426
+ </Show>
427
+ </div>
428
+ <Show when={item.keybind}>
429
+ <Keybind class="rounded-[4px]">{formatKeybind(item.keybind ?? "", language.t)}</Keybind>
430
+ </Show>
431
+ </div>
432
+ </Match>
433
+ <Match when={item.type === "session"}>
434
+ <div class="w-full flex items-center justify-between rounded-md pl-1">
435
+ <div class="flex items-center gap-x-3 grow min-w-0">
436
+ <Icon name="bubble-5" size="small" class="shrink-0 text-icon-weak" />
437
+ <div class="flex items-center gap-2 min-w-0">
438
+ <span
439
+ class="text-14-regular text-text-strong truncate"
440
+ classList={{ "opacity-70": !!item.archived }}
441
+ >
442
+ {item.title}
443
+ </span>
444
+ <Show when={item.description}>
445
+ <span
446
+ class="text-14-regular text-text-weak truncate"
447
+ classList={{ "opacity-70": !!item.archived }}
448
+ >
449
+ {item.description}
450
+ </span>
451
+ </Show>
452
+ </div>
453
+ </div>
454
+ <Show when={item.updated}>
455
+ <span class="text-12-regular text-text-weak whitespace-nowrap ml-2">
456
+ {getRelativeTime(new Date(item.updated!).toISOString(), language.t)}
457
+ </span>
458
+ </Show>
459
+ </div>
460
+ </Match>
461
+ </Switch>
462
+ )}
463
+ </List>
464
+ </Dialog>
465
+ )
466
+ }
@@ -0,0 +1,107 @@
1
+ import { useMutation } from "@tanstack/solid-query"
2
+ import { Component, createMemo, Show } from "solid-js"
3
+ import { useSync } from "@/context/sync"
4
+ import { useSDK } from "@/context/sdk"
5
+ import { Dialog } from "@reign-labs/ui/dialog"
6
+ import { List } from "@reign-labs/ui/list"
7
+ import { Switch } from "@reign-labs/ui/switch"
8
+ import { useLanguage } from "@/context/language"
9
+
10
+ const statusLabels = {
11
+ connected: "mcp.status.connected",
12
+ failed: "mcp.status.failed",
13
+ needs_auth: "mcp.status.needs_auth",
14
+ disabled: "mcp.status.disabled",
15
+ } as const
16
+
17
+ export const DialogSelectMcp: Component = () => {
18
+ const sync = useSync()
19
+ const sdk = useSDK()
20
+ const language = useLanguage()
21
+
22
+ const items = createMemo(() =>
23
+ Object.entries(sync.data.mcp ?? {})
24
+ .map(([name, status]) => ({ name, status: status.status }))
25
+ .sort((a, b) => a.name.localeCompare(b.name)),
26
+ )
27
+
28
+ const toggle = useMutation(() => ({
29
+ mutationFn: async (name: string) => {
30
+ const status = sync.data.mcp[name]
31
+ if (status?.status === "connected") {
32
+ await sdk.client.mcp.disconnect({ name })
33
+ } else {
34
+ await sdk.client.mcp.connect({ name })
35
+ }
36
+
37
+ const result = await sdk.client.mcp.status()
38
+ if (result.data) sync.set("mcp", result.data)
39
+ },
40
+ }))
41
+
42
+ const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length)
43
+ const totalCount = createMemo(() => items().length)
44
+
45
+ return (
46
+ <Dialog
47
+ title={language.t("dialog.mcp.title")}
48
+ description={language.t("dialog.mcp.description", { enabled: enabledCount(), total: totalCount() })}
49
+ >
50
+ <List
51
+ search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }}
52
+ emptyMessage={language.t("dialog.mcp.empty")}
53
+ key={(x) => x?.name ?? ""}
54
+ items={items}
55
+ filterKeys={["name", "status"]}
56
+ sortBy={(a, b) => a.name.localeCompare(b.name)}
57
+ onSelect={(x) => {
58
+ if (!x || toggle.isPending) return
59
+ toggle.mutate(x.name)
60
+ }}
61
+ >
62
+ {(i) => {
63
+ const mcpStatus = () => sync.data.mcp[i.name]
64
+ const status = () => mcpStatus()?.status
65
+ const statusLabel = () => {
66
+ const key = status() ? statusLabels[status() as keyof typeof statusLabels] : undefined
67
+ if (!key) return
68
+ return language.t(key)
69
+ }
70
+ const error = () => {
71
+ const s = mcpStatus()
72
+ return s?.status === "failed" ? s.error : undefined
73
+ }
74
+ const enabled = () => status() === "connected"
75
+ return (
76
+ <div class="w-full flex items-center justify-between gap-x-3">
77
+ <div class="flex flex-col gap-0.5 min-w-0">
78
+ <div class="flex items-center gap-2">
79
+ <span class="truncate">{i.name}</span>
80
+ <Show when={statusLabel()}>
81
+ <span class="text-11-regular text-text-weaker">{statusLabel()}</span>
82
+ </Show>
83
+ <Show when={toggle.isPending && toggle.variables === i.name}>
84
+ <span class="text-11-regular text-text-weak">{language.t("common.loading.ellipsis")}</span>
85
+ </Show>
86
+ </div>
87
+ <Show when={error()}>
88
+ <span class="text-11-regular text-text-weaker truncate">{error()}</span>
89
+ </Show>
90
+ </div>
91
+ <div onClick={(e) => e.stopPropagation()}>
92
+ <Switch
93
+ checked={enabled()}
94
+ disabled={toggle.isPending && toggle.variables === i.name}
95
+ onChange={() => {
96
+ if (toggle.isPending) return
97
+ toggle.mutate(i.name)
98
+ }}
99
+ />
100
+ </div>
101
+ </div>
102
+ )
103
+ }}
104
+ </List>
105
+ </Dialog>
106
+ )
107
+ }