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,486 @@
1
+ import { AppIcon } from "@reign-labs/ui/app-icon"
2
+ import { Button } from "@reign-labs/ui/button"
3
+ import { DropdownMenu } from "@reign-labs/ui/dropdown-menu"
4
+ import { Icon } from "@reign-labs/ui/icon"
5
+ import { IconButton } from "@reign-labs/ui/icon-button"
6
+ import { Keybind } from "@reign-labs/ui/keybind"
7
+ import { Spinner } from "@reign-labs/ui/spinner"
8
+ import { showToast } from "@reign-labs/ui/toast"
9
+ import { Tooltip, TooltipKeybind } from "@reign-labs/ui/tooltip"
10
+ import { getFilename } from "@reign-labs/util/path"
11
+ import { createEffect, createMemo, For, onCleanup, Show } from "solid-js"
12
+ import { createStore } from "solid-js/store"
13
+ import { Portal } from "solid-js/web"
14
+ import { useCommand } from "@/context/command"
15
+ import { useLanguage } from "@/context/language"
16
+ import { useLayout } from "@/context/layout"
17
+ import { usePlatform } from "@/context/platform"
18
+ import { useServer } from "@/context/server"
19
+ import { useSync } from "@/context/sync"
20
+ import { useTerminal } from "@/context/terminal"
21
+ import { focusTerminalById } from "@/pages/session/helpers"
22
+ import { useSessionLayout } from "@/pages/session/session-layout"
23
+ import { messageAgentColor } from "@/utils/agent"
24
+ import { decode64 } from "@/utils/base64"
25
+ import { Persist, persisted } from "@/utils/persist"
26
+ import { StatusPopover } from "../status-popover"
27
+
28
+ const OPEN_APPS = [
29
+ "vscode",
30
+ "cursor",
31
+ "zed",
32
+ "textmate",
33
+ "antigravity",
34
+ "finder",
35
+ "terminal",
36
+ "iterm2",
37
+ "ghostty",
38
+ "warp",
39
+ "xcode",
40
+ "android-studio",
41
+ "powershell",
42
+ "sublime-text",
43
+ ] as const
44
+
45
+ type OpenApp = (typeof OPEN_APPS)[number]
46
+ type OS = "macos" | "windows" | "linux" | "unknown"
47
+
48
+ const MAC_APPS = [
49
+ {
50
+ id: "vscode",
51
+ label: "session.header.open.app.vscode",
52
+ icon: "vscode",
53
+ openWith: "Visual Studio Code",
54
+ },
55
+ { id: "cursor", label: "session.header.open.app.cursor", icon: "cursor", openWith: "Cursor" },
56
+ { id: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "Zed" },
57
+ { id: "textmate", label: "session.header.open.app.textmate", icon: "textmate", openWith: "TextMate" },
58
+ {
59
+ id: "antigravity",
60
+ label: "session.header.open.app.antigravity",
61
+ icon: "antigravity",
62
+ openWith: "Antigravity",
63
+ },
64
+ { id: "terminal", label: "session.header.open.app.terminal", icon: "terminal", openWith: "Terminal" },
65
+ { id: "iterm2", label: "session.header.open.app.iterm2", icon: "iterm2", openWith: "iTerm" },
66
+ { id: "ghostty", label: "session.header.open.app.ghostty", icon: "ghostty", openWith: "Ghostty" },
67
+ { id: "warp", label: "session.header.open.app.warp", icon: "warp", openWith: "Warp" },
68
+ { id: "xcode", label: "session.header.open.app.xcode", icon: "xcode", openWith: "Xcode" },
69
+ {
70
+ id: "android-studio",
71
+ label: "session.header.open.app.androidStudio",
72
+ icon: "android-studio",
73
+ openWith: "Android Studio",
74
+ },
75
+ {
76
+ id: "sublime-text",
77
+ label: "session.header.open.app.sublimeText",
78
+ icon: "sublime-text",
79
+ openWith: "Sublime Text",
80
+ },
81
+ ] as const
82
+
83
+ const WINDOWS_APPS = [
84
+ { id: "vscode", label: "session.header.open.app.vscode", icon: "vscode", openWith: "code" },
85
+ { id: "cursor", label: "session.header.open.app.cursor", icon: "cursor", openWith: "cursor" },
86
+ { id: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "zed" },
87
+ {
88
+ id: "powershell",
89
+ label: "session.header.open.app.powershell",
90
+ icon: "powershell",
91
+ openWith: "powershell",
92
+ },
93
+ {
94
+ id: "sublime-text",
95
+ label: "session.header.open.app.sublimeText",
96
+ icon: "sublime-text",
97
+ openWith: "Sublime Text",
98
+ },
99
+ ] as const
100
+
101
+ const LINUX_APPS = [
102
+ { id: "vscode", label: "session.header.open.app.vscode", icon: "vscode", openWith: "code" },
103
+ { id: "cursor", label: "session.header.open.app.cursor", icon: "cursor", openWith: "cursor" },
104
+ { id: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "zed" },
105
+ {
106
+ id: "sublime-text",
107
+ label: "session.header.open.app.sublimeText",
108
+ icon: "sublime-text",
109
+ openWith: "Sublime Text",
110
+ },
111
+ ] as const
112
+
113
+ const detectOS = (platform: ReturnType<typeof usePlatform>): OS => {
114
+ if (platform.platform === "desktop" && platform.os) return platform.os
115
+ if (typeof navigator !== "object") return "unknown"
116
+ const value = navigator.platform || navigator.userAgent
117
+ if (/Mac/i.test(value)) return "macos"
118
+ if (/Win/i.test(value)) return "windows"
119
+ if (/Linux/i.test(value)) return "linux"
120
+ return "unknown"
121
+ }
122
+
123
+ const showRequestError = (language: ReturnType<typeof useLanguage>, err: unknown) => {
124
+ showToast({
125
+ variant: "error",
126
+ title: language.t("common.requestFailed"),
127
+ description: err instanceof Error ? err.message : String(err),
128
+ })
129
+ }
130
+
131
+ export function SessionHeader() {
132
+ const layout = useLayout()
133
+ const command = useCommand()
134
+ const server = useServer()
135
+ const platform = usePlatform()
136
+ const language = useLanguage()
137
+ const sync = useSync()
138
+ const terminal = useTerminal()
139
+ const { params, view } = useSessionLayout()
140
+
141
+ const projectDirectory = createMemo(() => decode64(params.dir) ?? "")
142
+ const project = createMemo(() => {
143
+ const directory = projectDirectory()
144
+ if (!directory) return
145
+ return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory))
146
+ })
147
+ const name = createMemo(() => {
148
+ const current = project()
149
+ if (current) return current.name || getFilename(current.worktree)
150
+ return getFilename(projectDirectory())
151
+ })
152
+ const hotkey = createMemo(() => command.keybind("file.open"))
153
+ const os = createMemo(() => detectOS(platform))
154
+
155
+ const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({
156
+ finder: true,
157
+ })
158
+
159
+ const apps = createMemo(() => {
160
+ if (os() === "macos") return MAC_APPS
161
+ if (os() === "windows") return WINDOWS_APPS
162
+ return LINUX_APPS
163
+ })
164
+
165
+ const fileManager = createMemo(() => {
166
+ if (os() === "macos") return { label: "session.header.open.finder", icon: "finder" as const }
167
+ if (os() === "windows") return { label: "session.header.open.fileExplorer", icon: "file-explorer" as const }
168
+ return { label: "session.header.open.fileManager", icon: "finder" as const }
169
+ })
170
+
171
+ createEffect(() => {
172
+ if (platform.platform !== "desktop") return
173
+ if (!platform.checkAppExists) return
174
+
175
+ const list = apps()
176
+
177
+ setExists(Object.fromEntries(list.map((app) => [app.id, undefined])) as Partial<Record<OpenApp, boolean>>)
178
+
179
+ void Promise.all(
180
+ list.map((app) =>
181
+ Promise.resolve(platform.checkAppExists?.(app.openWith))
182
+ .then((value) => Boolean(value))
183
+ .catch(() => false)
184
+ .then((ok) => [app.id, ok] as const),
185
+ ),
186
+ ).then((entries) => {
187
+ setExists(Object.fromEntries(entries) as Partial<Record<OpenApp, boolean>>)
188
+ })
189
+ })
190
+
191
+ const options = createMemo(() => {
192
+ return [
193
+ { id: "finder", label: language.t(fileManager().label), icon: fileManager().icon },
194
+ ...apps()
195
+ .filter((app) => exists[app.id])
196
+ .map((app) => ({ ...app, label: language.t(app.label) })),
197
+ ] as const
198
+ })
199
+
200
+ const toggleTerminal = () => {
201
+ const next = !view().terminal.opened()
202
+ view().terminal.toggle()
203
+ if (!next) return
204
+
205
+ const id = terminal.active()
206
+ if (!id) return
207
+ focusTerminalById(id)
208
+ }
209
+
210
+ const [prefs, setPrefs] = persisted(Persist.global("open.app"), createStore({ app: "finder" as OpenApp }))
211
+ const [menu, setMenu] = createStore({ open: false })
212
+ const [openRequest, setOpenRequest] = createStore({
213
+ app: undefined as OpenApp | undefined,
214
+ })
215
+
216
+ const canOpen = createMemo(() => platform.platform === "desktop" && !!platform.openPath && server.isLocal())
217
+ const current = createMemo(
218
+ () =>
219
+ options().find((o) => o.id === prefs.app) ??
220
+ options()[0] ??
221
+ ({ id: "finder", label: fileManager().label, icon: fileManager().icon } as const),
222
+ )
223
+ const opening = createMemo(() => openRequest.app !== undefined)
224
+ const tint = createMemo(() =>
225
+ messageAgentColor(params.id ? sync.data.message[params.id] : undefined, sync.data.agent),
226
+ )
227
+
228
+ const selectApp = (app: OpenApp) => {
229
+ if (!options().some((item) => item.id === app)) return
230
+ setPrefs("app", app)
231
+ }
232
+
233
+ const openDir = (app: OpenApp) => {
234
+ if (opening() || !canOpen() || !platform.openPath) return
235
+ const directory = projectDirectory()
236
+ if (!directory) return
237
+
238
+ const item = options().find((o) => o.id === app)
239
+ const openWith = item && "openWith" in item ? item.openWith : undefined
240
+ setOpenRequest("app", app)
241
+ platform
242
+ .openPath(directory, openWith)
243
+ .catch((err: unknown) => showRequestError(language, err))
244
+ .finally(() => {
245
+ setOpenRequest("app", undefined)
246
+ })
247
+ }
248
+
249
+ const copyPath = () => {
250
+ const directory = projectDirectory()
251
+ if (!directory) return
252
+ navigator.clipboard
253
+ .writeText(directory)
254
+ .then(() => {
255
+ showToast({
256
+ variant: "success",
257
+ icon: "circle-check",
258
+ title: language.t("session.share.copy.copied"),
259
+ description: directory,
260
+ })
261
+ })
262
+ .catch((err: unknown) => showRequestError(language, err))
263
+ }
264
+
265
+ const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center"))
266
+ const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right"))
267
+
268
+ return (
269
+ <>
270
+ <Show when={centerMount()}>
271
+ {(mount) => (
272
+ <Portal mount={mount()}>
273
+ <Button
274
+ type="button"
275
+ variant="ghost"
276
+ size="small"
277
+ class="hidden md:flex w-[240px] max-w-full min-w-0 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-panel shadow-none cursor-default"
278
+ onClick={() => command.trigger("file.open")}
279
+ aria-label={language.t("session.header.searchFiles")}
280
+ >
281
+ <div class="flex min-w-0 flex-1 items-center overflow-visible">
282
+ <span class="flex-1 min-w-0 text-12-regular text-text-weak truncate text-left">
283
+ {language.t("session.header.search.placeholder", {
284
+ project: name(),
285
+ })}
286
+ </span>
287
+ </div>
288
+
289
+ <Show when={hotkey()}>
290
+ {(keybind) => (
291
+ <Keybind class="shrink-0 !border-0 !bg-transparent !shadow-none px-0 text-text-weaker">
292
+ {keybind()}
293
+ </Keybind>
294
+ )}
295
+ </Show>
296
+ </Button>
297
+ </Portal>
298
+ )}
299
+ </Show>
300
+ <Show when={rightMount()}>
301
+ {(mount) => (
302
+ <Portal mount={mount()}>
303
+ <div class="flex items-center gap-2">
304
+ <Show when={projectDirectory()}>
305
+ <div class="hidden xl:flex items-center">
306
+ <Show
307
+ when={canOpen()}
308
+ fallback={
309
+ <div class="flex h-[24px] box-border items-center rounded-md border border-border-weak-base bg-surface-panel overflow-hidden">
310
+ <Button
311
+ variant="ghost"
312
+ class="rounded-none h-full py-0 pr-3 pl-0.5 gap-1.5 border-none shadow-none"
313
+ onClick={copyPath}
314
+ aria-label={language.t("session.header.open.copyPath")}
315
+ >
316
+ <Icon name="copy" size="small" class="text-icon-base" />
317
+ <span class="text-12-regular text-text-strong">
318
+ {language.t("session.header.open.copyPath")}
319
+ </span>
320
+ </Button>
321
+ </div>
322
+ }
323
+ >
324
+ <div class="flex items-center">
325
+ <div class="flex h-[24px] box-border items-center rounded-md border border-border-weak-base bg-surface-panel overflow-hidden">
326
+ <Button
327
+ variant="ghost"
328
+ class="rounded-none h-full px-0.5 border-none shadow-none disabled:!cursor-default"
329
+ classList={{
330
+ "bg-surface-raised-base-active": opening(),
331
+ }}
332
+ onClick={() => openDir(current().id)}
333
+ disabled={opening()}
334
+ aria-label={language.t("session.header.open.ariaLabel", { app: current().label })}
335
+ >
336
+ <div class="flex size-5 shrink-0 items-center justify-center [&_[data-component=app-icon]]:size-5">
337
+ <Show when={opening()} fallback={<AppIcon id={current().icon} />}>
338
+ <Spinner class="size-3.5" style={{ color: tint() ?? "var(--icon-base)" }} />
339
+ </Show>
340
+ </div>
341
+ </Button>
342
+ <DropdownMenu
343
+ gutter={4}
344
+ placement="bottom-end"
345
+ open={menu.open}
346
+ onOpenChange={(open) => setMenu("open", open)}
347
+ >
348
+ <DropdownMenu.Trigger
349
+ as={IconButton}
350
+ icon="chevron-down"
351
+ variant="ghost"
352
+ disabled={opening()}
353
+ class="rounded-none h-full w-[20px] p-0 border-none shadow-none data-[expanded]:bg-surface-raised-base-active disabled:!cursor-default"
354
+ classList={{
355
+ "bg-surface-raised-base-active": opening(),
356
+ }}
357
+ aria-label={language.t("session.header.open.menu")}
358
+ />
359
+ <DropdownMenu.Portal>
360
+ <DropdownMenu.Content class="[&_[data-slot=dropdown-menu-item]]:pl-1 [&_[data-slot=dropdown-menu-radio-item]]:pl-1 [&_[data-slot=dropdown-menu-radio-item]+[data-slot=dropdown-menu-radio-item]]:mt-1">
361
+ <DropdownMenu.Group>
362
+ <DropdownMenu.GroupLabel class="!px-1 !py-1">
363
+ {language.t("session.header.openIn")}
364
+ </DropdownMenu.GroupLabel>
365
+ <DropdownMenu.RadioGroup
366
+ class="mt-1"
367
+ value={current().id}
368
+ onChange={(value) => {
369
+ if (!OPEN_APPS.includes(value as OpenApp)) return
370
+ selectApp(value as OpenApp)
371
+ }}
372
+ >
373
+ <For each={options()}>
374
+ {(o) => (
375
+ <DropdownMenu.RadioItem
376
+ value={o.id}
377
+ disabled={opening()}
378
+ onSelect={() => {
379
+ setMenu("open", false)
380
+ openDir(o.id)
381
+ }}
382
+ >
383
+ <div class="flex size-5 shrink-0 items-center justify-center [&_[data-component=app-icon]]:size-5">
384
+ <AppIcon id={o.icon} />
385
+ </div>
386
+ <DropdownMenu.ItemLabel>{o.label}</DropdownMenu.ItemLabel>
387
+ <DropdownMenu.ItemIndicator>
388
+ <Icon name="check-small" size="small" class="text-icon-weak" />
389
+ </DropdownMenu.ItemIndicator>
390
+ </DropdownMenu.RadioItem>
391
+ )}
392
+ </For>
393
+ </DropdownMenu.RadioGroup>
394
+ </DropdownMenu.Group>
395
+ <DropdownMenu.Separator />
396
+ <DropdownMenu.Item
397
+ onSelect={() => {
398
+ setMenu("open", false)
399
+ copyPath()
400
+ }}
401
+ >
402
+ <div class="flex size-5 shrink-0 items-center justify-center">
403
+ <Icon name="copy" size="small" class="text-icon-weak" />
404
+ </div>
405
+ <DropdownMenu.ItemLabel>
406
+ {language.t("session.header.open.copyPath")}
407
+ </DropdownMenu.ItemLabel>
408
+ </DropdownMenu.Item>
409
+ </DropdownMenu.Content>
410
+ </DropdownMenu.Portal>
411
+ </DropdownMenu>
412
+ </div>
413
+ </div>
414
+ </Show>
415
+ </div>
416
+ </Show>
417
+ <div class="flex items-center gap-1">
418
+ <Tooltip placement="bottom" value={language.t("status.popover.trigger")}>
419
+ <StatusPopover />
420
+ </Tooltip>
421
+ <TooltipKeybind
422
+ title={language.t("command.terminal.toggle")}
423
+ keybind={command.keybind("terminal.toggle")}
424
+ >
425
+ <Button
426
+ variant="ghost"
427
+ class="group/terminal-toggle titlebar-icon w-8 h-6 p-0 box-border shrink-0"
428
+ onClick={toggleTerminal}
429
+ aria-label={language.t("command.terminal.toggle")}
430
+ aria-expanded={view().terminal.opened()}
431
+ aria-controls="terminal-panel"
432
+ >
433
+ <Icon size="small" name={view().terminal.opened() ? "terminal-active" : "terminal"} />
434
+ </Button>
435
+ </TooltipKeybind>
436
+
437
+ <div class="hidden md:flex items-center gap-1 shrink-0">
438
+ <TooltipKeybind
439
+ title={language.t("command.review.toggle")}
440
+ keybind={command.keybind("review.toggle")}
441
+ >
442
+ <Button
443
+ variant="ghost"
444
+ class="group/review-toggle titlebar-icon w-8 h-6 p-0 box-border"
445
+ onClick={() => view().reviewPanel.toggle()}
446
+ aria-label={language.t("command.review.toggle")}
447
+ aria-expanded={view().reviewPanel.opened()}
448
+ aria-controls="review-panel"
449
+ >
450
+ <Icon size="small" name={view().reviewPanel.opened() ? "review-active" : "review"} />
451
+ </Button>
452
+ </TooltipKeybind>
453
+
454
+ <TooltipKeybind
455
+ title={language.t("command.fileTree.toggle")}
456
+ keybind={command.keybind("fileTree.toggle")}
457
+ >
458
+ <Button
459
+ variant="ghost"
460
+ class="titlebar-icon w-8 h-6 p-0 box-border"
461
+ onClick={() => layout.fileTree.toggle()}
462
+ aria-label={language.t("command.fileTree.toggle")}
463
+ aria-expanded={layout.fileTree.opened()}
464
+ aria-controls="file-tree-panel"
465
+ >
466
+ <div class="relative flex items-center justify-center size-4">
467
+ <Icon
468
+ size="small"
469
+ name={layout.fileTree.opened() ? "file-tree-active" : "file-tree"}
470
+ classList={{
471
+ "text-icon-strong": layout.fileTree.opened(),
472
+ "text-icon-weak": !layout.fileTree.opened(),
473
+ }}
474
+ />
475
+ </div>
476
+ </Button>
477
+ </TooltipKeybind>
478
+ </div>
479
+ </div>
480
+ </div>
481
+ </Portal>
482
+ )}
483
+ </Show>
484
+ </>
485
+ )
486
+ }
@@ -0,0 +1,91 @@
1
+ import { Show, createMemo } from "solid-js"
2
+ import { DateTime } from "luxon"
3
+ import { useSync } from "@/context/sync"
4
+ import { useSDK } from "@/context/sdk"
5
+ import { useLanguage } from "@/context/language"
6
+ import { Icon } from "@reign-labs/ui/icon"
7
+ import { Mark } from "@reign-labs/ui/logo"
8
+ import { getDirectory, getFilename } from "@reign-labs/util/path"
9
+
10
+ const MAIN_WORKTREE = "main"
11
+ const CREATE_WORKTREE = "create"
12
+ const ROOT_CLASS = "size-full flex flex-col"
13
+
14
+ interface NewSessionViewProps {
15
+ worktree: string
16
+ }
17
+
18
+ export function NewSessionView(props: NewSessionViewProps) {
19
+ const sync = useSync()
20
+ const sdk = useSDK()
21
+ const language = useLanguage()
22
+
23
+ const sandboxes = createMemo(() => sync.project?.sandboxes ?? [])
24
+ const options = createMemo(() => [MAIN_WORKTREE, ...sandboxes(), CREATE_WORKTREE])
25
+ const current = createMemo(() => {
26
+ const selection = props.worktree
27
+ if (options().includes(selection)) return selection
28
+ return MAIN_WORKTREE
29
+ })
30
+ const projectRoot = createMemo(() => sync.project?.worktree ?? sdk.directory)
31
+ const isWorktree = createMemo(() => {
32
+ const project = sync.project
33
+ if (!project) return false
34
+ return sdk.directory !== project.worktree
35
+ })
36
+
37
+ const label = (value: string) => {
38
+ if (value === MAIN_WORKTREE) {
39
+ if (isWorktree()) return language.t("session.new.worktree.main")
40
+ const branch = sync.data.vcs?.branch
41
+ if (branch) return language.t("session.new.worktree.mainWithBranch", { branch })
42
+ return language.t("session.new.worktree.main")
43
+ }
44
+
45
+ if (value === CREATE_WORKTREE) return language.t("session.new.worktree.create")
46
+
47
+ return getFilename(value)
48
+ }
49
+
50
+ return (
51
+ <div class={ROOT_CLASS}>
52
+ <div class="h-12 shrink-0" aria-hidden />
53
+ <div class="flex-1 px-6 pb-30 flex items-center justify-center text-center">
54
+ <div class="w-full max-w-200 flex flex-col items-center text-center gap-4">
55
+ <div class="flex flex-col items-center gap-6">
56
+ <Mark class="w-10" />
57
+ <div class="text-20-medium text-text-strong">{language.t("session.new.title")}</div>
58
+ </div>
59
+ <div class="w-full flex flex-col gap-4 items-center">
60
+ <div class="flex items-start justify-center gap-3 min-h-5">
61
+ <div class="text-12-medium text-text-weak select-text leading-5 min-w-0 max-w-160 break-words text-center">
62
+ {getDirectory(projectRoot())}
63
+ <span class="text-text-strong">{getFilename(projectRoot())}</span>
64
+ </div>
65
+ </div>
66
+ <div class="flex items-start justify-center gap-1.5 min-h-5">
67
+ <Icon name="branch" size="small" class="mt-0.5 shrink-0" />
68
+ <div class="text-12-medium text-text-weak select-text leading-5 min-w-0 max-w-160 break-words text-center">
69
+ {label(current())}
70
+ </div>
71
+ </div>
72
+ <Show when={sync.project}>
73
+ {(project) => (
74
+ <div class="flex items-start justify-center gap-3 min-h-5">
75
+ <div class="text-12-medium text-text-weak leading-5 min-w-0 max-w-160 break-words text-center">
76
+ {language.t("session.new.lastModified")}&nbsp;
77
+ <span class="text-text-strong">
78
+ {DateTime.fromMillis(project().time.updated ?? project().time.created)
79
+ .setLocale(language.intl())
80
+ .toRelative()}
81
+ </span>
82
+ </div>
83
+ </div>
84
+ )}
85
+ </Show>
86
+ </div>
87
+ </div>
88
+ </div>
89
+ </div>
90
+ )
91
+ }
@@ -0,0 +1,70 @@
1
+ import { createMemo, Show } from "solid-js"
2
+ import type { JSX } from "solid-js"
3
+ import { createSortable } from "@thisbeyond/solid-dnd"
4
+ import { FileIcon } from "@reign-labs/ui/file-icon"
5
+ import { IconButton } from "@reign-labs/ui/icon-button"
6
+ import { TooltipKeybind } from "@reign-labs/ui/tooltip"
7
+ import { Tabs } from "@reign-labs/ui/tabs"
8
+ import { getFilename } from "@reign-labs/util/path"
9
+ import { useFile } from "@/context/file"
10
+ import { useLanguage } from "@/context/language"
11
+ import { useCommand } from "@/context/command"
12
+
13
+ export function FileVisual(props: { path: string; active?: boolean }): JSX.Element {
14
+ return (
15
+ <div class="flex items-center gap-x-1.5 min-w-0">
16
+ <Show
17
+ when={!props.active}
18
+ fallback={<FileIcon node={{ path: props.path, type: "file" }} class="size-4 shrink-0" />}
19
+ >
20
+ <span class="relative inline-flex size-4 shrink-0">
21
+ <FileIcon node={{ path: props.path, type: "file" }} class="absolute inset-0 size-4 tab-fileicon-color" />
22
+ <FileIcon node={{ path: props.path, type: "file" }} mono class="absolute inset-0 size-4 tab-fileicon-mono" />
23
+ </span>
24
+ </Show>
25
+ <span class="text-14-medium truncate">{getFilename(props.path)}</span>
26
+ </div>
27
+ )
28
+ }
29
+
30
+ export function SortableTab(props: { tab: string; onTabClose: (tab: string) => void }): JSX.Element {
31
+ const file = useFile()
32
+ const language = useLanguage()
33
+ const command = useCommand()
34
+ const sortable = createSortable(props.tab)
35
+ const path = createMemo(() => file.pathFromTab(props.tab))
36
+ const content = createMemo(() => {
37
+ const value = path()
38
+ if (!value) return
39
+ return <FileVisual path={value} />
40
+ })
41
+ return (
42
+ <div use:sortable class="h-full flex items-center" classList={{ "opacity-0": sortable.isActiveDraggable }}>
43
+ <div class="relative">
44
+ <Tabs.Trigger
45
+ value={props.tab}
46
+ closeButton={
47
+ <TooltipKeybind
48
+ title={language.t("common.closeTab")}
49
+ keybind={command.keybind("tab.close")}
50
+ placement="bottom"
51
+ gutter={10}
52
+ >
53
+ <IconButton
54
+ icon="close-small"
55
+ variant="ghost"
56
+ class="h-5 w-5"
57
+ onClick={() => props.onTabClose(props.tab)}
58
+ aria-label={language.t("common.closeTab")}
59
+ />
60
+ </TooltipKeybind>
61
+ }
62
+ hideCloseButton
63
+ onMiddleClick={() => props.onTabClose(props.tab)}
64
+ >
65
+ <Show when={content()}>{(value) => value()}</Show>
66
+ </Tabs.Trigger>
67
+ </div>
68
+ </div>
69
+ )
70
+ }