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,57 @@
1
+ export const MAX_TITLEBAR_HISTORY = 100
2
+
3
+ export type TitlebarAction = "back" | "forward" | undefined
4
+
5
+ export type TitlebarHistory = {
6
+ stack: string[]
7
+ index: number
8
+ action: TitlebarAction
9
+ }
10
+
11
+ export function applyPath(state: TitlebarHistory, current: string, max = MAX_TITLEBAR_HISTORY): TitlebarHistory {
12
+ if (!state.stack.length) {
13
+ const stack = current === "/" ? ["/"] : ["/", current]
14
+ return { stack, index: stack.length - 1, action: undefined }
15
+ }
16
+
17
+ const active = state.stack[state.index]
18
+ if (current === active) {
19
+ if (!state.action) return state
20
+ return { ...state, action: undefined }
21
+ }
22
+
23
+ if (state.action) return { ...state, action: undefined }
24
+
25
+ return pushPath(state, current, max)
26
+ }
27
+
28
+ export function pushPath(state: TitlebarHistory, path: string, max = MAX_TITLEBAR_HISTORY): TitlebarHistory {
29
+ const stack = state.stack.slice(0, state.index + 1).concat(path)
30
+ const next = trimHistory(stack, stack.length - 1, max)
31
+ return { ...state, ...next, action: undefined }
32
+ }
33
+
34
+ export function trimHistory(stack: string[], index: number, max = MAX_TITLEBAR_HISTORY) {
35
+ if (stack.length <= max) return { stack, index }
36
+ const cut = stack.length - max
37
+ return {
38
+ stack: stack.slice(cut),
39
+ index: Math.max(0, index - cut),
40
+ }
41
+ }
42
+
43
+ export function backPath(state: TitlebarHistory) {
44
+ if (state.index <= 0) return
45
+ const index = state.index - 1
46
+ const to = state.stack[index]
47
+ if (!to) return
48
+ return { state: { ...state, index, action: "back" as const }, to }
49
+ }
50
+
51
+ export function forwardPath(state: TitlebarHistory) {
52
+ if (state.index >= state.stack.length - 1) return
53
+ const index = state.index + 1
54
+ const to = state.stack[index]
55
+ if (!to) return
56
+ return { state: { ...state, index, action: "forward" as const }, to }
57
+ }
@@ -0,0 +1,312 @@
1
+ import { createEffect, createMemo, onCleanup, Show, untrack } from "solid-js"
2
+ import { createStore } from "solid-js/store"
3
+ import { useLocation, useNavigate, useParams } from "@solidjs/router"
4
+ import { IconButton } from "@reign-labs/ui/icon-button"
5
+ import { Icon } from "@reign-labs/ui/icon"
6
+ import { Button } from "@reign-labs/ui/button"
7
+ import { Tooltip, TooltipKeybind } from "@reign-labs/ui/tooltip"
8
+ import { useTheme } from "@reign-labs/ui/theme"
9
+
10
+ import { useLayout } from "@/context/layout"
11
+ import { usePlatform } from "@/context/platform"
12
+ import { useCommand } from "@/context/command"
13
+ import { useLanguage } from "@/context/language"
14
+ import { applyPath, backPath, forwardPath } from "./titlebar-history"
15
+
16
+ type TauriDesktopWindow = {
17
+ startDragging?: () => Promise<void>
18
+ toggleMaximize?: () => Promise<void>
19
+ }
20
+
21
+ type TauriThemeWindow = {
22
+ setTheme?: (theme?: "light" | "dark" | null) => Promise<void>
23
+ }
24
+
25
+ type TauriApi = {
26
+ window?: {
27
+ getCurrentWindow?: () => TauriDesktopWindow
28
+ }
29
+ webviewWindow?: {
30
+ getCurrentWebviewWindow?: () => TauriThemeWindow
31
+ }
32
+ }
33
+
34
+ const tauriApi = () => (window as unknown as { __TAURI__?: TauriApi }).__TAURI__
35
+ const currentDesktopWindow = () => tauriApi()?.window?.getCurrentWindow?.()
36
+ const currentThemeWindow = () => tauriApi()?.webviewWindow?.getCurrentWebviewWindow?.()
37
+
38
+ export function Titlebar() {
39
+ const layout = useLayout()
40
+ const platform = usePlatform()
41
+ const command = useCommand()
42
+ const language = useLanguage()
43
+ const theme = useTheme()
44
+ const navigate = useNavigate()
45
+ const location = useLocation()
46
+ const params = useParams()
47
+
48
+ const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
49
+ const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
50
+ const web = createMemo(() => platform.platform === "web")
51
+ const zoom = () => platform.webviewZoom?.() ?? 1
52
+ const minHeight = () => (mac() ? `${40 / zoom()}px` : undefined)
53
+
54
+ const [history, setHistory] = createStore({
55
+ stack: [] as string[],
56
+ index: 0,
57
+ action: undefined as "back" | "forward" | undefined,
58
+ })
59
+
60
+ const path = () => `${location.pathname}${location.search}${location.hash}`
61
+ const creating = createMemo(() => {
62
+ if (!params.dir) return false
63
+ if (params.id) return false
64
+ const parts = location.pathname.replace(/\/+$/, "").split("/")
65
+ return parts.at(-1) === "session"
66
+ })
67
+
68
+ createEffect(() => {
69
+ const current = path()
70
+
71
+ untrack(() => {
72
+ const next = applyPath(history, current)
73
+ if (next === history) return
74
+ setHistory(next)
75
+ })
76
+ })
77
+
78
+ const canBack = createMemo(() => history.index > 0)
79
+ const canForward = createMemo(() => history.index < history.stack.length - 1)
80
+ const hasProjects = createMemo(() => layout.projects.list().length > 0)
81
+
82
+ const back = () => {
83
+ const next = backPath(history)
84
+ if (!next) return
85
+ setHistory(next.state)
86
+ navigate(next.to)
87
+ }
88
+
89
+ const forward = () => {
90
+ const next = forwardPath(history)
91
+ if (!next) return
92
+ setHistory(next.state)
93
+ navigate(next.to)
94
+ }
95
+
96
+ command.register(() => [
97
+ {
98
+ id: "common.goBack",
99
+ title: language.t("common.goBack"),
100
+ category: language.t("command.category.view"),
101
+ keybind: "mod+[",
102
+ onSelect: back,
103
+ },
104
+ {
105
+ id: "common.goForward",
106
+ title: language.t("common.goForward"),
107
+ category: language.t("command.category.view"),
108
+ keybind: "mod+]",
109
+ onSelect: forward,
110
+ },
111
+ ])
112
+
113
+ const getWin = () => {
114
+ if (platform.platform !== "desktop") return
115
+ return currentDesktopWindow()
116
+ }
117
+
118
+ createEffect(() => {
119
+ if (platform.platform !== "desktop") return
120
+
121
+ const scheme = theme.colorScheme()
122
+ const value = scheme === "system" ? null : scheme
123
+
124
+ const win = currentThemeWindow()
125
+ if (!win?.setTheme) return
126
+
127
+ void win.setTheme(value).catch(() => undefined)
128
+ })
129
+
130
+ const interactive = (target: EventTarget | null) => {
131
+ if (!(target instanceof Element)) return false
132
+
133
+ const selector =
134
+ "button, a, input, textarea, select, option, [role='button'], [role='menuitem'], [contenteditable='true'], [contenteditable='']"
135
+
136
+ return !!target.closest(selector)
137
+ }
138
+
139
+ const drag = (e: MouseEvent) => {
140
+ if (platform.platform !== "desktop") return
141
+ if (e.buttons !== 1) return
142
+ if (interactive(e.target)) return
143
+
144
+ const win = getWin()
145
+ if (!win?.startDragging) return
146
+
147
+ e.preventDefault()
148
+ void win.startDragging().catch(() => undefined)
149
+ }
150
+
151
+ const maximize = (e: MouseEvent) => {
152
+ if (platform.platform !== "desktop") return
153
+ if (interactive(e.target)) return
154
+ if (e.target instanceof Element && e.target.closest("[data-tauri-decorum-tb]")) return
155
+
156
+ const win = getWin()
157
+ if (!win?.toggleMaximize) return
158
+
159
+ e.preventDefault()
160
+ void win.toggleMaximize().catch(() => undefined)
161
+ }
162
+
163
+ return (
164
+ <header
165
+ class="h-10 shrink-0 bg-background-base relative grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center"
166
+ style={{ "min-height": minHeight() }}
167
+ data-tauri-drag-region
168
+ onMouseDown={drag}
169
+ onDblClick={maximize}
170
+ >
171
+ <div
172
+ classList={{
173
+ "flex items-center min-w-0": true,
174
+ "pl-2": !mac(),
175
+ }}
176
+ >
177
+ <Show when={mac()}>
178
+ <div class="h-full shrink-0" style={{ width: `${72 / zoom()}px` }} />
179
+ <div class="xl:hidden w-10 shrink-0 flex items-center justify-center">
180
+ <IconButton
181
+ icon="menu"
182
+ variant="ghost"
183
+ class="titlebar-icon rounded-md"
184
+ onClick={layout.mobileSidebar.toggle}
185
+ aria-label={language.t("sidebar.menu.toggle")}
186
+ aria-expanded={layout.mobileSidebar.opened()}
187
+ />
188
+ </div>
189
+ </Show>
190
+ <Show when={!mac()}>
191
+ <div class="xl:hidden w-[48px] shrink-0 flex items-center justify-center">
192
+ <IconButton
193
+ icon="menu"
194
+ variant="ghost"
195
+ class="titlebar-icon rounded-md"
196
+ onClick={layout.mobileSidebar.toggle}
197
+ aria-label={language.t("sidebar.menu.toggle")}
198
+ aria-expanded={layout.mobileSidebar.opened()}
199
+ />
200
+ </div>
201
+ </Show>
202
+ <div class="flex items-center gap-1 shrink-0">
203
+ <TooltipKeybind
204
+ class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
205
+ placement="bottom"
206
+ title={language.t("command.sidebar.toggle")}
207
+ keybind={command.keybind("sidebar.toggle")}
208
+ >
209
+ <Button
210
+ variant="ghost"
211
+ class="group/sidebar-toggle titlebar-icon w-8 h-6 p-0 box-border"
212
+ onClick={layout.sidebar.toggle}
213
+ aria-label={language.t("command.sidebar.toggle")}
214
+ aria-expanded={layout.sidebar.opened()}
215
+ >
216
+ <Icon size="small" name={layout.sidebar.opened() ? "sidebar-active" : "sidebar"} />
217
+ </Button>
218
+ </TooltipKeybind>
219
+ <div class="hidden xl:flex items-center shrink-0">
220
+ <Show when={params.dir}>
221
+ <div
222
+ class="flex items-center shrink-0 w-8 mr-1"
223
+ aria-hidden={layout.sidebar.opened() ? "true" : undefined}
224
+ >
225
+ <div
226
+ class="transition-opacity"
227
+ classList={{
228
+ "opacity-100 duration-120 ease-out": !layout.sidebar.opened(),
229
+ "opacity-0 duration-120 ease-in delay-0 pointer-events-none": layout.sidebar.opened(),
230
+ }}
231
+ >
232
+ <TooltipKeybind
233
+ placement="bottom"
234
+ title={language.t("command.session.new")}
235
+ keybind={command.keybind("session.new")}
236
+ openDelay={2000}
237
+ >
238
+ <Button
239
+ variant="ghost"
240
+ icon={creating() ? "new-session-active" : "new-session"}
241
+ class="titlebar-icon w-8 h-6 p-0 box-border"
242
+ disabled={layout.sidebar.opened()}
243
+ tabIndex={layout.sidebar.opened() ? -1 : undefined}
244
+ onClick={() => {
245
+ if (!params.dir) return
246
+ navigate(`/${params.dir}/session`)
247
+ }}
248
+ aria-label={language.t("command.session.new")}
249
+ aria-current={creating() ? "page" : undefined}
250
+ />
251
+ </TooltipKeybind>
252
+ </div>
253
+ </div>
254
+ </Show>
255
+ <Show when={hasProjects()}>
256
+ <div
257
+ class="flex items-center gap-0 transition-transform"
258
+ classList={{
259
+ "translate-x-0": !layout.sidebar.opened(),
260
+ "-translate-x-[36px]": layout.sidebar.opened(),
261
+ "duration-180 ease-out": !layout.sidebar.opened(),
262
+ "duration-180 ease-in": layout.sidebar.opened(),
263
+ }}
264
+ >
265
+ <Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
266
+ <Button
267
+ variant="ghost"
268
+ icon="chevron-left"
269
+ class="titlebar-icon w-6 h-6 p-0 box-border"
270
+ disabled={!canBack()}
271
+ onClick={back}
272
+ aria-label={language.t("common.goBack")}
273
+ />
274
+ </Tooltip>
275
+ <Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
276
+ <Button
277
+ variant="ghost"
278
+ icon="chevron-right"
279
+ class="titlebar-icon w-6 h-6 p-0 box-border"
280
+ disabled={!canForward()}
281
+ onClick={forward}
282
+ aria-label={language.t("common.goForward")}
283
+ />
284
+ </Tooltip>
285
+ </div>
286
+ </Show>
287
+ </div>
288
+ </div>
289
+ <div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
290
+ </div>
291
+
292
+ <div class="min-w-0 flex items-center justify-center pointer-events-none">
293
+ <div id="opencode-titlebar-center" class="pointer-events-auto min-w-0 flex justify-center w-fit max-w-full" />
294
+ </div>
295
+
296
+ <div
297
+ classList={{
298
+ "flex items-center min-w-0 justify-end": true,
299
+ "pr-2": !windows(),
300
+ }}
301
+ data-tauri-drag-region
302
+ onMouseDown={drag}
303
+ >
304
+ <div id="opencode-titlebar-right" class="flex items-center gap-1 shrink-0 justify-end" />
305
+ <Show when={windows()}>
306
+ {!tauriApi() && <div class="w-36 shrink-0" />}
307
+ <div data-tauri-decorum-tb class="flex flex-row" />
308
+ </Show>
309
+ </div>
310
+ </header>
311
+ )
312
+ }
@@ -0,0 +1,89 @@
1
+ export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
2
+
3
+ export const ACCEPTED_FILE_TYPES = [
4
+ ...ACCEPTED_IMAGE_TYPES,
5
+ "application/pdf",
6
+ "text/*",
7
+ "application/json",
8
+ "application/ld+json",
9
+ "application/toml",
10
+ "application/x-toml",
11
+ "application/x-yaml",
12
+ "application/xml",
13
+ "application/yaml",
14
+ ".c",
15
+ ".cc",
16
+ ".cjs",
17
+ ".conf",
18
+ ".cpp",
19
+ ".css",
20
+ ".csv",
21
+ ".cts",
22
+ ".env",
23
+ ".go",
24
+ ".gql",
25
+ ".graphql",
26
+ ".h",
27
+ ".hh",
28
+ ".hpp",
29
+ ".htm",
30
+ ".html",
31
+ ".ini",
32
+ ".java",
33
+ ".js",
34
+ ".json",
35
+ ".jsx",
36
+ ".log",
37
+ ".md",
38
+ ".mdx",
39
+ ".mjs",
40
+ ".mts",
41
+ ".py",
42
+ ".rb",
43
+ ".rs",
44
+ ".sass",
45
+ ".scss",
46
+ ".sh",
47
+ ".sql",
48
+ ".toml",
49
+ ".ts",
50
+ ".tsx",
51
+ ".txt",
52
+ ".xml",
53
+ ".yaml",
54
+ ".yml",
55
+ ".zsh",
56
+ ]
57
+
58
+ const MIME_EXT = new Map([
59
+ ["image/png", "png"],
60
+ ["image/jpeg", "jpg"],
61
+ ["image/gif", "gif"],
62
+ ["image/webp", "webp"],
63
+ ["application/pdf", "pdf"],
64
+ ["application/json", "json"],
65
+ ["application/ld+json", "jsonld"],
66
+ ["application/toml", "toml"],
67
+ ["application/x-toml", "toml"],
68
+ ["application/x-yaml", "yaml"],
69
+ ["application/xml", "xml"],
70
+ ["application/yaml", "yaml"],
71
+ ])
72
+
73
+ const TEXT_EXT = ["txt", "text", "md", "markdown", "log", "csv"]
74
+
75
+ export const ACCEPTED_FILE_EXTENSIONS = Array.from(
76
+ new Set(
77
+ ACCEPTED_FILE_TYPES.flatMap((item) => {
78
+ if (item.startsWith(".")) return [item.slice(1)]
79
+ if (item === "text/*") return TEXT_EXT
80
+ const out = MIME_EXT.get(item)
81
+ return out ? [out] : []
82
+ }),
83
+ ),
84
+ ).sort()
85
+
86
+ export function filePickerFilters(ext?: string[]) {
87
+ if (!ext || ext.length === 0) return undefined
88
+ return [{ name: "Files", extensions: ext }]
89
+ }
@@ -0,0 +1,69 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { formatKeybind, matchKeybind, parseKeybind } from "./command"
3
+
4
+ describe("command keybind helpers", () => {
5
+ test("parseKeybind handles aliases and multiple combos", () => {
6
+ const keybinds = parseKeybind("control+option+k, mod+shift+comma")
7
+
8
+ expect(keybinds).toHaveLength(2)
9
+ expect(keybinds[0]).toEqual({
10
+ key: "k",
11
+ ctrl: true,
12
+ meta: false,
13
+ shift: false,
14
+ alt: true,
15
+ })
16
+ expect(keybinds[1]?.shift).toBe(true)
17
+ expect(keybinds[1]?.key).toBe("comma")
18
+ expect(Boolean(keybinds[1]?.ctrl || keybinds[1]?.meta)).toBe(true)
19
+ })
20
+
21
+ test("parseKeybind treats none and empty as disabled", () => {
22
+ expect(parseKeybind("none")).toEqual([])
23
+ expect(parseKeybind("")).toEqual([])
24
+ })
25
+
26
+ test("matchKeybind normalizes punctuation keys", () => {
27
+ const keybinds = parseKeybind("ctrl+comma, shift+plus, meta+space")
28
+
29
+ expect(matchKeybind(keybinds, new KeyboardEvent("keydown", { key: ",", ctrlKey: true }))).toBe(true)
30
+ expect(matchKeybind(keybinds, new KeyboardEvent("keydown", { key: "+", shiftKey: true }))).toBe(true)
31
+ expect(matchKeybind(keybinds, new KeyboardEvent("keydown", { key: " ", metaKey: true }))).toBe(true)
32
+ expect(matchKeybind(keybinds, new KeyboardEvent("keydown", { key: ",", ctrlKey: true, altKey: true }))).toBe(false)
33
+ })
34
+
35
+ test("matchKeybind supports bracket keys", () => {
36
+ const keybinds = parseKeybind("mod+alt+[, mod+alt+]")
37
+ const prev = keybinds[0]
38
+ const next = keybinds[1]
39
+
40
+ expect(
41
+ matchKeybind(
42
+ keybinds,
43
+ new KeyboardEvent("keydown", { key: "[", ctrlKey: prev?.ctrl, metaKey: prev?.meta, altKey: true }),
44
+ ),
45
+ ).toBe(true)
46
+ expect(
47
+ matchKeybind(
48
+ keybinds,
49
+ new KeyboardEvent("keydown", { key: "]", ctrlKey: next?.ctrl, metaKey: next?.meta, altKey: true }),
50
+ ),
51
+ ).toBe(true)
52
+ })
53
+
54
+ test("formatKeybind returns human readable output", () => {
55
+ const display = formatKeybind("ctrl+alt+arrowup")
56
+
57
+ expect(display).toContain("↑")
58
+ expect(display.includes("Ctrl") || display.includes("⌃")).toBe(true)
59
+ expect(display.includes("Alt") || display.includes("⌥")).toBe(true)
60
+ expect(formatKeybind("none")).toBe("")
61
+ })
62
+
63
+ test("formatKeybind prefers the first combo", () => {
64
+ const display = formatKeybind("mod+k,mod+p")
65
+
66
+ expect(display.includes("K") || display.includes("k")).toBe(true)
67
+ expect(display.includes("P") || display.includes("p")).toBe(false)
68
+ })
69
+ })
@@ -0,0 +1,25 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { upsertCommandRegistration } from "./command"
3
+
4
+ describe("upsertCommandRegistration", () => {
5
+ test("replaces keyed registrations", () => {
6
+ const one = () => [{ id: "one", title: "One" }]
7
+ const two = () => [{ id: "two", title: "Two" }]
8
+
9
+ const next = upsertCommandRegistration([{ key: "layout", options: one }], { key: "layout", options: two })
10
+
11
+ expect(next).toHaveLength(1)
12
+ expect(next[0]?.options).toBe(two)
13
+ })
14
+
15
+ test("keeps unkeyed registrations additive", () => {
16
+ const one = () => [{ id: "one", title: "One" }]
17
+ const two = () => [{ id: "two", title: "Two" }]
18
+
19
+ const next = upsertCommandRegistration([{ options: one }], { options: two })
20
+
21
+ expect(next).toHaveLength(2)
22
+ expect(next[0]?.options).toBe(two)
23
+ expect(next[1]?.options).toBe(one)
24
+ })
25
+ })