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,91 @@
1
+ import { usePlatform } from "@/context/platform"
2
+ import type { ServerConnection } from "@/context/server"
3
+ import { createSdkForServer } from "./server"
4
+
5
+ export type ServerHealth = { healthy: boolean; version?: string }
6
+
7
+ interface CheckServerHealthOptions {
8
+ timeoutMs?: number
9
+ signal?: AbortSignal
10
+ retryCount?: number
11
+ retryDelayMs?: number
12
+ }
13
+
14
+ const defaultTimeoutMs = 3000
15
+ const defaultRetryCount = 2
16
+ const defaultRetryDelayMs = 100
17
+
18
+ function timeoutSignal(timeoutMs: number) {
19
+ const timeout = (AbortSignal as unknown as { timeout?: (ms: number) => AbortSignal }).timeout
20
+ if (timeout) {
21
+ try {
22
+ return {
23
+ signal: timeout.call(AbortSignal, timeoutMs),
24
+ clear: undefined as (() => void) | undefined,
25
+ }
26
+ } catch {}
27
+ }
28
+ const controller = new AbortController()
29
+ const timer = setTimeout(() => controller.abort(), timeoutMs)
30
+ return { signal: controller.signal, clear: () => clearTimeout(timer) }
31
+ }
32
+
33
+ function wait(ms: number, signal?: AbortSignal) {
34
+ return new Promise<void>((resolve, reject) => {
35
+ if (signal?.aborted) {
36
+ reject(new DOMException("Aborted", "AbortError"))
37
+ return
38
+ }
39
+ const timer = setTimeout(() => {
40
+ signal?.removeEventListener("abort", onAbort)
41
+ resolve()
42
+ }, ms)
43
+ const onAbort = () => {
44
+ clearTimeout(timer)
45
+ reject(new DOMException("Aborted", "AbortError"))
46
+ }
47
+ signal?.addEventListener("abort", onAbort, { once: true })
48
+ })
49
+ }
50
+
51
+ function retryable(error: unknown, signal?: AbortSignal) {
52
+ if (signal?.aborted) return false
53
+ if (!(error instanceof Error)) return false
54
+ if (error.name === "AbortError" || error.name === "TimeoutError") return false
55
+ if (error instanceof TypeError) return true
56
+ return /network|fetch|econnreset|econnrefused|enotfound|timedout/i.test(error.message)
57
+ }
58
+
59
+ export async function checkServerHealth(
60
+ server: ServerConnection.HttpBase,
61
+ fetch: typeof globalThis.fetch,
62
+ opts?: CheckServerHealthOptions,
63
+ ): Promise<ServerHealth> {
64
+ const timeout = opts?.signal ? undefined : timeoutSignal(opts?.timeoutMs ?? defaultTimeoutMs)
65
+ const signal = opts?.signal ?? timeout?.signal
66
+ const retryCount = opts?.retryCount ?? defaultRetryCount
67
+ const retryDelayMs = opts?.retryDelayMs ?? defaultRetryDelayMs
68
+ const next = (count: number, error: unknown) => {
69
+ if (count >= retryCount || !retryable(error, signal)) return Promise.resolve({ healthy: false } as const)
70
+ return wait(retryDelayMs * (count + 1), signal)
71
+ .then(() => attempt(count + 1))
72
+ .catch(() => ({ healthy: false }))
73
+ }
74
+ const attempt = (count: number): Promise<ServerHealth> =>
75
+ createSdkForServer({
76
+ server,
77
+ fetch,
78
+ signal,
79
+ })
80
+ .global.health()
81
+ .then((x) => (x.error ? next(count, x.error) : { healthy: x.data?.healthy === true, version: x.data?.version }))
82
+ .catch((error) => next(count, error))
83
+ return attempt(0).finally(() => timeout?.clear?.())
84
+ }
85
+
86
+ export function useCheckServerHealth() {
87
+ const platform = usePlatform()
88
+ const fetcher = platform.fetch ?? globalThis.fetch
89
+
90
+ return (http: ServerConnection.HttpBase) => checkServerHealth(http, fetcher)
91
+ }
@@ -0,0 +1,22 @@
1
+ import { createOpencodeClient } from "@reign-labs/sdk/v2/client"
2
+ import type { ServerConnection } from "@/context/server"
3
+
4
+ export function createSdkForServer({
5
+ server,
6
+ ...config
7
+ }: Omit<NonNullable<Parameters<typeof createOpencodeClient>[0]>, "baseUrl"> & {
8
+ server: ServerConnection.HttpBase
9
+ }) {
10
+ const auth = (() => {
11
+ if (!server.password) return
12
+ return {
13
+ Authorization: `Basic ${btoa(`${server.username ?? "opencode"}:${server.password}`)}`,
14
+ }
15
+ })()
16
+
17
+ return createOpencodeClient({
18
+ ...config,
19
+ headers: { ...config.headers, ...auth },
20
+ baseUrl: server.url,
21
+ })
22
+ }
@@ -0,0 +1,49 @@
1
+ import { useDragDropContext } from "@thisbeyond/solid-dnd"
2
+ import type { Transformer } from "@thisbeyond/solid-dnd"
3
+ import { createRoot, onCleanup, type JSXElement } from "solid-js"
4
+
5
+ type DragEvent = { draggable?: { id?: unknown } }
6
+
7
+ const isDragEvent = (event: unknown): event is DragEvent => {
8
+ if (typeof event !== "object" || event === null) return false
9
+ return "draggable" in event
10
+ }
11
+
12
+ export const getDraggableId = (event: unknown): string | undefined => {
13
+ if (!isDragEvent(event)) return undefined
14
+ const draggable = event.draggable
15
+ if (!draggable) return undefined
16
+ return typeof draggable.id === "string" ? draggable.id : undefined
17
+ }
18
+
19
+ const createTransformer = (id: string, axis: "x" | "y"): Transformer => ({
20
+ id,
21
+ order: 100,
22
+ callback: (transform) => (axis === "x" ? { ...transform, x: 0 } : { ...transform, y: 0 }),
23
+ })
24
+
25
+ const createAxisConstraint = (axis: "x" | "y", transformerId: string) => (): JSXElement => {
26
+ const context = useDragDropContext()
27
+ if (!context) return null
28
+ const [, { onDragStart, onDragEnd, addTransformer, removeTransformer }] = context
29
+ const transformer = createTransformer(transformerId, axis)
30
+ const dispose = createRoot((dispose) => {
31
+ onDragStart((event) => {
32
+ const id = getDraggableId(event)
33
+ if (!id) return
34
+ addTransformer("draggables", id, transformer)
35
+ })
36
+ onDragEnd((event) => {
37
+ const id = getDraggableId(event)
38
+ if (!id) return
39
+ removeTransformer("draggables", id, transformer.id)
40
+ })
41
+ return dispose
42
+ })
43
+ onCleanup(dispose)
44
+ return null
45
+ }
46
+
47
+ export const ConstrainDragXAxis = createAxisConstraint("x", "constrain-x-axis")
48
+
49
+ export const ConstrainDragYAxis = createAxisConstraint("y", "constrain-y-axis")
@@ -0,0 +1,117 @@
1
+ import alert01 from "@reign-labs/ui/audio/alert-01.aac"
2
+ import alert02 from "@reign-labs/ui/audio/alert-02.aac"
3
+ import alert03 from "@reign-labs/ui/audio/alert-03.aac"
4
+ import alert04 from "@reign-labs/ui/audio/alert-04.aac"
5
+ import alert05 from "@reign-labs/ui/audio/alert-05.aac"
6
+ import alert06 from "@reign-labs/ui/audio/alert-06.aac"
7
+ import alert07 from "@reign-labs/ui/audio/alert-07.aac"
8
+ import alert08 from "@reign-labs/ui/audio/alert-08.aac"
9
+ import alert09 from "@reign-labs/ui/audio/alert-09.aac"
10
+ import alert10 from "@reign-labs/ui/audio/alert-10.aac"
11
+ import bipbop01 from "@reign-labs/ui/audio/bip-bop-01.aac"
12
+ import bipbop02 from "@reign-labs/ui/audio/bip-bop-02.aac"
13
+ import bipbop03 from "@reign-labs/ui/audio/bip-bop-03.aac"
14
+ import bipbop04 from "@reign-labs/ui/audio/bip-bop-04.aac"
15
+ import bipbop05 from "@reign-labs/ui/audio/bip-bop-05.aac"
16
+ import bipbop06 from "@reign-labs/ui/audio/bip-bop-06.aac"
17
+ import bipbop07 from "@reign-labs/ui/audio/bip-bop-07.aac"
18
+ import bipbop08 from "@reign-labs/ui/audio/bip-bop-08.aac"
19
+ import bipbop09 from "@reign-labs/ui/audio/bip-bop-09.aac"
20
+ import bipbop10 from "@reign-labs/ui/audio/bip-bop-10.aac"
21
+ import nope01 from "@reign-labs/ui/audio/nope-01.aac"
22
+ import nope02 from "@reign-labs/ui/audio/nope-02.aac"
23
+ import nope03 from "@reign-labs/ui/audio/nope-03.aac"
24
+ import nope04 from "@reign-labs/ui/audio/nope-04.aac"
25
+ import nope05 from "@reign-labs/ui/audio/nope-05.aac"
26
+ import nope06 from "@reign-labs/ui/audio/nope-06.aac"
27
+ import nope07 from "@reign-labs/ui/audio/nope-07.aac"
28
+ import nope08 from "@reign-labs/ui/audio/nope-08.aac"
29
+ import nope09 from "@reign-labs/ui/audio/nope-09.aac"
30
+ import nope10 from "@reign-labs/ui/audio/nope-10.aac"
31
+ import nope11 from "@reign-labs/ui/audio/nope-11.aac"
32
+ import nope12 from "@reign-labs/ui/audio/nope-12.aac"
33
+ import staplebops01 from "@reign-labs/ui/audio/staplebops-01.aac"
34
+ import staplebops02 from "@reign-labs/ui/audio/staplebops-02.aac"
35
+ import staplebops03 from "@reign-labs/ui/audio/staplebops-03.aac"
36
+ import staplebops04 from "@reign-labs/ui/audio/staplebops-04.aac"
37
+ import staplebops05 from "@reign-labs/ui/audio/staplebops-05.aac"
38
+ import staplebops06 from "@reign-labs/ui/audio/staplebops-06.aac"
39
+ import staplebops07 from "@reign-labs/ui/audio/staplebops-07.aac"
40
+ import yup01 from "@reign-labs/ui/audio/yup-01.aac"
41
+ import yup02 from "@reign-labs/ui/audio/yup-02.aac"
42
+ import yup03 from "@reign-labs/ui/audio/yup-03.aac"
43
+ import yup04 from "@reign-labs/ui/audio/yup-04.aac"
44
+ import yup05 from "@reign-labs/ui/audio/yup-05.aac"
45
+ import yup06 from "@reign-labs/ui/audio/yup-06.aac"
46
+
47
+ export const SOUND_OPTIONS = [
48
+ { id: "alert-01", label: "sound.option.alert01", src: alert01 },
49
+ { id: "alert-02", label: "sound.option.alert02", src: alert02 },
50
+ { id: "alert-03", label: "sound.option.alert03", src: alert03 },
51
+ { id: "alert-04", label: "sound.option.alert04", src: alert04 },
52
+ { id: "alert-05", label: "sound.option.alert05", src: alert05 },
53
+ { id: "alert-06", label: "sound.option.alert06", src: alert06 },
54
+ { id: "alert-07", label: "sound.option.alert07", src: alert07 },
55
+ { id: "alert-08", label: "sound.option.alert08", src: alert08 },
56
+ { id: "alert-09", label: "sound.option.alert09", src: alert09 },
57
+ { id: "alert-10", label: "sound.option.alert10", src: alert10 },
58
+ { id: "bip-bop-01", label: "sound.option.bipbop01", src: bipbop01 },
59
+ { id: "bip-bop-02", label: "sound.option.bipbop02", src: bipbop02 },
60
+ { id: "bip-bop-03", label: "sound.option.bipbop03", src: bipbop03 },
61
+ { id: "bip-bop-04", label: "sound.option.bipbop04", src: bipbop04 },
62
+ { id: "bip-bop-05", label: "sound.option.bipbop05", src: bipbop05 },
63
+ { id: "bip-bop-06", label: "sound.option.bipbop06", src: bipbop06 },
64
+ { id: "bip-bop-07", label: "sound.option.bipbop07", src: bipbop07 },
65
+ { id: "bip-bop-08", label: "sound.option.bipbop08", src: bipbop08 },
66
+ { id: "bip-bop-09", label: "sound.option.bipbop09", src: bipbop09 },
67
+ { id: "bip-bop-10", label: "sound.option.bipbop10", src: bipbop10 },
68
+ { id: "staplebops-01", label: "sound.option.staplebops01", src: staplebops01 },
69
+ { id: "staplebops-02", label: "sound.option.staplebops02", src: staplebops02 },
70
+ { id: "staplebops-03", label: "sound.option.staplebops03", src: staplebops03 },
71
+ { id: "staplebops-04", label: "sound.option.staplebops04", src: staplebops04 },
72
+ { id: "staplebops-05", label: "sound.option.staplebops05", src: staplebops05 },
73
+ { id: "staplebops-06", label: "sound.option.staplebops06", src: staplebops06 },
74
+ { id: "staplebops-07", label: "sound.option.staplebops07", src: staplebops07 },
75
+ { id: "nope-01", label: "sound.option.nope01", src: nope01 },
76
+ { id: "nope-02", label: "sound.option.nope02", src: nope02 },
77
+ { id: "nope-03", label: "sound.option.nope03", src: nope03 },
78
+ { id: "nope-04", label: "sound.option.nope04", src: nope04 },
79
+ { id: "nope-05", label: "sound.option.nope05", src: nope05 },
80
+ { id: "nope-06", label: "sound.option.nope06", src: nope06 },
81
+ { id: "nope-07", label: "sound.option.nope07", src: nope07 },
82
+ { id: "nope-08", label: "sound.option.nope08", src: nope08 },
83
+ { id: "nope-09", label: "sound.option.nope09", src: nope09 },
84
+ { id: "nope-10", label: "sound.option.nope10", src: nope10 },
85
+ { id: "nope-11", label: "sound.option.nope11", src: nope11 },
86
+ { id: "nope-12", label: "sound.option.nope12", src: nope12 },
87
+ { id: "yup-01", label: "sound.option.yup01", src: yup01 },
88
+ { id: "yup-02", label: "sound.option.yup02", src: yup02 },
89
+ { id: "yup-03", label: "sound.option.yup03", src: yup03 },
90
+ { id: "yup-04", label: "sound.option.yup04", src: yup04 },
91
+ { id: "yup-05", label: "sound.option.yup05", src: yup05 },
92
+ { id: "yup-06", label: "sound.option.yup06", src: yup06 },
93
+ ] as const
94
+
95
+ export type SoundOption = (typeof SOUND_OPTIONS)[number]
96
+ export type SoundID = SoundOption["id"]
97
+
98
+ const soundById = Object.fromEntries(SOUND_OPTIONS.map((s) => [s.id, s.src])) as Record<SoundID, string>
99
+
100
+ export function soundSrc(id: string | undefined) {
101
+ if (!id) return
102
+ if (!(id in soundById)) return
103
+ return soundById[id as SoundID]
104
+ }
105
+
106
+ export function playSound(src: string | undefined) {
107
+ if (typeof Audio === "undefined") return
108
+ if (!src) return
109
+ const audio = new Audio(src)
110
+ audio.play().catch(() => undefined)
111
+
112
+ // Return a cleanup function to pause the sound.
113
+ return () => {
114
+ audio.pause()
115
+ audio.currentTime = 0
116
+ }
117
+ }
@@ -0,0 +1,64 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { terminalWriter } from "./terminal-writer"
3
+
4
+ describe("terminalWriter", () => {
5
+ test("buffers and flushes once per schedule", () => {
6
+ const calls: string[] = []
7
+ const scheduled: VoidFunction[] = []
8
+ const writer = terminalWriter(
9
+ (data, done) => {
10
+ calls.push(data)
11
+ done?.()
12
+ },
13
+ (flush) => scheduled.push(flush),
14
+ )
15
+
16
+ writer.push("a")
17
+ writer.push("b")
18
+ writer.push("c")
19
+
20
+ expect(calls).toEqual([])
21
+ expect(scheduled).toHaveLength(1)
22
+
23
+ scheduled[0]?.()
24
+ expect(calls).toEqual(["abc"])
25
+ })
26
+
27
+ test("flush is a no-op when empty", () => {
28
+ const calls: string[] = []
29
+ const writer = terminalWriter(
30
+ (data, done) => {
31
+ calls.push(data)
32
+ done?.()
33
+ },
34
+ (flush) => flush(),
35
+ )
36
+ writer.flush()
37
+ expect(calls).toEqual([])
38
+ })
39
+
40
+ test("flush waits for pending write completion", () => {
41
+ const calls: string[] = []
42
+ let done: VoidFunction | undefined
43
+ const writer = terminalWriter(
44
+ (data, finish) => {
45
+ calls.push(data)
46
+ done = finish
47
+ },
48
+ (flush) => flush(),
49
+ )
50
+
51
+ writer.push("a")
52
+
53
+ let settled = false
54
+ writer.flush(() => {
55
+ settled = true
56
+ })
57
+
58
+ expect(calls).toEqual(["a"])
59
+ expect(settled).toBe(false)
60
+
61
+ done?.()
62
+ expect(settled).toBe(true)
63
+ })
64
+ })
@@ -0,0 +1,65 @@
1
+ export function terminalWriter(
2
+ write: (data: string, done?: VoidFunction) => void,
3
+ schedule: (flush: VoidFunction) => void = queueMicrotask,
4
+ ) {
5
+ let chunks: string[] | undefined
6
+ let waits: VoidFunction[] | undefined
7
+ let scheduled = false
8
+ let writing = false
9
+
10
+ const settle = () => {
11
+ if (scheduled || writing || chunks?.length) return
12
+ const list = waits
13
+ if (!list?.length) return
14
+ waits = undefined
15
+ for (const fn of list) {
16
+ fn()
17
+ }
18
+ }
19
+
20
+ const run = () => {
21
+ if (writing) return
22
+ scheduled = false
23
+ const items = chunks
24
+ if (!items?.length) {
25
+ settle()
26
+ return
27
+ }
28
+ chunks = undefined
29
+ writing = true
30
+ write(items.join(""), () => {
31
+ writing = false
32
+ if (chunks?.length) {
33
+ if (scheduled) return
34
+ scheduled = true
35
+ schedule(run)
36
+ return
37
+ }
38
+ settle()
39
+ })
40
+ }
41
+
42
+ const push = (data: string) => {
43
+ if (!data) return
44
+ if (chunks) chunks.push(data)
45
+ else chunks = [data]
46
+
47
+ if (scheduled || writing) return
48
+ scheduled = true
49
+ schedule(run)
50
+ }
51
+
52
+ const flush = (done?: VoidFunction) => {
53
+ if (!scheduled && !writing && !chunks?.length) {
54
+ done?.()
55
+ return
56
+ }
57
+ if (done) {
58
+ if (waits) waits.push(done)
59
+ else waits = [done]
60
+ }
61
+ run()
62
+ }
63
+
64
+ return { push, flush }
65
+ }
@@ -0,0 +1,22 @@
1
+ type TimeKey =
2
+ | "common.time.justNow"
3
+ | "common.time.minutesAgo.short"
4
+ | "common.time.hoursAgo.short"
5
+ | "common.time.daysAgo.short"
6
+
7
+ type Translate = (key: TimeKey, params?: Record<string, string | number>) => string
8
+
9
+ export function getRelativeTime(dateString: string, t: Translate): string {
10
+ const date = new Date(dateString)
11
+ const now = new Date()
12
+ const diffMs = now.getTime() - date.getTime()
13
+ const diffSeconds = Math.floor(diffMs / 1000)
14
+ const diffMinutes = Math.floor(diffSeconds / 60)
15
+ const diffHours = Math.floor(diffMinutes / 60)
16
+ const diffDays = Math.floor(diffHours / 24)
17
+
18
+ if (diffSeconds < 60) return t("common.time.justNow")
19
+ if (diffMinutes < 60) return t("common.time.minutesAgo.short", { count: diffMinutes })
20
+ if (diffHours < 24) return t("common.time.hoursAgo.short", { count: diffHours })
21
+ return t("common.time.daysAgo.short", { count: diffDays })
22
+ }
@@ -0,0 +1,78 @@
1
+ import { afterEach, describe, expect, test } from "bun:test"
2
+ import { uuid } from "./uuid"
3
+
4
+ const cryptoDescriptor = Object.getOwnPropertyDescriptor(globalThis, "crypto")
5
+ const secureDescriptor = Object.getOwnPropertyDescriptor(globalThis, "isSecureContext")
6
+ const randomDescriptor = Object.getOwnPropertyDescriptor(Math, "random")
7
+
8
+ const setCrypto = (value: Partial<Crypto>) => {
9
+ Object.defineProperty(globalThis, "crypto", {
10
+ configurable: true,
11
+ value: value as Crypto,
12
+ })
13
+ }
14
+
15
+ const setSecure = (value: boolean) => {
16
+ Object.defineProperty(globalThis, "isSecureContext", {
17
+ configurable: true,
18
+ value,
19
+ })
20
+ }
21
+
22
+ const setRandom = (value: () => number) => {
23
+ Object.defineProperty(Math, "random", {
24
+ configurable: true,
25
+ value,
26
+ })
27
+ }
28
+
29
+ afterEach(() => {
30
+ if (cryptoDescriptor) {
31
+ Object.defineProperty(globalThis, "crypto", cryptoDescriptor)
32
+ }
33
+
34
+ if (secureDescriptor) {
35
+ Object.defineProperty(globalThis, "isSecureContext", secureDescriptor)
36
+ }
37
+
38
+ if (!secureDescriptor) {
39
+ delete (globalThis as { isSecureContext?: boolean }).isSecureContext
40
+ }
41
+
42
+ if (randomDescriptor) {
43
+ Object.defineProperty(Math, "random", randomDescriptor)
44
+ }
45
+ })
46
+
47
+ describe("uuid", () => {
48
+ test("uses randomUUID in secure contexts", () => {
49
+ setCrypto({ randomUUID: () => "00000000-0000-0000-0000-000000000000" })
50
+ setSecure(true)
51
+ expect(uuid()).toBe("00000000-0000-0000-0000-000000000000")
52
+ })
53
+
54
+ test("falls back in insecure contexts", () => {
55
+ setCrypto({ randomUUID: () => "00000000-0000-0000-0000-000000000000" })
56
+ setSecure(false)
57
+ setRandom(() => 0.5)
58
+ expect(uuid()).toBe("8")
59
+ })
60
+
61
+ test("falls back when randomUUID throws", () => {
62
+ setCrypto({
63
+ randomUUID: () => {
64
+ throw new DOMException("Failed", "OperationError")
65
+ },
66
+ })
67
+ setSecure(true)
68
+ setRandom(() => 0.5)
69
+ expect(uuid()).toBe("8")
70
+ })
71
+
72
+ test("falls back when randomUUID is unavailable", () => {
73
+ setCrypto({})
74
+ setSecure(true)
75
+ setRandom(() => 0.5)
76
+ expect(uuid()).toBe("8")
77
+ })
78
+ })
@@ -0,0 +1,12 @@
1
+ const fallback = () => Math.random().toString(16).slice(2)
2
+
3
+ export function uuid() {
4
+ const c = globalThis.crypto
5
+ if (!c || typeof c.randomUUID !== "function") return fallback()
6
+ if (typeof globalThis.isSecureContext === "boolean" && !globalThis.isSecureContext) return fallback()
7
+ try {
8
+ return c.randomUUID()
9
+ } catch {
10
+ return fallback()
11
+ }
12
+ }
@@ -0,0 +1,46 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { Worktree } from "./worktree"
3
+
4
+ const dir = (name: string) => `/tmp/opencode-worktree-${name}-${crypto.randomUUID()}`
5
+
6
+ describe("Worktree", () => {
7
+ test("normalizes trailing slashes", () => {
8
+ const key = dir("normalize")
9
+ Worktree.ready(`${key}/`)
10
+
11
+ expect(Worktree.get(key)).toEqual({ status: "ready" })
12
+ })
13
+
14
+ test("pending does not overwrite a terminal state", () => {
15
+ const key = dir("pending")
16
+ Worktree.failed(key, "boom")
17
+ Worktree.pending(key)
18
+
19
+ expect(Worktree.get(key)).toEqual({ status: "failed", message: "boom" })
20
+ })
21
+
22
+ test("wait resolves shared pending waiter when ready", async () => {
23
+ const key = dir("wait-ready")
24
+ Worktree.pending(key)
25
+
26
+ const a = Worktree.wait(key)
27
+ const b = Worktree.wait(`${key}/`)
28
+
29
+ expect(a).toBe(b)
30
+
31
+ Worktree.ready(key)
32
+
33
+ expect(await a).toEqual({ status: "ready" })
34
+ expect(await b).toEqual({ status: "ready" })
35
+ })
36
+
37
+ test("wait resolves with failure message", async () => {
38
+ const key = dir("wait-failed")
39
+ const waiting = Worktree.wait(key)
40
+
41
+ Worktree.failed(key, "permission denied")
42
+
43
+ expect(await waiting).toEqual({ status: "failed", message: "permission denied" })
44
+ expect(await Worktree.wait(key)).toEqual({ status: "failed", message: "permission denied" })
45
+ })
46
+ })
@@ -0,0 +1,73 @@
1
+ const normalize = (directory: string) => directory.replace(/[\\/]+$/, "")
2
+
3
+ type State =
4
+ | {
5
+ status: "pending"
6
+ }
7
+ | {
8
+ status: "ready"
9
+ }
10
+ | {
11
+ status: "failed"
12
+ message: string
13
+ }
14
+
15
+ const state = new Map<string, State>()
16
+ const waiters = new Map<
17
+ string,
18
+ {
19
+ promise: Promise<State>
20
+ resolve: (state: State) => void
21
+ }
22
+ >()
23
+
24
+ function deferred() {
25
+ const box = { resolve: (_: State) => {} }
26
+ const promise = new Promise<State>((resolve) => {
27
+ box.resolve = resolve
28
+ })
29
+ return { promise, resolve: box.resolve }
30
+ }
31
+
32
+ export const Worktree = {
33
+ get(directory: string) {
34
+ return state.get(normalize(directory))
35
+ },
36
+ pending(directory: string) {
37
+ const key = normalize(directory)
38
+ const current = state.get(key)
39
+ if (current && current.status !== "pending") return
40
+ state.set(key, { status: "pending" })
41
+ },
42
+ ready(directory: string) {
43
+ const key = normalize(directory)
44
+ const next = { status: "ready" } as const
45
+ state.set(key, next)
46
+ const waiter = waiters.get(key)
47
+ if (!waiter) return
48
+ waiters.delete(key)
49
+ waiter.resolve(next)
50
+ },
51
+ failed(directory: string, message: string) {
52
+ const key = normalize(directory)
53
+ const next = { status: "failed", message } as const
54
+ state.set(key, next)
55
+ const waiter = waiters.get(key)
56
+ if (!waiter) return
57
+ waiters.delete(key)
58
+ waiter.resolve(next)
59
+ },
60
+ wait(directory: string) {
61
+ const key = normalize(directory)
62
+ const current = state.get(key)
63
+ if (current && current.status !== "pending") return Promise.resolve(current)
64
+
65
+ const existing = waiters.get(key)
66
+ if (existing) return existing.promise
67
+
68
+ const waiter = deferred()
69
+
70
+ waiters.set(key, waiter)
71
+ return waiter.promise
72
+ },
73
+ }
package/sst-env.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /* This file is auto-generated by SST. Do not edit. */
2
+ /* tslint:disable */
3
+ /* eslint-disable */
4
+ /* deno-fmt-ignore-file */
5
+ /* biome-ignore-all lint: auto-generated */
6
+
7
+ /// <reference path="../../sst-env.d.ts" />
8
+
9
+ import "sst"
10
+ export {}
package/tsconfig.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "compilerOptions": {
4
+ "composite": true,
5
+ "target": "ESNext",
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowSyntheticDefaultImports": true,
10
+ "esModuleInterop": true,
11
+ "jsx": "preserve",
12
+ "jsxImportSource": "solid-js",
13
+ "allowJs": true,
14
+ "resolveJsonModule": true,
15
+ "strict": true,
16
+ "noEmit": false,
17
+ "emitDeclarationOnly": true,
18
+ "outDir": "node_modules/.ts-dist",
19
+ "isolatedModules": true,
20
+ "paths": {
21
+ "@/*": ["./src/*"]
22
+ }
23
+ },
24
+ "include": ["src", "package.json"],
25
+ "exclude": ["dist", "ts-dist"]
26
+ }