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,2 @@
1
+ export { SessionComposerRegion } from "./session-composer-region"
2
+ export { createSessionComposerState } from "./session-composer-state"
@@ -0,0 +1,255 @@
1
+ import { Show, createEffect, createMemo, onCleanup } from "solid-js"
2
+ import { createStore } from "solid-js/store"
3
+ import { useSpring } from "@reign-labs/ui/motion-spring"
4
+ import { PromptInput } from "@/components/prompt-input"
5
+ import { useLanguage } from "@/context/language"
6
+ import { usePrompt } from "@/context/prompt"
7
+ import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff"
8
+ import { useSessionKey } from "@/pages/session/session-layout"
9
+ import { SessionPermissionDock } from "@/pages/session/composer/session-permission-dock"
10
+ import { SessionQuestionDock } from "@/pages/session/composer/session-question-dock"
11
+ import { SessionFollowupDock } from "@/pages/session/composer/session-followup-dock"
12
+ import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock"
13
+ import type { SessionComposerState } from "@/pages/session/composer/session-composer-state"
14
+ import { SessionTodoDock } from "@/pages/session/composer/session-todo-dock"
15
+ import type { FollowupDraft } from "@/components/prompt-input/submit"
16
+
17
+ export function SessionComposerRegion(props: {
18
+ state: SessionComposerState
19
+ ready: boolean
20
+ centered: boolean
21
+ inputRef: (el: HTMLDivElement) => void
22
+ newSessionWorktree: string
23
+ onNewSessionWorktreeReset: () => void
24
+ onSubmit: () => void
25
+ onResponseSubmit: () => void
26
+ followup?: {
27
+ queue: () => boolean
28
+ items: { id: string; text: string }[]
29
+ sending?: string
30
+ edit?: { id: string; prompt: FollowupDraft["prompt"]; context: FollowupDraft["context"] }
31
+ onQueue: (draft: FollowupDraft) => void
32
+ onAbort: () => void
33
+ onSend: (id: string) => void
34
+ onEdit: (id: string) => void
35
+ onEditLoaded: () => void
36
+ }
37
+ revert?: {
38
+ items: { id: string; text: string }[]
39
+ restoring?: string
40
+ disabled?: boolean
41
+ onRestore: (id: string) => void
42
+ }
43
+ setPromptDockRef: (el: HTMLDivElement) => void
44
+ }) {
45
+ const prompt = usePrompt()
46
+ const language = useLanguage()
47
+ const route = useSessionKey()
48
+
49
+ const handoffPrompt = createMemo(() => getSessionHandoff(route.sessionKey())?.prompt)
50
+
51
+ const previewPrompt = () =>
52
+ prompt
53
+ .current()
54
+ .map((part) => {
55
+ if (part.type === "file") return `[file:${part.path}]`
56
+ if (part.type === "agent") return `@${part.name}`
57
+ if (part.type === "image") return `[image:${part.filename}]`
58
+ return part.content
59
+ })
60
+ .join("")
61
+ .trim()
62
+
63
+ createEffect(() => {
64
+ if (!prompt.ready()) return
65
+ setSessionHandoff(route.sessionKey(), { prompt: previewPrompt() })
66
+ })
67
+
68
+ const [store, setStore] = createStore({
69
+ ready: false,
70
+ height: 320,
71
+ body: undefined as HTMLDivElement | undefined,
72
+ })
73
+ let timer: number | undefined
74
+ let frame: number | undefined
75
+
76
+ const clear = () => {
77
+ if (timer !== undefined) {
78
+ window.clearTimeout(timer)
79
+ timer = undefined
80
+ }
81
+ if (frame !== undefined) {
82
+ cancelAnimationFrame(frame)
83
+ frame = undefined
84
+ }
85
+ }
86
+
87
+ createEffect(() => {
88
+ route.sessionKey()
89
+ const ready = props.ready
90
+ const delay = 140
91
+
92
+ clear()
93
+ setStore("ready", false)
94
+ if (!ready) return
95
+
96
+ frame = requestAnimationFrame(() => {
97
+ frame = undefined
98
+ timer = window.setTimeout(() => {
99
+ setStore("ready", true)
100
+ timer = undefined
101
+ }, delay)
102
+ })
103
+ })
104
+
105
+ onCleanup(clear)
106
+
107
+ const open = createMemo(() => store.ready && props.state.dock() && !props.state.closing())
108
+ const progress = useSpring(() => (open() ? 1 : 0), { visualDuration: 0.3, bounce: 0 })
109
+ const value = createMemo(() => Math.max(0, Math.min(1, progress())))
110
+ const dock = createMemo(() => (store.ready && props.state.dock()) || value() > 0.001)
111
+ const rolled = createMemo(() => (props.revert?.items.length ? props.revert : undefined))
112
+ const lift = createMemo(() => (rolled() ? 18 : 36 * value()))
113
+ const full = createMemo(() => Math.max(78, store.height))
114
+
115
+ createEffect(() => {
116
+ const el = store.body
117
+ if (!el) return
118
+ const update = () => {
119
+ setStore("height", el.getBoundingClientRect().height)
120
+ }
121
+ update()
122
+ const observer = new ResizeObserver(update)
123
+ observer.observe(el)
124
+ onCleanup(() => observer.disconnect())
125
+ })
126
+
127
+ return (
128
+ <div
129
+ ref={props.setPromptDockRef}
130
+ data-component="session-prompt-dock"
131
+ class="shrink-0 w-full pb-3 flex flex-col justify-center items-center bg-background-stronger pointer-events-none"
132
+ >
133
+ <div
134
+ classList={{
135
+ "w-full px-3 pointer-events-auto": true,
136
+ "md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
137
+ }}
138
+ >
139
+ <Show when={props.state.questionRequest()} keyed>
140
+ {(request) => (
141
+ <div>
142
+ <SessionQuestionDock request={request} onSubmit={props.onResponseSubmit} />
143
+ </div>
144
+ )}
145
+ </Show>
146
+
147
+ <Show when={props.state.permissionRequest()} keyed>
148
+ {(request) => (
149
+ <div>
150
+ <SessionPermissionDock
151
+ request={request}
152
+ responding={props.state.permissionResponding()}
153
+ onDecide={(response) => {
154
+ props.onResponseSubmit()
155
+ props.state.decide(response)
156
+ }}
157
+ />
158
+ </div>
159
+ )}
160
+ </Show>
161
+
162
+ <Show when={!props.state.blocked()}>
163
+ <Show
164
+ when={prompt.ready()}
165
+ fallback={
166
+ <>
167
+ <Show when={rolled()} keyed>
168
+ {(revert) => (
169
+ <div class="pb-2">
170
+ <SessionRevertDock
171
+ items={revert.items}
172
+ restoring={revert.restoring}
173
+ disabled={revert.disabled}
174
+ onRestore={revert.onRestore}
175
+ />
176
+ </div>
177
+ )}
178
+ </Show>
179
+ <div class="w-full min-h-32 md:min-h-40 rounded-md border border-border-weak-base bg-background-base/50 px-4 py-3 text-text-weak whitespace-pre-wrap pointer-events-none">
180
+ {handoffPrompt() || language.t("prompt.loading")}
181
+ </div>
182
+ </>
183
+ }
184
+ >
185
+ <Show when={dock()}>
186
+ <div
187
+ classList={{
188
+ "overflow-hidden": true,
189
+ "pointer-events-none": value() < 0.98,
190
+ }}
191
+ style={{
192
+ "max-height": `${full() * value()}px`,
193
+ }}
194
+ >
195
+ <div ref={(el) => setStore("body", el)}>
196
+ <SessionTodoDock
197
+ sessionID={route.params.id}
198
+ todos={props.state.todos()}
199
+ collapseLabel={language.t("session.todo.collapse")}
200
+ expandLabel={language.t("session.todo.expand")}
201
+ dockProgress={value()}
202
+ />
203
+ </div>
204
+ </div>
205
+ </Show>
206
+ <Show when={rolled()} keyed>
207
+ {(revert) => (
208
+ <div
209
+ style={{
210
+ "margin-top": `${-36 * value()}px`,
211
+ }}
212
+ >
213
+ <SessionRevertDock
214
+ items={revert.items}
215
+ restoring={revert.restoring}
216
+ disabled={revert.disabled}
217
+ onRestore={revert.onRestore}
218
+ />
219
+ </div>
220
+ )}
221
+ </Show>
222
+ <div
223
+ classList={{
224
+ "relative z-10": true,
225
+ }}
226
+ style={{
227
+ "margin-top": `${-lift()}px`,
228
+ }}
229
+ >
230
+ <Show when={props.followup?.items.length}>
231
+ <SessionFollowupDock
232
+ items={props.followup!.items}
233
+ sending={props.followup!.sending}
234
+ onSend={props.followup!.onSend}
235
+ onEdit={props.followup!.onEdit}
236
+ />
237
+ </Show>
238
+ <PromptInput
239
+ ref={props.inputRef}
240
+ newSessionWorktree={props.newSessionWorktree}
241
+ onNewSessionWorktreeReset={props.onNewSessionWorktreeReset}
242
+ edit={props.followup?.edit}
243
+ onEditLoaded={props.followup?.onEditLoaded}
244
+ shouldQueue={props.followup?.queue}
245
+ onQueue={props.followup?.onQueue}
246
+ onAbort={props.followup?.onAbort}
247
+ onSubmit={props.onSubmit}
248
+ />
249
+ </div>
250
+ </Show>
251
+ </Show>
252
+ </div>
253
+ </div>
254
+ )
255
+ }
@@ -0,0 +1,128 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import type { PermissionRequest, QuestionRequest, Session } from "@reign-labs/sdk/v2/client"
3
+ import { todoState } from "./session-composer-state"
4
+ import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree"
5
+
6
+ const session = (input: { id: string; parentID?: string }) =>
7
+ ({
8
+ id: input.id,
9
+ parentID: input.parentID,
10
+ }) as Session
11
+
12
+ const permission = (id: string, sessionID: string) =>
13
+ ({
14
+ id,
15
+ sessionID,
16
+ }) as PermissionRequest
17
+
18
+ const question = (id: string, sessionID: string) =>
19
+ ({
20
+ id,
21
+ sessionID,
22
+ questions: [],
23
+ }) as QuestionRequest
24
+
25
+ describe("sessionPermissionRequest", () => {
26
+ test("prefers the current session permission", () => {
27
+ const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
28
+ const permissions = {
29
+ root: [permission("perm-root", "root")],
30
+ child: [permission("perm-child", "child")],
31
+ }
32
+
33
+ expect(sessionPermissionRequest(sessions, permissions, "root")?.id).toBe("perm-root")
34
+ })
35
+
36
+ test("returns a nested child permission", () => {
37
+ const sessions = [
38
+ session({ id: "root" }),
39
+ session({ id: "child", parentID: "root" }),
40
+ session({ id: "grand", parentID: "child" }),
41
+ session({ id: "other" }),
42
+ ]
43
+ const permissions = {
44
+ grand: [permission("perm-grand", "grand")],
45
+ other: [permission("perm-other", "other")],
46
+ }
47
+
48
+ expect(sessionPermissionRequest(sessions, permissions, "root")?.id).toBe("perm-grand")
49
+ })
50
+
51
+ test("returns undefined without a matching tree permission", () => {
52
+ const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
53
+ const permissions = {
54
+ other: [permission("perm-other", "other")],
55
+ }
56
+
57
+ expect(sessionPermissionRequest(sessions, permissions, "root")).toBeUndefined()
58
+ })
59
+
60
+ test("skips filtered permissions in the current tree", () => {
61
+ const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
62
+ const permissions = {
63
+ root: [permission("perm-root", "root")],
64
+ child: [permission("perm-child", "child")],
65
+ }
66
+
67
+ expect(sessionPermissionRequest(sessions, permissions, "root", (item) => item.id !== "perm-root"))?.toMatchObject({
68
+ id: "perm-child",
69
+ })
70
+ })
71
+
72
+ test("returns undefined when all tree permissions are filtered out", () => {
73
+ const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
74
+ const permissions = {
75
+ root: [permission("perm-root", "root")],
76
+ child: [permission("perm-child", "child")],
77
+ }
78
+
79
+ expect(sessionPermissionRequest(sessions, permissions, "root", () => false)).toBeUndefined()
80
+ })
81
+ })
82
+
83
+ describe("sessionQuestionRequest", () => {
84
+ test("prefers the current session question", () => {
85
+ const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
86
+ const questions = {
87
+ root: [question("q-root", "root")],
88
+ child: [question("q-child", "child")],
89
+ }
90
+
91
+ expect(sessionQuestionRequest(sessions, questions, "root")?.id).toBe("q-root")
92
+ })
93
+
94
+ test("returns a nested child question", () => {
95
+ const sessions = [
96
+ session({ id: "root" }),
97
+ session({ id: "child", parentID: "root" }),
98
+ session({ id: "grand", parentID: "child" }),
99
+ ]
100
+ const questions = {
101
+ grand: [question("q-grand", "grand")],
102
+ }
103
+
104
+ expect(sessionQuestionRequest(sessions, questions, "root")?.id).toBe("q-grand")
105
+ })
106
+ })
107
+
108
+ describe("todoState", () => {
109
+ test("hides when there are no todos", () => {
110
+ expect(todoState({ count: 0, done: false, live: true })).toBe("hide")
111
+ })
112
+
113
+ test("opens while the session is still working", () => {
114
+ expect(todoState({ count: 2, done: false, live: true })).toBe("open")
115
+ })
116
+
117
+ test("closes completed todos after a running turn", () => {
118
+ expect(todoState({ count: 2, done: true, live: true })).toBe("close")
119
+ })
120
+
121
+ test("clears stale todos when the turn ends", () => {
122
+ expect(todoState({ count: 2, done: false, live: false })).toBe("clear")
123
+ })
124
+
125
+ test("clears completed todos when the session is no longer live", () => {
126
+ expect(todoState({ count: 2, done: true, live: false })).toBe("clear")
127
+ })
128
+ })
@@ -0,0 +1,249 @@
1
+ import { createEffect, createMemo, on, onCleanup, onMount } from "solid-js"
2
+ import { createStore } from "solid-js/store"
3
+ import type { PermissionRequest, QuestionRequest, Todo } from "@reign-labs/sdk/v2"
4
+ import { useParams } from "@solidjs/router"
5
+ import { showToast } from "@reign-labs/ui/toast"
6
+ import { useGlobalSync } from "@/context/global-sync"
7
+ import { useLanguage } from "@/context/language"
8
+ import { usePermission } from "@/context/permission"
9
+ import { useSDK } from "@/context/sdk"
10
+ import { useSync } from "@/context/sync"
11
+ import { composerDriver, composerEnabled, composerEvent } from "@/testing/session-composer"
12
+ import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree"
13
+
14
+ export const todoState = (input: {
15
+ count: number
16
+ done: boolean
17
+ live: boolean
18
+ }): "hide" | "clear" | "open" | "close" => {
19
+ if (input.count === 0) return "hide"
20
+ if (!input.live) return "clear"
21
+ if (!input.done) return "open"
22
+ return "close"
23
+ }
24
+
25
+ const idle = { type: "idle" as const }
26
+
27
+ export function createSessionComposerState(options?: { closeMs?: number | (() => number) }) {
28
+ const params = useParams()
29
+ const sdk = useSDK()
30
+ const sync = useSync()
31
+ const globalSync = useGlobalSync()
32
+ const language = useLanguage()
33
+ const permission = usePermission()
34
+
35
+ const questionRequest = createMemo((): QuestionRequest | undefined => {
36
+ return sessionQuestionRequest(sync.data.session, sync.data.question, params.id)
37
+ })
38
+
39
+ const permissionRequest = createMemo((): PermissionRequest | undefined => {
40
+ return sessionPermissionRequest(sync.data.session, sync.data.permission, params.id, (item) => {
41
+ return !permission.autoResponds(item, sdk.directory)
42
+ })
43
+ })
44
+
45
+ const blocked = createMemo(() => {
46
+ const id = params.id
47
+ if (!id) return false
48
+ return !!permissionRequest() || !!questionRequest()
49
+ })
50
+
51
+ const [test, setTest] = createStore({
52
+ on: false,
53
+ live: undefined as boolean | undefined,
54
+ todos: undefined as Todo[] | undefined,
55
+ })
56
+
57
+ const pull = () => {
58
+ const id = params.id
59
+ if (!id) {
60
+ setTest({ on: false, live: undefined, todos: undefined })
61
+ return
62
+ }
63
+
64
+ const next = composerDriver(id)
65
+ if (!next) {
66
+ setTest({ on: false, live: undefined, todos: undefined })
67
+ return
68
+ }
69
+
70
+ setTest({
71
+ on: true,
72
+ live: next.live,
73
+ todos: next.todos?.map((todo) => ({ ...todo })),
74
+ })
75
+ }
76
+
77
+ onMount(() => {
78
+ if (!composerEnabled()) return
79
+
80
+ pull()
81
+ createEffect(on(() => params.id, pull, { defer: true }))
82
+
83
+ const onEvent = (event: Event) => {
84
+ const detail = (event as CustomEvent<{ sessionID?: string }>).detail
85
+ if (detail?.sessionID !== params.id) return
86
+ pull()
87
+ }
88
+
89
+ window.addEventListener(composerEvent, onEvent)
90
+ onCleanup(() => window.removeEventListener(composerEvent, onEvent))
91
+ })
92
+
93
+ const todos = createMemo((): Todo[] => {
94
+ if (test.on && test.todos !== undefined) return test.todos
95
+ const id = params.id
96
+ if (!id) return []
97
+ return globalSync.data.session_todo[id] ?? []
98
+ })
99
+
100
+ const done = createMemo(
101
+ () => todos().length > 0 && todos().every((todo) => todo.status === "completed" || todo.status === "cancelled"),
102
+ )
103
+
104
+ const status = createMemo(() => {
105
+ const id = params.id
106
+ if (!id) return idle
107
+ return sync.data.session_status[id] ?? idle
108
+ })
109
+
110
+ const busy = createMemo(() => status().type !== "idle")
111
+ const live = createMemo(() => {
112
+ if (test.on && test.live !== undefined) return test.live
113
+ return busy() || blocked()
114
+ })
115
+
116
+ const [store, setStore] = createStore({
117
+ responding: undefined as string | undefined,
118
+ dock: todos().length > 0 && live(),
119
+ closing: false,
120
+ opening: false,
121
+ })
122
+
123
+ const permissionResponding = createMemo(() => {
124
+ const perm = permissionRequest()
125
+ if (!perm) return false
126
+ return store.responding === perm.id
127
+ })
128
+
129
+ const decide = (response: "once" | "always" | "reject") => {
130
+ const perm = permissionRequest()
131
+ if (!perm) return
132
+ if (store.responding === perm.id) return
133
+
134
+ setStore("responding", perm.id)
135
+ sdk.client.permission
136
+ .respond({ sessionID: perm.sessionID, permissionID: perm.id, response })
137
+ .catch((err: unknown) => {
138
+ const description = err instanceof Error ? err.message : String(err)
139
+ showToast({ title: language.t("common.requestFailed"), description })
140
+ })
141
+ .finally(() => {
142
+ setStore("responding", (id) => (id === perm.id ? undefined : id))
143
+ })
144
+ }
145
+
146
+ let timer: number | undefined
147
+ let raf: number | undefined
148
+
149
+ const closeMs = () => {
150
+ const value = options?.closeMs
151
+ if (typeof value === "function") return Math.max(0, value())
152
+ if (typeof value === "number") return Math.max(0, value)
153
+ return 400
154
+ }
155
+
156
+ const scheduleClose = () => {
157
+ if (timer) window.clearTimeout(timer)
158
+ timer = window.setTimeout(() => {
159
+ setStore({ dock: false, closing: false })
160
+ timer = undefined
161
+ }, closeMs())
162
+ }
163
+
164
+ // Keep stale turn todos from reopening if the model never clears them.
165
+ const clear = () => {
166
+ if (test.on && test.todos !== undefined) {
167
+ setTest("todos", [])
168
+ return
169
+ }
170
+ const id = params.id
171
+ if (!id) return
172
+ globalSync.todo.set(id, [])
173
+ sync.set("todo", id, [])
174
+ }
175
+
176
+ createEffect(
177
+ on(
178
+ () => [todos().length, done(), live()] as const,
179
+ ([count, complete, active]) => {
180
+ if (raf) cancelAnimationFrame(raf)
181
+ raf = undefined
182
+
183
+ const next = todoState({
184
+ count,
185
+ done: complete,
186
+ live: active,
187
+ })
188
+
189
+ if (next === "hide") {
190
+ if (timer) window.clearTimeout(timer)
191
+ timer = undefined
192
+ setStore({ dock: false, closing: false, opening: false })
193
+ return
194
+ }
195
+
196
+ if (next === "clear") {
197
+ if (timer) window.clearTimeout(timer)
198
+ timer = undefined
199
+ clear()
200
+ return
201
+ }
202
+
203
+ if (next === "open") {
204
+ if (timer) window.clearTimeout(timer)
205
+ timer = undefined
206
+ const hidden = !store.dock || store.closing
207
+ setStore({ dock: true, closing: false })
208
+ if (hidden) {
209
+ setStore("opening", true)
210
+ raf = requestAnimationFrame(() => {
211
+ setStore("opening", false)
212
+ raf = undefined
213
+ })
214
+ return
215
+ }
216
+ setStore("opening", false)
217
+ return
218
+ }
219
+
220
+ setStore({ dock: true, opening: false, closing: true })
221
+ if (!timer) scheduleClose()
222
+ },
223
+ ),
224
+ )
225
+
226
+ onCleanup(() => {
227
+ if (!timer) return
228
+ window.clearTimeout(timer)
229
+ })
230
+
231
+ onCleanup(() => {
232
+ if (!raf) return
233
+ cancelAnimationFrame(raf)
234
+ })
235
+
236
+ return {
237
+ blocked,
238
+ questionRequest,
239
+ permissionRequest,
240
+ permissionResponding,
241
+ decide,
242
+ todos,
243
+ dock: () => store.dock,
244
+ closing: () => store.closing,
245
+ opening: () => store.opening,
246
+ }
247
+ }
248
+
249
+ export type SessionComposerState = ReturnType<typeof createSessionComposerState>