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,453 @@
1
+ import { For, Match, Show, Switch, createEffect, createMemo, onCleanup, type JSX } from "solid-js"
2
+ import { createStore } from "solid-js/store"
3
+ import { createMediaQuery } from "@solid-primitives/media"
4
+ import { Tabs } from "@reign-labs/ui/tabs"
5
+ import { IconButton } from "@reign-labs/ui/icon-button"
6
+ import { TooltipKeybind } from "@reign-labs/ui/tooltip"
7
+ import { ResizeHandle } from "@reign-labs/ui/resize-handle"
8
+ import { Mark } from "@reign-labs/ui/logo"
9
+ import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
10
+ import type { DragEvent } from "@thisbeyond/solid-dnd"
11
+ import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
12
+ import { useDialog } from "@reign-labs/ui/context/dialog"
13
+
14
+ import FileTree from "@/components/file-tree"
15
+ import { SessionContextUsage } from "@/components/session-context-usage"
16
+ import { DialogSelectFile } from "@/components/dialog-select-file"
17
+ import { SessionContextTab, SortableTab, FileVisual } from "@/components/session"
18
+ import { useCommand } from "@/context/command"
19
+ import { useFile, type SelectedLineRange } from "@/context/file"
20
+ import { useLanguage } from "@/context/language"
21
+ import { useLayout } from "@/context/layout"
22
+ import { useSync } from "@/context/sync"
23
+ import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
24
+ import { FileTabContent } from "@/pages/session/file-tabs"
25
+ import { createOpenSessionFileTab, createSessionTabs, getTabReorderIndex, type Sizing } from "@/pages/session/helpers"
26
+ import { setSessionHandoff } from "@/pages/session/handoff"
27
+ import { useSessionLayout } from "@/pages/session/session-layout"
28
+
29
+ export function SessionSidePanel(props: {
30
+ reviewPanel: () => JSX.Element
31
+ activeDiff?: string
32
+ focusReviewDiff: (path: string) => void
33
+ reviewSnap: boolean
34
+ size: Sizing
35
+ }) {
36
+ const layout = useLayout()
37
+ const sync = useSync()
38
+ const file = useFile()
39
+ const language = useLanguage()
40
+ const command = useCommand()
41
+ const dialog = useDialog()
42
+ const { params, sessionKey, tabs, view } = useSessionLayout()
43
+
44
+ const isDesktop = createMediaQuery("(min-width: 768px)")
45
+
46
+ const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
47
+ const fileOpen = createMemo(() => isDesktop() && layout.fileTree.opened())
48
+ const open = createMemo(() => reviewOpen() || fileOpen())
49
+ const reviewTab = createMemo(() => isDesktop())
50
+ const panelWidth = createMemo(() => {
51
+ if (!open()) return "0px"
52
+ if (reviewOpen()) return `calc(100% - ${layout.session.width()}px)`
53
+ return `${layout.fileTree.width()}px`
54
+ })
55
+ const treeWidth = createMemo(() => (fileOpen() ? `${layout.fileTree.width()}px` : "0px"))
56
+
57
+ const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
58
+ const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
59
+ const reviewCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
60
+ const hasReview = createMemo(() => reviewCount() > 0)
61
+ const diffsReady = createMemo(() => {
62
+ const id = params.id
63
+ if (!id) return true
64
+ if (!hasReview()) return true
65
+ return sync.data.session_diff[id] !== undefined
66
+ })
67
+
68
+ const reviewEmptyKey = createMemo(() => {
69
+ if (sync.project && !sync.project.vcs) return "session.review.noVcs"
70
+ if (sync.data.config.snapshot === false) return "session.review.noSnapshot"
71
+ return "session.review.noChanges"
72
+ })
73
+
74
+ const diffFiles = createMemo(() => diffs().map((d) => d.file))
75
+ const kinds = createMemo(() => {
76
+ const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => {
77
+ if (!a) return b
78
+ if (a === b) return a
79
+ return "mix" as const
80
+ }
81
+
82
+ const normalize = (p: string) => p.replaceAll("\\\\", "/").replace(/\/+$/, "")
83
+
84
+ const out = new Map<string, "add" | "del" | "mix">()
85
+ for (const diff of diffs()) {
86
+ const file = normalize(diff.file)
87
+ const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix"
88
+
89
+ out.set(file, kind)
90
+
91
+ const parts = file.split("/")
92
+ for (const [idx] of parts.slice(0, -1).entries()) {
93
+ const dir = parts.slice(0, idx + 1).join("/")
94
+ if (!dir) continue
95
+ out.set(dir, merge(out.get(dir), kind))
96
+ }
97
+ }
98
+ return out
99
+ })
100
+
101
+ const empty = (msg: string) => (
102
+ <div class="h-full flex flex-col">
103
+ <div class="h-6 shrink-0" aria-hidden />
104
+ <div class="flex-1 pb-64 flex items-center justify-center text-center">
105
+ <div class="text-12-regular text-text-weak">{msg}</div>
106
+ </div>
107
+ </div>
108
+ )
109
+
110
+ const nofiles = createMemo(() => {
111
+ const state = file.tree.state("")
112
+ if (!state?.loaded) return false
113
+ return file.tree.children("").length === 0
114
+ })
115
+
116
+ const normalizeTab = (tab: string) => {
117
+ if (!tab.startsWith("file://")) return tab
118
+ return file.tab(tab)
119
+ }
120
+
121
+ const openReviewPanel = () => {
122
+ if (!view().reviewPanel.opened()) view().reviewPanel.open()
123
+ }
124
+
125
+ const openTab = createOpenSessionFileTab({
126
+ normalizeTab,
127
+ openTab: tabs().open,
128
+ pathFromTab: file.pathFromTab,
129
+ loadFile: file.load,
130
+ openReviewPanel,
131
+ setActive: tabs().setActive,
132
+ })
133
+
134
+ const tabState = createSessionTabs({
135
+ tabs,
136
+ pathFromTab: file.pathFromTab,
137
+ normalizeTab,
138
+ review: reviewTab,
139
+ hasReview,
140
+ })
141
+ const contextOpen = tabState.contextOpen
142
+ const openedTabs = tabState.openedTabs
143
+ const activeTab = tabState.activeTab
144
+ const activeFileTab = tabState.activeFileTab
145
+
146
+ const fileTreeTab = () => layout.fileTree.tab()
147
+
148
+ const setFileTreeTabValue = (value: string) => {
149
+ if (value !== "changes" && value !== "all") return
150
+ layout.fileTree.setTab(value)
151
+ }
152
+
153
+ const showAllFiles = () => {
154
+ if (fileTreeTab() !== "changes") return
155
+ layout.fileTree.setTab("all")
156
+ }
157
+
158
+ const [store, setStore] = createStore({
159
+ activeDraggable: undefined as string | undefined,
160
+ })
161
+
162
+ const handleDragStart = (event: unknown) => {
163
+ const id = getDraggableId(event)
164
+ if (!id) return
165
+ setStore("activeDraggable", id)
166
+ }
167
+
168
+ const handleDragOver = (event: DragEvent) => {
169
+ const { draggable, droppable } = event
170
+ if (!draggable || !droppable) return
171
+
172
+ const currentTabs = tabs().all()
173
+ const toIndex = getTabReorderIndex(currentTabs, draggable.id.toString(), droppable.id.toString())
174
+ if (toIndex === undefined) return
175
+ tabs().move(draggable.id.toString(), toIndex)
176
+ }
177
+
178
+ const handleDragEnd = () => {
179
+ setStore("activeDraggable", undefined)
180
+ }
181
+
182
+ createEffect(() => {
183
+ if (!file.ready()) return
184
+
185
+ setSessionHandoff(sessionKey(), {
186
+ files: tabs()
187
+ .all()
188
+ .reduce<Record<string, SelectedLineRange | null>>((acc, tab) => {
189
+ const path = file.pathFromTab(tab)
190
+ if (!path) return acc
191
+
192
+ const selected = file.selectedLines(path)
193
+ acc[path] =
194
+ selected && typeof selected === "object" && "start" in selected && "end" in selected
195
+ ? (selected as SelectedLineRange)
196
+ : null
197
+
198
+ return acc
199
+ }, {}),
200
+ })
201
+ })
202
+
203
+ return (
204
+ <Show when={isDesktop()}>
205
+ <aside
206
+ id="review-panel"
207
+ aria-label={language.t("session.panel.reviewAndFiles")}
208
+ aria-hidden={!open()}
209
+ inert={!open()}
210
+ class="relative min-w-0 h-full flex shrink-0 overflow-hidden bg-background-base"
211
+ classList={{
212
+ "pointer-events-none": !open(),
213
+ "transition-[width] duration-[240ms] ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
214
+ !props.size.active() && !props.reviewSnap,
215
+ }}
216
+ style={{ width: panelWidth() }}
217
+ >
218
+ <div class="size-full flex border-l border-border-weaker-base">
219
+ <div
220
+ aria-hidden={!reviewOpen()}
221
+ inert={!reviewOpen()}
222
+ class="relative min-w-0 h-full flex-1 overflow-hidden bg-background-base"
223
+ classList={{
224
+ "pointer-events-none": !reviewOpen(),
225
+ }}
226
+ >
227
+ <div class="size-full min-w-0 h-full bg-background-base">
228
+ <DragDropProvider
229
+ onDragStart={handleDragStart}
230
+ onDragEnd={handleDragEnd}
231
+ onDragOver={handleDragOver}
232
+ collisionDetector={closestCenter}
233
+ >
234
+ <DragDropSensors />
235
+ <ConstrainDragYAxis />
236
+ <Tabs value={activeTab()} onChange={openTab}>
237
+ <div class="sticky top-0 shrink-0 flex">
238
+ <Tabs.List
239
+ ref={(el: HTMLDivElement) => {
240
+ const stop = createFileTabListSync({ el, contextOpen })
241
+ onCleanup(stop)
242
+ }}
243
+ >
244
+ <Show when={reviewTab()}>
245
+ <Tabs.Trigger value="review">
246
+ <div class="flex items-center gap-1.5">
247
+ <div>{language.t("session.tab.review")}</div>
248
+ <Show when={hasReview()}>
249
+ <div>{reviewCount()}</div>
250
+ </Show>
251
+ </div>
252
+ </Tabs.Trigger>
253
+ </Show>
254
+ <Show when={contextOpen()}>
255
+ <Tabs.Trigger
256
+ value="context"
257
+ closeButton={
258
+ <TooltipKeybind
259
+ title={language.t("common.closeTab")}
260
+ keybind={command.keybind("tab.close")}
261
+ placement="bottom"
262
+ gutter={10}
263
+ >
264
+ <IconButton
265
+ icon="close-small"
266
+ variant="ghost"
267
+ class="h-5 w-5"
268
+ onClick={() => tabs().close("context")}
269
+ aria-label={language.t("common.closeTab")}
270
+ />
271
+ </TooltipKeybind>
272
+ }
273
+ hideCloseButton
274
+ onMiddleClick={() => tabs().close("context")}
275
+ >
276
+ <div class="flex items-center gap-2">
277
+ <SessionContextUsage variant="indicator" />
278
+ <div>{language.t("session.tab.context")}</div>
279
+ </div>
280
+ </Tabs.Trigger>
281
+ </Show>
282
+ <SortableProvider ids={openedTabs()}>
283
+ <For each={openedTabs()}>{(tab) => <SortableTab tab={tab} onTabClose={tabs().close} />}</For>
284
+ </SortableProvider>
285
+ <div class="bg-background-stronger h-full shrink-0 sticky right-0 z-10 flex items-center justify-center pr-3">
286
+ <TooltipKeybind
287
+ title={language.t("command.file.open")}
288
+ keybind={command.keybind("file.open")}
289
+ class="flex items-center"
290
+ >
291
+ <IconButton
292
+ icon="plus-small"
293
+ variant="ghost"
294
+ iconSize="large"
295
+ class="!rounded-md"
296
+ onClick={() =>
297
+ dialog.show(() => <DialogSelectFile mode="files" onOpenFile={showAllFiles} />)
298
+ }
299
+ aria-label={language.t("command.file.open")}
300
+ />
301
+ </TooltipKeybind>
302
+ </div>
303
+ </Tabs.List>
304
+ </div>
305
+
306
+ <Show when={reviewTab()}>
307
+ <Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
308
+ <Show when={activeTab() === "review"}>{props.reviewPanel()}</Show>
309
+ </Tabs.Content>
310
+ </Show>
311
+
312
+ <Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict">
313
+ <Show when={activeTab() === "empty"}>
314
+ <div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
315
+ <div class="h-full px-6 pb-42 -mt-4 flex flex-col items-center justify-center text-center gap-6">
316
+ <Mark class="w-14 opacity-10" />
317
+ <div class="text-14-regular text-text-weak max-w-56">
318
+ {language.t("session.files.selectToOpen")}
319
+ </div>
320
+ </div>
321
+ </div>
322
+ </Show>
323
+ </Tabs.Content>
324
+
325
+ <Show when={contextOpen()}>
326
+ <Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict">
327
+ <Show when={activeTab() === "context"}>
328
+ <div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
329
+ <SessionContextTab />
330
+ </div>
331
+ </Show>
332
+ </Tabs.Content>
333
+ </Show>
334
+
335
+ <Show when={activeFileTab()} keyed>
336
+ {(tab) => <FileTabContent tab={tab} />}
337
+ </Show>
338
+ </Tabs>
339
+ <DragOverlay>
340
+ <Show when={store.activeDraggable} keyed>
341
+ {(tab) => {
342
+ const path = file.pathFromTab(tab)
343
+ return (
344
+ <div data-component="tabs-drag-preview">
345
+ <Show when={path}>{(p) => <FileVisual active path={p()} />}</Show>
346
+ </div>
347
+ )
348
+ }}
349
+ </Show>
350
+ </DragOverlay>
351
+ </DragDropProvider>
352
+ </div>
353
+ </div>
354
+
355
+ <div
356
+ id="file-tree-panel"
357
+ aria-hidden={!fileOpen()}
358
+ inert={!fileOpen()}
359
+ class="relative min-w-0 h-full shrink-0 overflow-hidden"
360
+ classList={{
361
+ "pointer-events-none": !fileOpen(),
362
+ "transition-[width] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
363
+ !props.size.active(),
364
+ }}
365
+ style={{ width: treeWidth() }}
366
+ >
367
+ <div
368
+ class="h-full flex flex-col overflow-hidden group/filetree"
369
+ classList={{ "border-l border-border-weaker-base": reviewOpen() }}
370
+ >
371
+ <Tabs
372
+ variant="pill"
373
+ value={fileTreeTab()}
374
+ onChange={setFileTreeTabValue}
375
+ class="h-full"
376
+ data-scope="filetree"
377
+ >
378
+ <Tabs.List>
379
+ <Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
380
+ {reviewCount()}{" "}
381
+ {language.t(reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other")}
382
+ </Tabs.Trigger>
383
+ <Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}>
384
+ {language.t("session.files.all")}
385
+ </Tabs.Trigger>
386
+ </Tabs.List>
387
+ <Tabs.Content value="changes" class="bg-background-stronger px-3 py-0">
388
+ <Switch>
389
+ <Match when={hasReview()}>
390
+ <Show
391
+ when={diffsReady()}
392
+ fallback={
393
+ <div class="px-2 py-2 text-12-regular text-text-weak">
394
+ {language.t("common.loading")}
395
+ {language.t("common.loading.ellipsis")}
396
+ </div>
397
+ }
398
+ >
399
+ <FileTree
400
+ path=""
401
+ class="pt-3"
402
+ allowed={diffFiles()}
403
+ kinds={kinds()}
404
+ draggable={false}
405
+ active={props.activeDiff}
406
+ onFileClick={(node) => props.focusReviewDiff(node.path)}
407
+ />
408
+ </Show>
409
+ </Match>
410
+ <Match when={true}>
411
+ {empty(
412
+ language.t(sync.project && !sync.project.vcs ? "session.review.noChanges" : reviewEmptyKey()),
413
+ )}
414
+ </Match>
415
+ </Switch>
416
+ </Tabs.Content>
417
+ <Tabs.Content value="all" class="bg-background-stronger px-3 py-0">
418
+ <Switch>
419
+ <Match when={nofiles()}>{empty(language.t("session.files.empty"))}</Match>
420
+ <Match when={true}>
421
+ <FileTree
422
+ path=""
423
+ class="pt-3"
424
+ modified={diffFiles()}
425
+ kinds={kinds()}
426
+ onFileClick={(node) => openTab(file.tab(node.path))}
427
+ />
428
+ </Match>
429
+ </Switch>
430
+ </Tabs.Content>
431
+ </Tabs>
432
+ </div>
433
+ <Show when={fileOpen()}>
434
+ <div onPointerDown={() => props.size.start()}>
435
+ <ResizeHandle
436
+ direction="horizontal"
437
+ edge="start"
438
+ size={layout.fileTree.width()}
439
+ min={200}
440
+ max={480}
441
+ onResize={(width) => {
442
+ props.size.touch()
443
+ layout.fileTree.resize(width)
444
+ }}
445
+ />
446
+ </div>
447
+ </Show>
448
+ </div>
449
+ </div>
450
+ </aside>
451
+ </Show>
452
+ )
453
+ }
@@ -0,0 +1,16 @@
1
+ import { isDefaultTitle as isDefaultTerminalTitle } from "@/context/terminal-title"
2
+
3
+ export const terminalTabLabel = (input: {
4
+ title?: string
5
+ titleNumber?: number
6
+ t: (key: string, vars?: Record<string, string | number | boolean>) => string
7
+ }) => {
8
+ const title = input.title ?? ""
9
+ const number = input.titleNumber ?? 0
10
+ const isDefaultTitle = Number.isFinite(number) && number > 0 && isDefaultTerminalTitle(title, number)
11
+
12
+ if (title && !isDefaultTitle) return title
13
+ if (number > 0) return input.t("terminal.title.numbered", { number })
14
+ if (title) return title
15
+ return input.t("terminal.title")
16
+ }
@@ -0,0 +1,25 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { terminalTabLabel } from "./terminal-label"
3
+
4
+ const t = (key: string, vars?: Record<string, string | number | boolean>) => {
5
+ if (key === "terminal.title.numbered") return `Terminal ${vars?.number}`
6
+ if (key === "terminal.title") return "Terminal"
7
+ return key
8
+ }
9
+
10
+ describe("terminalTabLabel", () => {
11
+ test("returns custom title unchanged", () => {
12
+ const label = terminalTabLabel({ title: "server", titleNumber: 3, t })
13
+ expect(label).toBe("server")
14
+ })
15
+
16
+ test("normalizes default numbered title", () => {
17
+ const label = terminalTabLabel({ title: "Terminal 2", titleNumber: 2, t })
18
+ expect(label).toBe("Terminal 2")
19
+ })
20
+
21
+ test("falls back to generic title", () => {
22
+ const label = terminalTabLabel({ title: "", titleNumber: 0, t })
23
+ expect(label).toBe("Terminal")
24
+ })
25
+ })