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,82 @@
1
+ import { beforeAll, describe, expect, mock, test } from "bun:test"
2
+
3
+ let getWorkspaceTerminalCacheKey: (dir: string) => string
4
+ let getLegacyTerminalStorageKeys: (dir: string, legacySessionID?: string) => string[]
5
+ let migrateTerminalState: (value: unknown) => unknown
6
+
7
+ beforeAll(async () => {
8
+ mock.module("@solidjs/router", () => ({
9
+ useNavigate: () => () => undefined,
10
+ useParams: () => ({}),
11
+ }))
12
+ mock.module("@reign-labs/ui/context", () => ({
13
+ createSimpleContext: () => ({
14
+ use: () => undefined,
15
+ provider: () => undefined,
16
+ }),
17
+ }))
18
+ const mod = await import("./terminal")
19
+ getWorkspaceTerminalCacheKey = mod.getWorkspaceTerminalCacheKey
20
+ getLegacyTerminalStorageKeys = mod.getLegacyTerminalStorageKeys
21
+ migrateTerminalState = mod.migrateTerminalState
22
+ })
23
+
24
+ describe("getWorkspaceTerminalCacheKey", () => {
25
+ test("uses workspace-only directory cache key", () => {
26
+ expect(getWorkspaceTerminalCacheKey("/repo")).toBe("/repo:__workspace__")
27
+ })
28
+ })
29
+
30
+ describe("getLegacyTerminalStorageKeys", () => {
31
+ test("keeps workspace storage path when no legacy session id", () => {
32
+ expect(getLegacyTerminalStorageKeys("/repo")).toEqual(["/repo/terminal.v1"])
33
+ })
34
+
35
+ test("includes legacy session path before workspace path", () => {
36
+ expect(getLegacyTerminalStorageKeys("/repo", "session-123")).toEqual([
37
+ "/repo/terminal/session-123.v1",
38
+ "/repo/terminal.v1",
39
+ ])
40
+ })
41
+ })
42
+
43
+ describe("migrateTerminalState", () => {
44
+ test("drops invalid terminals and restores a valid active terminal", () => {
45
+ expect(
46
+ migrateTerminalState({
47
+ active: "missing",
48
+ all: [
49
+ null,
50
+ { id: "one", title: "Terminal 2" },
51
+ { id: "one", title: "duplicate", titleNumber: 9 },
52
+ { id: "two", title: "logs", titleNumber: 4, rows: 24, cols: 80 },
53
+ { title: "no-id" },
54
+ ],
55
+ }),
56
+ ).toEqual({
57
+ active: "one",
58
+ all: [
59
+ { id: "one", title: "Terminal 2", titleNumber: 2 },
60
+ { id: "two", title: "logs", titleNumber: 4, rows: 24, cols: 80 },
61
+ ],
62
+ })
63
+ })
64
+
65
+ test("keeps a valid active id", () => {
66
+ expect(
67
+ migrateTerminalState({
68
+ active: "two",
69
+ all: [
70
+ { id: "one", title: "Terminal 1" },
71
+ { id: "two", title: "shell", titleNumber: 7 },
72
+ ],
73
+ }),
74
+ ).toEqual({
75
+ active: "two",
76
+ all: [
77
+ { id: "one", title: "Terminal 1", titleNumber: 1 },
78
+ { id: "two", title: "shell", titleNumber: 7 },
79
+ ],
80
+ })
81
+ })
82
+ })
@@ -0,0 +1,437 @@
1
+ import { createStore, produce } from "solid-js/store"
2
+ import { createSimpleContext } from "@reign-labs/ui/context"
3
+ import { batch, createEffect, createMemo, createRoot, on, onCleanup } from "solid-js"
4
+ import { useParams } from "@solidjs/router"
5
+ import { useSDK } from "./sdk"
6
+ import type { Platform } from "./platform"
7
+ import { defaultTitle, titleNumber } from "./terminal-title"
8
+ import { Persist, persisted, removePersisted } from "@/utils/persist"
9
+
10
+ export type LocalPTY = {
11
+ id: string
12
+ title: string
13
+ titleNumber: number
14
+ rows?: number
15
+ cols?: number
16
+ buffer?: string
17
+ scrollY?: number
18
+ cursor?: number
19
+ }
20
+
21
+ const WORKSPACE_KEY = "__workspace__"
22
+ const MAX_TERMINAL_SESSIONS = 20
23
+
24
+ function record(value: unknown): value is Record<string, unknown> {
25
+ return typeof value === "object" && value !== null && !Array.isArray(value)
26
+ }
27
+
28
+ function text(value: unknown) {
29
+ return typeof value === "string" ? value : undefined
30
+ }
31
+
32
+ function num(value: unknown) {
33
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined
34
+ }
35
+
36
+ function numberFromTitle(title: string) {
37
+ return titleNumber(title, MAX_TERMINAL_SESSIONS)
38
+ }
39
+
40
+ function pty(value: unknown): LocalPTY | undefined {
41
+ if (!record(value)) return
42
+
43
+ const id = text(value.id)
44
+ if (!id) return
45
+
46
+ const title = text(value.title) ?? ""
47
+ const number = num(value.titleNumber)
48
+ const rows = num(value.rows)
49
+ const cols = num(value.cols)
50
+ const buffer = text(value.buffer)
51
+ const scrollY = num(value.scrollY)
52
+ const cursor = num(value.cursor)
53
+
54
+ return {
55
+ id,
56
+ title,
57
+ titleNumber: number && number > 0 ? number : (numberFromTitle(title) ?? 0),
58
+ ...(rows !== undefined ? { rows } : {}),
59
+ ...(cols !== undefined ? { cols } : {}),
60
+ ...(buffer !== undefined ? { buffer } : {}),
61
+ ...(scrollY !== undefined ? { scrollY } : {}),
62
+ ...(cursor !== undefined ? { cursor } : {}),
63
+ }
64
+ }
65
+
66
+ export function migrateTerminalState(value: unknown) {
67
+ if (!record(value)) return value
68
+
69
+ const seen = new Set<string>()
70
+ const all = (Array.isArray(value.all) ? value.all : []).flatMap((item) => {
71
+ const next = pty(item)
72
+ if (!next || seen.has(next.id)) return []
73
+ seen.add(next.id)
74
+ return [next]
75
+ })
76
+
77
+ const active = text(value.active)
78
+
79
+ return {
80
+ active: active && seen.has(active) ? active : all[0]?.id,
81
+ all,
82
+ }
83
+ }
84
+
85
+ export function getWorkspaceTerminalCacheKey(dir: string) {
86
+ return `${dir}:${WORKSPACE_KEY}`
87
+ }
88
+
89
+ export function getLegacyTerminalStorageKeys(dir: string, legacySessionID?: string) {
90
+ if (!legacySessionID) return [`${dir}/terminal.v1`]
91
+ return [`${dir}/terminal/${legacySessionID}.v1`, `${dir}/terminal.v1`]
92
+ }
93
+
94
+ type TerminalSession = ReturnType<typeof createWorkspaceTerminalSession>
95
+
96
+ type TerminalCacheEntry = {
97
+ value: TerminalSession
98
+ dispose: VoidFunction
99
+ }
100
+
101
+ const caches = new Set<Map<string, TerminalCacheEntry>>()
102
+
103
+ const trimTerminal = (pty: LocalPTY) => {
104
+ if (!pty.buffer && pty.cursor === undefined && pty.scrollY === undefined) return pty
105
+ return {
106
+ ...pty,
107
+ buffer: undefined,
108
+ cursor: undefined,
109
+ scrollY: undefined,
110
+ }
111
+ }
112
+
113
+ export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], platform?: Platform) {
114
+ const key = getWorkspaceTerminalCacheKey(dir)
115
+ for (const cache of caches) {
116
+ const entry = cache.get(key)
117
+ entry?.value.clear()
118
+ }
119
+
120
+ removePersisted(Persist.workspace(dir, "terminal"), platform)
121
+
122
+ const legacy = new Set(getLegacyTerminalStorageKeys(dir))
123
+ for (const id of sessionIDs ?? []) {
124
+ for (const key of getLegacyTerminalStorageKeys(dir, id)) {
125
+ legacy.add(key)
126
+ }
127
+ }
128
+ for (const key of legacy) {
129
+ removePersisted({ key }, platform)
130
+ }
131
+ }
132
+
133
+ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, legacySessionID?: string) {
134
+ const legacy = getLegacyTerminalStorageKeys(dir, legacySessionID)
135
+
136
+ const [store, setStore, _, ready] = persisted(
137
+ {
138
+ ...Persist.workspace(dir, "terminal", legacy),
139
+ migrate: migrateTerminalState,
140
+ },
141
+ createStore<{
142
+ active?: string
143
+ all: LocalPTY[]
144
+ }>({
145
+ all: [],
146
+ }),
147
+ )
148
+
149
+ const pickNextTerminalNumber = () => {
150
+ const existingTitleNumbers = new Set(
151
+ store.all.flatMap((pty) => {
152
+ const direct = Number.isFinite(pty.titleNumber) && pty.titleNumber > 0 ? pty.titleNumber : undefined
153
+ if (direct !== undefined) return [direct]
154
+ const parsed = numberFromTitle(pty.title)
155
+ if (parsed === undefined) return []
156
+ return [parsed]
157
+ }),
158
+ )
159
+
160
+ return (
161
+ Array.from({ length: existingTitleNumbers.size + 1 }, (_, index) => index + 1).find(
162
+ (number) => !existingTitleNumbers.has(number),
163
+ ) ?? 1
164
+ )
165
+ }
166
+
167
+ const removeExited = (id: string) => {
168
+ const all = store.all
169
+ const index = all.findIndex((x) => x.id === id)
170
+ if (index === -1) return
171
+ const active = store.active === id ? (index === 0 ? all[1]?.id : all[0]?.id) : store.active
172
+ batch(() => {
173
+ setStore("active", active)
174
+ setStore(
175
+ "all",
176
+ produce((draft) => {
177
+ draft.splice(index, 1)
178
+ }),
179
+ )
180
+ })
181
+ }
182
+
183
+ const unsub = sdk.event.on("pty.exited", (event: { properties: { id: string } }) => {
184
+ removeExited(event.properties.id)
185
+ })
186
+ onCleanup(unsub)
187
+
188
+ const update = (client: ReturnType<typeof useSDK>["client"], pty: Partial<LocalPTY> & { id: string }) => {
189
+ const index = store.all.findIndex((x) => x.id === pty.id)
190
+ const previous = index >= 0 ? store.all[index] : undefined
191
+ if (index >= 0) {
192
+ setStore("all", index, (item) => ({ ...item, ...pty }))
193
+ }
194
+ client.pty
195
+ .update({
196
+ ptyID: pty.id,
197
+ title: pty.title,
198
+ size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined,
199
+ })
200
+ .catch((error: unknown) => {
201
+ if (previous) {
202
+ const currentIndex = store.all.findIndex((item) => item.id === pty.id)
203
+ if (currentIndex >= 0) setStore("all", currentIndex, previous)
204
+ }
205
+ console.error("Failed to update terminal", error)
206
+ })
207
+ }
208
+
209
+ const clone = async (client: ReturnType<typeof useSDK>["client"], id: string) => {
210
+ const index = store.all.findIndex((x) => x.id === id)
211
+ const pty = store.all[index]
212
+ if (!pty) return
213
+ const next = await client.pty
214
+ .create({
215
+ title: pty.title,
216
+ })
217
+ .catch((error: unknown) => {
218
+ console.error("Failed to clone terminal", error)
219
+ return undefined
220
+ })
221
+ if (!next?.data) return
222
+
223
+ const active = store.active === pty.id
224
+
225
+ batch(() => {
226
+ setStore("all", index, {
227
+ id: next.data.id,
228
+ title: next.data.title ?? pty.title,
229
+ titleNumber: pty.titleNumber,
230
+ buffer: undefined,
231
+ cursor: undefined,
232
+ scrollY: undefined,
233
+ rows: undefined,
234
+ cols: undefined,
235
+ })
236
+ if (active) {
237
+ setStore("active", next.data.id)
238
+ }
239
+ })
240
+ }
241
+
242
+ return {
243
+ ready,
244
+ all: createMemo(() => store.all),
245
+ active: createMemo(() => store.active),
246
+ clear() {
247
+ batch(() => {
248
+ setStore("active", undefined)
249
+ setStore("all", [])
250
+ })
251
+ },
252
+ new() {
253
+ const nextNumber = pickNextTerminalNumber()
254
+
255
+ sdk.client.pty
256
+ .create({ title: defaultTitle(nextNumber) })
257
+ .then((pty: { data?: { id?: string; title?: string } }) => {
258
+ const id = pty.data?.id
259
+ if (!id) return
260
+ const newTerminal = {
261
+ id,
262
+ title: pty.data?.title ?? defaultTitle(nextNumber),
263
+ titleNumber: nextNumber,
264
+ }
265
+ setStore("all", store.all.length, newTerminal)
266
+ setStore("active", id)
267
+ })
268
+ .catch((error: unknown) => {
269
+ console.error("Failed to create terminal", error)
270
+ })
271
+ },
272
+ update(pty: Partial<LocalPTY> & { id: string }) {
273
+ update(sdk.client, pty)
274
+ },
275
+ trim(id: string) {
276
+ const index = store.all.findIndex((x) => x.id === id)
277
+ if (index === -1) return
278
+ setStore("all", index, (pty) => trimTerminal(pty))
279
+ },
280
+ trimAll() {
281
+ setStore("all", (all) => {
282
+ const next = all.map(trimTerminal)
283
+ if (next.every((pty, index) => pty === all[index])) return all
284
+ return next
285
+ })
286
+ },
287
+ async clone(id: string) {
288
+ await clone(sdk.client, id)
289
+ },
290
+ bind() {
291
+ const client = sdk.client
292
+ return {
293
+ trim(id: string) {
294
+ const index = store.all.findIndex((x) => x.id === id)
295
+ if (index === -1) return
296
+ setStore("all", index, (pty) => trimTerminal(pty))
297
+ },
298
+ update(pty: Partial<LocalPTY> & { id: string }) {
299
+ update(client, pty)
300
+ },
301
+ async clone(id: string) {
302
+ await clone(client, id)
303
+ },
304
+ }
305
+ },
306
+ open(id: string) {
307
+ setStore("active", id)
308
+ },
309
+ next() {
310
+ const index = store.all.findIndex((x) => x.id === store.active)
311
+ if (index === -1) return
312
+ const nextIndex = (index + 1) % store.all.length
313
+ setStore("active", store.all[nextIndex]?.id)
314
+ },
315
+ previous() {
316
+ const index = store.all.findIndex((x) => x.id === store.active)
317
+ if (index === -1) return
318
+ const prevIndex = index === 0 ? store.all.length - 1 : index - 1
319
+ setStore("active", store.all[prevIndex]?.id)
320
+ },
321
+ async close(id: string) {
322
+ const index = store.all.findIndex((f) => f.id === id)
323
+ if (index !== -1) {
324
+ batch(() => {
325
+ if (store.active === id) {
326
+ const next = index > 0 ? store.all[index - 1]?.id : store.all[1]?.id
327
+ setStore("active", next)
328
+ }
329
+ setStore(
330
+ "all",
331
+ produce((all) => {
332
+ all.splice(index, 1)
333
+ }),
334
+ )
335
+ })
336
+ }
337
+
338
+ await sdk.client.pty.remove({ ptyID: id }).catch((error: unknown) => {
339
+ console.error("Failed to close terminal", error)
340
+ })
341
+ },
342
+ move(id: string, to: number) {
343
+ const index = store.all.findIndex((f) => f.id === id)
344
+ if (index === -1) return
345
+ setStore(
346
+ "all",
347
+ produce((all) => {
348
+ all.splice(to, 0, all.splice(index, 1)[0])
349
+ }),
350
+ )
351
+ },
352
+ }
353
+ }
354
+
355
+ export const { use: useTerminal, provider: TerminalProvider } = createSimpleContext({
356
+ name: "Terminal",
357
+ gate: false,
358
+ init: () => {
359
+ const sdk = useSDK()
360
+ const params = useParams()
361
+ const cache = new Map<string, TerminalCacheEntry>()
362
+
363
+ caches.add(cache)
364
+ onCleanup(() => caches.delete(cache))
365
+
366
+ const disposeAll = () => {
367
+ for (const entry of cache.values()) {
368
+ entry.dispose()
369
+ }
370
+ cache.clear()
371
+ }
372
+
373
+ onCleanup(disposeAll)
374
+
375
+ const prune = () => {
376
+ while (cache.size > MAX_TERMINAL_SESSIONS) {
377
+ const first = cache.keys().next().value
378
+ if (!first) return
379
+ const entry = cache.get(first)
380
+ entry?.dispose()
381
+ cache.delete(first)
382
+ }
383
+ }
384
+
385
+ const loadWorkspace = (dir: string, legacySessionID?: string) => {
386
+ // Terminals are workspace-scoped so tabs persist while switching sessions in the same directory.
387
+ const key = getWorkspaceTerminalCacheKey(dir)
388
+ const existing = cache.get(key)
389
+ if (existing) {
390
+ cache.delete(key)
391
+ cache.set(key, existing)
392
+ return existing.value
393
+ }
394
+
395
+ const entry = createRoot((dispose) => ({
396
+ value: createWorkspaceTerminalSession(sdk, dir, legacySessionID),
397
+ dispose,
398
+ }))
399
+
400
+ cache.set(key, entry)
401
+ prune()
402
+ return entry.value
403
+ }
404
+
405
+ const workspace = createMemo(() => loadWorkspace(params.dir!, params.id))
406
+
407
+ createEffect(
408
+ on(
409
+ () => ({ dir: params.dir, id: params.id }),
410
+ (next, prev) => {
411
+ if (!prev?.dir) return
412
+ if (next.dir === prev.dir && next.id === prev.id) return
413
+ if (next.dir === prev.dir && next.id) return
414
+ loadWorkspace(prev.dir, prev.id).trimAll()
415
+ },
416
+ { defer: true },
417
+ ),
418
+ )
419
+
420
+ return {
421
+ ready: () => workspace().ready(),
422
+ all: () => workspace().all(),
423
+ active: () => workspace().active(),
424
+ new: () => workspace().new(),
425
+ update: (pty: Partial<LocalPTY> & { id: string }) => workspace().update(pty),
426
+ trim: (id: string) => workspace().trim(id),
427
+ trimAll: () => workspace().trimAll(),
428
+ clone: (id: string) => workspace().clone(id),
429
+ bind: () => workspace(),
430
+ open: (id: string) => workspace().open(id),
431
+ close: (id: string) => workspace().close(id),
432
+ move: (id: string, to: number) => workspace().move(id, to),
433
+ next: () => workspace().next(),
434
+ previous: () => workspace().previous(),
435
+ }
436
+ },
437
+ })
package/src/entry.tsx ADDED
@@ -0,0 +1,144 @@
1
+ // @refresh reload
2
+
3
+ import { render } from "solid-js/web"
4
+ import { AppBaseProviders, AppInterface } from "@/app"
5
+ import { type Platform, PlatformProvider } from "@/context/platform"
6
+ import { dict as en } from "@/i18n/en"
7
+ import { dict as zh } from "@/i18n/zh"
8
+ import { handleNotificationClick } from "@/utils/notification-click"
9
+ import pkg from "../package.json"
10
+ import { ServerConnection } from "./context/server"
11
+
12
+ const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl"
13
+
14
+ const getLocale = () => {
15
+ if (typeof navigator !== "object") return "en" as const
16
+ const languages = navigator.languages?.length ? navigator.languages : [navigator.language]
17
+ for (const language of languages) {
18
+ if (!language) continue
19
+ if (language.toLowerCase().startsWith("zh")) return "zh" as const
20
+ }
21
+ return "en" as const
22
+ }
23
+
24
+ const getRootNotFoundError = () => {
25
+ const key = "error.dev.rootNotFound" as const
26
+ const locale = getLocale()
27
+ return locale === "zh" ? (zh[key] ?? en[key]) : en[key]
28
+ }
29
+
30
+ const getStorage = (key: string) => {
31
+ if (typeof localStorage === "undefined") return null
32
+ try {
33
+ return localStorage.getItem(key)
34
+ } catch {
35
+ return null
36
+ }
37
+ }
38
+
39
+ const setStorage = (key: string, value: string | null) => {
40
+ if (typeof localStorage === "undefined") return
41
+ try {
42
+ if (value !== null) {
43
+ localStorage.setItem(key, value)
44
+ return
45
+ }
46
+ localStorage.removeItem(key)
47
+ } catch {
48
+ return
49
+ }
50
+ }
51
+
52
+ const readDefaultServerUrl = () => getStorage(DEFAULT_SERVER_URL_KEY)
53
+ const writeDefaultServerUrl = (url: string | null) => setStorage(DEFAULT_SERVER_URL_KEY, url)
54
+
55
+ const notify: Platform["notify"] = async (title, description, href) => {
56
+ if (!("Notification" in window)) return
57
+
58
+ const permission =
59
+ Notification.permission === "default"
60
+ ? await Notification.requestPermission().catch(() => "denied")
61
+ : Notification.permission
62
+
63
+ if (permission !== "granted") return
64
+
65
+ const inView = document.visibilityState === "visible" && document.hasFocus()
66
+ if (inView) return
67
+
68
+ const notification = new Notification(title, {
69
+ body: description ?? "",
70
+ icon: "https://code.reign-labs.com/favicon-96x96-v3.png",
71
+ })
72
+
73
+ notification.onclick = () => {
74
+ handleNotificationClick(href)
75
+ notification.close()
76
+ }
77
+ }
78
+
79
+ const openLink: Platform["openLink"] = (url) => {
80
+ window.open(url, "_blank")
81
+ }
82
+
83
+ const back: Platform["back"] = () => {
84
+ window.history.back()
85
+ }
86
+
87
+ const forward: Platform["forward"] = () => {
88
+ window.history.forward()
89
+ }
90
+
91
+ const restart: Platform["restart"] = async () => {
92
+ window.location.reload()
93
+ }
94
+
95
+ const root = document.getElementById("root")
96
+ if (!(root instanceof HTMLElement) && import.meta.env.DEV) {
97
+ throw new Error(getRootNotFoundError())
98
+ }
99
+
100
+ const getCurrentUrl = () => {
101
+ if (location.hostname.includes("code.reign-labs.com")) return "https://server.code.reign-labs.com"
102
+ if (import.meta.env.DEV)
103
+ return `http://${import.meta.env.VITE_REIGNCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_REIGNCODE_SERVER_PORT ?? "4096"}`
104
+ return location.origin
105
+ }
106
+
107
+ const getDefaultUrl = () => {
108
+ const lsDefault = readDefaultServerUrl()
109
+ if (lsDefault) return lsDefault
110
+ return getCurrentUrl()
111
+ }
112
+
113
+ const platform: Platform = {
114
+ platform: "web",
115
+ version: pkg.version,
116
+ openLink,
117
+ back,
118
+ forward,
119
+ restart,
120
+ notify,
121
+ getDefaultServer: async () => {
122
+ const stored = readDefaultServerUrl()
123
+ return stored ? ServerConnection.Key.make(stored) : null
124
+ },
125
+ setDefaultServer: writeDefaultServerUrl,
126
+ }
127
+
128
+ if (root instanceof HTMLElement) {
129
+ const server: ServerConnection.Http = { type: "http", http: { url: getCurrentUrl() } }
130
+ render(
131
+ () => (
132
+ <PlatformProvider value={platform}>
133
+ <AppBaseProviders>
134
+ <AppInterface
135
+ defaultServer={ServerConnection.Key.make(getDefaultUrl())}
136
+ servers={[server]}
137
+ disableHealthCheck
138
+ />
139
+ </AppBaseProviders>
140
+ </PlatformProvider>
141
+ ),
142
+ root,
143
+ )
144
+ }
package/src/env.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import "solid-js"
2
+
3
+ interface ImportMetaEnv {
4
+ readonly VITE_REIGNCODE_SERVER_HOST: string
5
+ readonly VITE_REIGNCODE_SERVER_PORT: string
6
+ }
7
+
8
+ interface ImportMeta {
9
+ readonly env: ImportMetaEnv
10
+ }
11
+
12
+ declare module "solid-js" {
13
+ namespace JSX {
14
+ interface Directives {
15
+ sortable: true
16
+ }
17
+ }
18
+ }