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,456 @@
1
+ import { createEffect, createMemo, Match, on, onCleanup, Switch } from "solid-js"
2
+ import { createStore } from "solid-js/store"
3
+ import { Dynamic } from "solid-js/web"
4
+ import type { FileSearchHandle } from "@reign-labs/ui/file"
5
+ import { useFileComponent } from "@reign-labs/ui/context/file"
6
+ import { cloneSelectedLineRange, previewSelectedLines } from "@reign-labs/ui/pierre/selection-bridge"
7
+ import { createLineCommentController } from "@reign-labs/ui/line-comment-annotations"
8
+ import { sampledChecksum } from "@reign-labs/util/encode"
9
+ import { DropdownMenu } from "@reign-labs/ui/dropdown-menu"
10
+ import { IconButton } from "@reign-labs/ui/icon-button"
11
+ import { Tabs } from "@reign-labs/ui/tabs"
12
+ import { ScrollView } from "@reign-labs/ui/scroll-view"
13
+ import { showToast } from "@reign-labs/ui/toast"
14
+ import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file"
15
+ import { useComments } from "@/context/comments"
16
+ import { useLanguage } from "@/context/language"
17
+ import { usePrompt } from "@/context/prompt"
18
+ import { getSessionHandoff } from "@/pages/session/handoff"
19
+ import { useSessionLayout } from "@/pages/session/session-layout"
20
+ import { createSessionTabs } from "@/pages/session/helpers"
21
+
22
+ function FileCommentMenu(props: {
23
+ moreLabel: string
24
+ editLabel: string
25
+ deleteLabel: string
26
+ onEdit: VoidFunction
27
+ onDelete: VoidFunction
28
+ }) {
29
+ return (
30
+ <div onMouseDown={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}>
31
+ <DropdownMenu gutter={4} placement="bottom-end">
32
+ <DropdownMenu.Trigger
33
+ as={IconButton}
34
+ icon="dot-grid"
35
+ variant="ghost"
36
+ size="small"
37
+ class="size-6 rounded-md"
38
+ aria-label={props.moreLabel}
39
+ />
40
+ <DropdownMenu.Portal>
41
+ <DropdownMenu.Content>
42
+ <DropdownMenu.Item onSelect={props.onEdit}>
43
+ <DropdownMenu.ItemLabel>{props.editLabel}</DropdownMenu.ItemLabel>
44
+ </DropdownMenu.Item>
45
+ <DropdownMenu.Item onSelect={props.onDelete}>
46
+ <DropdownMenu.ItemLabel>{props.deleteLabel}</DropdownMenu.ItemLabel>
47
+ </DropdownMenu.Item>
48
+ </DropdownMenu.Content>
49
+ </DropdownMenu.Portal>
50
+ </DropdownMenu>
51
+ </div>
52
+ )
53
+ }
54
+
55
+ export function FileTabContent(props: { tab: string }) {
56
+ const file = useFile()
57
+ const comments = useComments()
58
+ const language = useLanguage()
59
+ const prompt = usePrompt()
60
+ const fileComponent = useFileComponent()
61
+ const { sessionKey, tabs, view } = useSessionLayout()
62
+ const activeFileTab = createSessionTabs({
63
+ tabs,
64
+ pathFromTab: file.pathFromTab,
65
+ normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab),
66
+ }).activeFileTab
67
+
68
+ let scroll: HTMLDivElement | undefined
69
+ let scrollFrame: number | undefined
70
+ let restoreFrame: number | undefined
71
+ let pending: { x: number; y: number } | undefined
72
+ let codeScroll: HTMLElement[] = []
73
+ let find: FileSearchHandle | null = null
74
+
75
+ const search = {
76
+ register: (handle: FileSearchHandle | null) => {
77
+ find = handle
78
+ },
79
+ }
80
+
81
+ const path = createMemo(() => file.pathFromTab(props.tab))
82
+ const state = createMemo(() => {
83
+ const p = path()
84
+ if (!p) return
85
+ return file.get(p)
86
+ })
87
+ const contents = createMemo(() => state()?.content?.content ?? "")
88
+ const cacheKey = createMemo(() => sampledChecksum(contents()))
89
+ const selectedLines = createMemo<SelectedLineRange | null>(() => {
90
+ const p = path()
91
+ if (!p) return null
92
+ if (file.ready()) return (file.selectedLines(p) as SelectedLineRange | undefined) ?? null
93
+ return (getSessionHandoff(sessionKey())?.files[p] as SelectedLineRange | undefined) ?? null
94
+ })
95
+
96
+ const selectionPreview = (source: string, selection: FileSelection) => {
97
+ return previewSelectedLines(source, {
98
+ start: selection.startLine,
99
+ end: selection.endLine,
100
+ })
101
+ }
102
+
103
+ const addCommentToContext = (input: {
104
+ file: string
105
+ selection: SelectedLineRange
106
+ comment: string
107
+ preview?: string
108
+ origin?: "review" | "file"
109
+ }) => {
110
+ const selection = selectionFromLines(input.selection)
111
+ const preview =
112
+ input.preview ??
113
+ (() => {
114
+ if (input.file === path()) return selectionPreview(contents(), selection)
115
+ const source = file.get(input.file)?.content?.content
116
+ if (!source) return undefined
117
+ return selectionPreview(source, selection)
118
+ })()
119
+
120
+ const saved = comments.add({
121
+ file: input.file,
122
+ selection: input.selection,
123
+ comment: input.comment,
124
+ })
125
+ prompt.context.add({
126
+ type: "file",
127
+ path: input.file,
128
+ selection,
129
+ comment: input.comment,
130
+ commentID: saved.id,
131
+ commentOrigin: input.origin,
132
+ preview,
133
+ })
134
+ }
135
+
136
+ const updateCommentInContext = (input: {
137
+ id: string
138
+ file: string
139
+ selection: SelectedLineRange
140
+ comment: string
141
+ }) => {
142
+ comments.update(input.file, input.id, input.comment)
143
+ const preview =
144
+ input.file === path() ? selectionPreview(contents(), selectionFromLines(input.selection)) : undefined
145
+ prompt.context.updateComment(input.file, input.id, {
146
+ comment: input.comment,
147
+ ...(preview ? { preview } : {}),
148
+ })
149
+ }
150
+
151
+ const removeCommentFromContext = (input: { id: string; file: string }) => {
152
+ comments.remove(input.file, input.id)
153
+ prompt.context.removeComment(input.file, input.id)
154
+ }
155
+
156
+ const fileComments = createMemo(() => {
157
+ const p = path()
158
+ if (!p) return []
159
+ return comments.list(p)
160
+ })
161
+
162
+ const commentedLines = createMemo(() => fileComments().map((comment) => comment.selection))
163
+
164
+ const [note, setNote] = createStore({
165
+ openedComment: null as string | null,
166
+ commenting: null as SelectedLineRange | null,
167
+ selected: null as SelectedLineRange | null,
168
+ })
169
+
170
+ const syncSelected = (range: SelectedLineRange | null) => {
171
+ const p = path()
172
+ if (!p) return
173
+ file.setSelectedLines(p, range ? cloneSelectedLineRange(range) : null)
174
+ }
175
+
176
+ const activeSelection = () => note.selected ?? selectedLines()
177
+
178
+ const commentsUi = createLineCommentController({
179
+ comments: fileComments,
180
+ label: language.t("ui.lineComment.submit"),
181
+ draftKey: () => path() ?? props.tab,
182
+ state: {
183
+ opened: () => note.openedComment,
184
+ setOpened: (id) => setNote("openedComment", id),
185
+ selected: () => note.selected,
186
+ setSelected: (range) => setNote("selected", range),
187
+ commenting: () => note.commenting,
188
+ setCommenting: (range) => setNote("commenting", range),
189
+ syncSelected,
190
+ hoverSelected: syncSelected,
191
+ },
192
+ getHoverSelectedRange: activeSelection,
193
+ cancelDraftOnCommentToggle: true,
194
+ clearSelectionOnSelectionEndNull: true,
195
+ onSubmit: ({ comment, selection }) => {
196
+ const p = path()
197
+ if (!p) return
198
+ addCommentToContext({ file: p, selection, comment, origin: "file" })
199
+ },
200
+ onUpdate: ({ id, comment, selection }) => {
201
+ const p = path()
202
+ if (!p) return
203
+ updateCommentInContext({ id, file: p, selection, comment })
204
+ },
205
+ onDelete: (comment) => {
206
+ const p = path()
207
+ if (!p) return
208
+ removeCommentFromContext({ id: comment.id, file: p })
209
+ },
210
+ editSubmitLabel: language.t("common.save"),
211
+ renderCommentActions: (_, controls) => (
212
+ <FileCommentMenu
213
+ moreLabel={language.t("common.moreOptions")}
214
+ editLabel={language.t("common.edit")}
215
+ deleteLabel={language.t("common.delete")}
216
+ onEdit={controls.edit}
217
+ onDelete={controls.remove}
218
+ />
219
+ ),
220
+ })
221
+
222
+ createEffect(() => {
223
+ if (typeof window === "undefined") return
224
+
225
+ const onKeyDown = (event: KeyboardEvent) => {
226
+ if (activeFileTab() !== props.tab) return
227
+ if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) return
228
+ if (event.key.toLowerCase() !== "f") return
229
+
230
+ event.preventDefault()
231
+ event.stopPropagation()
232
+ find?.focus()
233
+ }
234
+
235
+ window.addEventListener("keydown", onKeyDown, { capture: true })
236
+ onCleanup(() => window.removeEventListener("keydown", onKeyDown, { capture: true }))
237
+ })
238
+
239
+ createEffect(
240
+ on(
241
+ path,
242
+ () => {
243
+ commentsUi.note.reset()
244
+ },
245
+ { defer: true },
246
+ ),
247
+ )
248
+
249
+ createEffect(() => {
250
+ const focus = comments.focus()
251
+ const p = path()
252
+ if (!focus || !p) return
253
+ if (focus.file !== p) return
254
+ if (activeFileTab() !== props.tab) return
255
+
256
+ const target = fileComments().find((comment) => comment.id === focus.id)
257
+ if (!target) return
258
+
259
+ commentsUi.note.openComment(target.id, target.selection, { cancelDraft: true })
260
+ requestAnimationFrame(() => comments.clearFocus())
261
+ })
262
+
263
+ const getCodeScroll = () => {
264
+ const el = scroll
265
+ if (!el) return []
266
+
267
+ const host = el.querySelector("diffs-container")
268
+ if (!(host instanceof HTMLElement)) return []
269
+
270
+ const root = host.shadowRoot
271
+ if (!root) return []
272
+
273
+ return Array.from(root.querySelectorAll("[data-code]")).filter(
274
+ (node): node is HTMLElement => node instanceof HTMLElement && node.clientWidth > 0,
275
+ )
276
+ }
277
+
278
+ const queueScrollUpdate = (next: { x: number; y: number }) => {
279
+ pending = next
280
+ if (scrollFrame !== undefined) return
281
+
282
+ scrollFrame = requestAnimationFrame(() => {
283
+ scrollFrame = undefined
284
+
285
+ const out = pending
286
+ pending = undefined
287
+ if (!out) return
288
+
289
+ view().setScroll(props.tab, out)
290
+ })
291
+ }
292
+
293
+ const handleCodeScroll = (event: Event) => {
294
+ const el = scroll
295
+ if (!el) return
296
+
297
+ const target = event.currentTarget
298
+ if (!(target instanceof HTMLElement)) return
299
+
300
+ queueScrollUpdate({
301
+ x: target.scrollLeft,
302
+ y: el.scrollTop,
303
+ })
304
+ }
305
+
306
+ const syncCodeScroll = () => {
307
+ const next = getCodeScroll()
308
+ if (next.length === codeScroll.length && next.every((el, i) => el === codeScroll[i])) return
309
+
310
+ for (const item of codeScroll) {
311
+ item.removeEventListener("scroll", handleCodeScroll)
312
+ }
313
+
314
+ codeScroll = next
315
+
316
+ for (const item of codeScroll) {
317
+ item.addEventListener("scroll", handleCodeScroll)
318
+ }
319
+ }
320
+
321
+ const restoreScroll = () => {
322
+ const el = scroll
323
+ if (!el) return
324
+
325
+ const s = view().scroll(props.tab)
326
+ if (!s) return
327
+
328
+ syncCodeScroll()
329
+
330
+ if (codeScroll.length > 0) {
331
+ for (const item of codeScroll) {
332
+ if (item.scrollLeft !== s.x) item.scrollLeft = s.x
333
+ }
334
+ }
335
+
336
+ if (el.scrollTop !== s.y) el.scrollTop = s.y
337
+ if (codeScroll.length > 0) return
338
+ if (el.scrollLeft !== s.x) el.scrollLeft = s.x
339
+ }
340
+
341
+ const queueRestore = () => {
342
+ if (restoreFrame !== undefined) return
343
+
344
+ restoreFrame = requestAnimationFrame(() => {
345
+ restoreFrame = undefined
346
+ restoreScroll()
347
+ })
348
+ }
349
+
350
+ const handleScroll = (event: Event & { currentTarget: HTMLDivElement }) => {
351
+ if (codeScroll.length === 0) syncCodeScroll()
352
+
353
+ queueScrollUpdate({
354
+ x: codeScroll[0]?.scrollLeft ?? event.currentTarget.scrollLeft,
355
+ y: event.currentTarget.scrollTop,
356
+ })
357
+ }
358
+
359
+ const cancelCommenting = () => {
360
+ const p = path()
361
+ if (p) file.setSelectedLines(p, null)
362
+ setNote("commenting", null)
363
+ }
364
+
365
+ let prev = {
366
+ loaded: false,
367
+ ready: false,
368
+ active: false,
369
+ }
370
+
371
+ createEffect(() => {
372
+ const loaded = !!state()?.loaded
373
+ const ready = file.ready()
374
+ const active = activeFileTab() === props.tab
375
+ const restore = (loaded && !prev.loaded) || (ready && !prev.ready) || (active && loaded && !prev.active)
376
+ prev = { loaded, ready, active }
377
+ if (!restore) return
378
+ queueRestore()
379
+ })
380
+
381
+ onCleanup(() => {
382
+ for (const item of codeScroll) {
383
+ item.removeEventListener("scroll", handleCodeScroll)
384
+ }
385
+
386
+ if (scrollFrame !== undefined) cancelAnimationFrame(scrollFrame)
387
+ if (restoreFrame !== undefined) cancelAnimationFrame(restoreFrame)
388
+ })
389
+
390
+ const renderFile = (source: string) => (
391
+ <div class="relative overflow-hidden pb-40">
392
+ <Dynamic
393
+ component={fileComponent}
394
+ mode="text"
395
+ file={{
396
+ name: path() ?? "",
397
+ contents: source,
398
+ cacheKey: cacheKey(),
399
+ }}
400
+ enableLineSelection
401
+ enableHoverUtility
402
+ selectedLines={activeSelection()}
403
+ commentedLines={commentedLines()}
404
+ onRendered={() => {
405
+ queueRestore()
406
+ }}
407
+ annotations={commentsUi.annotations()}
408
+ renderAnnotation={commentsUi.renderAnnotation}
409
+ renderHoverUtility={commentsUi.renderHoverUtility}
410
+ onLineSelected={(range: SelectedLineRange | null) => {
411
+ commentsUi.onLineSelected(range)
412
+ }}
413
+ onLineNumberSelectionEnd={commentsUi.onLineNumberSelectionEnd}
414
+ onLineSelectionEnd={(range: SelectedLineRange | null) => {
415
+ commentsUi.onLineSelectionEnd(range)
416
+ }}
417
+ search={search}
418
+ class="select-text"
419
+ media={{
420
+ mode: "auto",
421
+ path: path(),
422
+ current: state()?.content,
423
+ onLoad: queueRestore,
424
+ onError: (args: { kind: "image" | "audio" | "svg" }) => {
425
+ if (args.kind !== "svg") return
426
+ showToast({
427
+ variant: "error",
428
+ title: language.t("toast.file.loadFailed.title"),
429
+ })
430
+ },
431
+ }}
432
+ />
433
+ </div>
434
+ )
435
+
436
+ return (
437
+ <Tabs.Content value={props.tab} class="mt-3 relative h-full">
438
+ <ScrollView
439
+ class="h-full"
440
+ viewportRef={(el: HTMLDivElement) => {
441
+ scroll = el
442
+ restoreScroll()
443
+ }}
444
+ onScroll={handleScroll as any}
445
+ >
446
+ <Switch>
447
+ <Match when={state()?.loaded}>{renderFile(contents())}</Match>
448
+ <Match when={state()?.loading}>
449
+ <div class="px-6 py-4 text-text-weak">{language.t("common.loading")}...</div>
450
+ </Match>
451
+ <Match when={state()?.error}>{(err) => <div class="px-6 py-4 text-text-weak">{err()}</div>}</Match>
452
+ </Switch>
453
+ </ScrollView>
454
+ </Tabs.Content>
455
+ )
456
+ }
@@ -0,0 +1,36 @@
1
+ import type { SelectedLineRange } from "@/context/file"
2
+
3
+ type HandoffSession = {
4
+ prompt: string
5
+ files: Record<string, SelectedLineRange | null>
6
+ }
7
+
8
+ const MAX = 40
9
+
10
+ const store = {
11
+ session: new Map<string, HandoffSession>(),
12
+ terminal: new Map<string, string[]>(),
13
+ }
14
+
15
+ const touch = <K, V>(map: Map<K, V>, key: K, value: V) => {
16
+ map.delete(key)
17
+ map.set(key, value)
18
+ while (map.size > MAX) {
19
+ const first = map.keys().next().value
20
+ if (first === undefined) return
21
+ map.delete(first)
22
+ }
23
+ }
24
+
25
+ export const setSessionHandoff = (key: string, patch: Partial<HandoffSession>) => {
26
+ const prev = store.session.get(key) ?? { prompt: "", files: {} }
27
+ touch(store.session, key, { ...prev, ...patch })
28
+ }
29
+
30
+ export const getSessionHandoff = (key: string) => store.session.get(key)
31
+
32
+ export const setTerminalHandoff = (key: string, value: string[]) => {
33
+ touch(store.terminal, key, value)
34
+ }
35
+
36
+ export const getTerminalHandoff = (key: string) => store.terminal.get(key)
@@ -0,0 +1,181 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { createMemo, createRoot } from "solid-js"
3
+ import { createStore } from "solid-js/store"
4
+ import {
5
+ createOpenReviewFile,
6
+ createOpenSessionFileTab,
7
+ createSessionTabs,
8
+ focusTerminalById,
9
+ getTabReorderIndex,
10
+ shouldFocusTerminalOnKeyDown,
11
+ } from "./helpers"
12
+
13
+ describe("createOpenReviewFile", () => {
14
+ test("opens and loads selected review file", () => {
15
+ const calls: string[] = []
16
+ const openReviewFile = createOpenReviewFile({
17
+ showAllFiles: () => calls.push("show"),
18
+ tabForPath: (path) => {
19
+ calls.push(`tab:${path}`)
20
+ return `file://${path}`
21
+ },
22
+ openTab: (tab) => calls.push(`open:${tab}`),
23
+ setActive: (tab) => calls.push(`active:${tab}`),
24
+ loadFile: (path) => calls.push(`load:${path}`),
25
+ })
26
+
27
+ openReviewFile("src/a.ts")
28
+
29
+ expect(calls).toEqual(["show", "load:src/a.ts", "tab:src/a.ts", "open:file://src/a.ts", "active:file://src/a.ts"])
30
+ })
31
+ })
32
+
33
+ describe("createOpenSessionFileTab", () => {
34
+ test("activates the opened file tab", () => {
35
+ const calls: string[] = []
36
+ const openTab = createOpenSessionFileTab({
37
+ normalizeTab: (value) => {
38
+ calls.push(`normalize:${value}`)
39
+ return `file://${value}`
40
+ },
41
+ openTab: (tab) => calls.push(`open:${tab}`),
42
+ pathFromTab: (tab) => {
43
+ calls.push(`path:${tab}`)
44
+ return tab.slice("file://".length)
45
+ },
46
+ loadFile: (path) => calls.push(`load:${path}`),
47
+ openReviewPanel: () => calls.push("review"),
48
+ setActive: (tab) => calls.push(`active:${tab}`),
49
+ })
50
+
51
+ openTab("src/a.ts")
52
+
53
+ expect(calls).toEqual([
54
+ "normalize:src/a.ts",
55
+ "open:file://src/a.ts",
56
+ "path:file://src/a.ts",
57
+ "load:src/a.ts",
58
+ "review",
59
+ "active:file://src/a.ts",
60
+ ])
61
+ })
62
+ })
63
+
64
+ describe("focusTerminalById", () => {
65
+ test("focuses textarea when present", () => {
66
+ document.body.innerHTML = `<div id="terminal-wrapper-one"><div data-component="terminal"><textarea></textarea></div></div>`
67
+
68
+ const focused = focusTerminalById("one")
69
+
70
+ expect(focused).toBe(true)
71
+ expect(document.activeElement?.tagName).toBe("TEXTAREA")
72
+ })
73
+
74
+ test("falls back to terminal element focus", () => {
75
+ document.body.innerHTML = `<div id="terminal-wrapper-two"><div data-component="terminal" tabindex="0"></div></div>`
76
+ const terminal = document.querySelector('[data-component="terminal"]') as HTMLElement
77
+ let pointerDown = false
78
+ terminal.addEventListener("pointerdown", () => {
79
+ pointerDown = true
80
+ })
81
+
82
+ const focused = focusTerminalById("two")
83
+
84
+ expect(focused).toBe(true)
85
+ expect(document.activeElement).toBe(terminal)
86
+ expect(pointerDown).toBe(true)
87
+ })
88
+ })
89
+
90
+ describe("shouldFocusTerminalOnKeyDown", () => {
91
+ test("skips pure modifier keys", () => {
92
+ expect(shouldFocusTerminalOnKeyDown(new KeyboardEvent("keydown", { key: "Meta", metaKey: true }))).toBe(false)
93
+ expect(shouldFocusTerminalOnKeyDown(new KeyboardEvent("keydown", { key: "Control", ctrlKey: true }))).toBe(false)
94
+ expect(shouldFocusTerminalOnKeyDown(new KeyboardEvent("keydown", { key: "Alt", altKey: true }))).toBe(false)
95
+ expect(shouldFocusTerminalOnKeyDown(new KeyboardEvent("keydown", { key: "Shift", shiftKey: true }))).toBe(false)
96
+ })
97
+
98
+ test("skips shortcut key combos", () => {
99
+ expect(shouldFocusTerminalOnKeyDown(new KeyboardEvent("keydown", { key: "c", metaKey: true }))).toBe(false)
100
+ expect(shouldFocusTerminalOnKeyDown(new KeyboardEvent("keydown", { key: "c", ctrlKey: true }))).toBe(false)
101
+ expect(shouldFocusTerminalOnKeyDown(new KeyboardEvent("keydown", { key: "ArrowLeft", altKey: true }))).toBe(false)
102
+ })
103
+
104
+ test("keeps plain typing focused on terminal", () => {
105
+ expect(shouldFocusTerminalOnKeyDown(new KeyboardEvent("keydown", { key: "a" }))).toBe(true)
106
+ expect(shouldFocusTerminalOnKeyDown(new KeyboardEvent("keydown", { key: "A", shiftKey: true }))).toBe(true)
107
+ })
108
+ })
109
+
110
+ describe("getTabReorderIndex", () => {
111
+ test("returns target index for valid drag reorder", () => {
112
+ expect(getTabReorderIndex(["a", "b", "c"], "a", "c")).toBe(2)
113
+ })
114
+
115
+ test("returns undefined for unknown droppable id", () => {
116
+ expect(getTabReorderIndex(["a", "b", "c"], "a", "missing")).toBeUndefined()
117
+ })
118
+ })
119
+
120
+ describe("createSessionTabs", () => {
121
+ test("normalizes the effective file tab", () => {
122
+ createRoot((dispose) => {
123
+ const [state] = createStore({
124
+ active: undefined as string | undefined,
125
+ all: ["file://src/a.ts", "context"],
126
+ })
127
+ const tabs = createMemo(() => ({ active: () => state.active, all: () => state.all }))
128
+ const result = createSessionTabs({
129
+ tabs,
130
+ pathFromTab: (tab) => (tab.startsWith("file://") ? tab.slice("file://".length) : undefined),
131
+ normalizeTab: (tab) => (tab.startsWith("file://") ? `norm:${tab.slice("file://".length)}` : tab),
132
+ })
133
+
134
+ expect(result.activeTab()).toBe("norm:src/a.ts")
135
+ expect(result.activeFileTab()).toBe("norm:src/a.ts")
136
+ expect(result.closableTab()).toBe("norm:src/a.ts")
137
+ dispose()
138
+ })
139
+ })
140
+
141
+ test("prefers context and review fallbacks when no file tab is active", () => {
142
+ createRoot((dispose) => {
143
+ const [state] = createStore({
144
+ active: undefined as string | undefined,
145
+ all: ["context"],
146
+ })
147
+ const tabs = createMemo(() => ({ active: () => state.active, all: () => state.all }))
148
+ const result = createSessionTabs({
149
+ tabs,
150
+ pathFromTab: () => undefined,
151
+ normalizeTab: (tab) => tab,
152
+ review: () => true,
153
+ hasReview: () => true,
154
+ })
155
+
156
+ expect(result.activeTab()).toBe("context")
157
+ expect(result.closableTab()).toBe("context")
158
+ dispose()
159
+ })
160
+
161
+ createRoot((dispose) => {
162
+ const [state] = createStore({
163
+ active: undefined as string | undefined,
164
+ all: [],
165
+ })
166
+ const tabs = createMemo(() => ({ active: () => state.active, all: () => state.all }))
167
+ const result = createSessionTabs({
168
+ tabs,
169
+ pathFromTab: () => undefined,
170
+ normalizeTab: (tab) => tab,
171
+ review: () => true,
172
+ hasReview: () => true,
173
+ })
174
+
175
+ expect(result.activeTab()).toBe("review")
176
+ expect(result.activeFileTab()).toBeUndefined()
177
+ expect(result.closableTab()).toBeUndefined()
178
+ dispose()
179
+ })
180
+ })
181
+ })