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,384 @@
1
+ import { createEffect, createMemo, For, Show, type Accessor, type JSX } from "solid-js"
2
+ import { createStore } from "solid-js/store"
3
+ import { base64Encode } from "@reign-labs/util/encode"
4
+ import { Button } from "@reign-labs/ui/button"
5
+ import { ContextMenu } from "@reign-labs/ui/context-menu"
6
+ import { HoverCard } from "@reign-labs/ui/hover-card"
7
+ import { Icon } from "@reign-labs/ui/icon"
8
+ import { createSortable } from "@thisbeyond/solid-dnd"
9
+ import { useLayout, type LocalProject } from "@/context/layout"
10
+ import { useGlobalSync } from "@/context/global-sync"
11
+ import { useLanguage } from "@/context/language"
12
+ import { useNotification } from "@/context/notification"
13
+ import { ProjectIcon, SessionItem, type SessionItemProps } from "./sidebar-items"
14
+ import { childMapByParent, displayName, sortedRootSessions } from "./helpers"
15
+
16
+ export type ProjectSidebarContext = {
17
+ currentDir: Accessor<string>
18
+ currentProject: Accessor<LocalProject | undefined>
19
+ sidebarOpened: Accessor<boolean>
20
+ sidebarHovering: Accessor<boolean>
21
+ hoverProject: Accessor<string | undefined>
22
+ nav: Accessor<HTMLElement | undefined>
23
+ onProjectMouseEnter: (worktree: string, event: MouseEvent) => void
24
+ onProjectMouseLeave: (worktree: string) => void
25
+ onProjectFocus: (worktree: string) => void
26
+ onHoverOpenChanged: (worktree: string, hovered: boolean) => void
27
+ navigateToProject: (directory: string) => void
28
+ openSidebar: () => void
29
+ closeProject: (directory: string) => void
30
+ showEditProjectDialog: (project: LocalProject) => void
31
+ toggleProjectWorkspaces: (project: LocalProject) => void
32
+ workspacesEnabled: (project: LocalProject) => boolean
33
+ workspaceIds: (project: LocalProject) => string[]
34
+ workspaceLabel: (directory: string, branch?: string, projectId?: string) => string
35
+ sessionProps: Omit<SessionItemProps, "session" | "list" | "slug" | "children" | "mobile" | "dense" | "popover">
36
+ setHoverSession: (id: string | undefined) => void
37
+ }
38
+
39
+ export const ProjectDragOverlay = (props: {
40
+ projects: Accessor<LocalProject[]>
41
+ activeProject: Accessor<string | undefined>
42
+ }): JSX.Element => {
43
+ const project = createMemo(() => props.projects().find((p) => p.worktree === props.activeProject()))
44
+ return (
45
+ <Show when={project()}>
46
+ {(p) => (
47
+ <div class="bg-background-base rounded-xl p-1">
48
+ <ProjectIcon project={p()} />
49
+ </div>
50
+ )}
51
+ </Show>
52
+ )
53
+ }
54
+
55
+ const ProjectTile = (props: {
56
+ project: LocalProject
57
+ mobile?: boolean
58
+ nav: Accessor<HTMLElement | undefined>
59
+ sidebarHovering: Accessor<boolean>
60
+ selected: Accessor<boolean>
61
+ active: Accessor<boolean>
62
+ overlay: Accessor<boolean>
63
+ suppressHover: Accessor<boolean>
64
+ dirs: Accessor<string[]>
65
+ onProjectMouseEnter: (worktree: string, event: MouseEvent) => void
66
+ onProjectMouseLeave: (worktree: string) => void
67
+ onProjectFocus: (worktree: string) => void
68
+ navigateToProject: (directory: string) => void
69
+ showEditProjectDialog: (project: LocalProject) => void
70
+ toggleProjectWorkspaces: (project: LocalProject) => void
71
+ workspacesEnabled: (project: LocalProject) => boolean
72
+ closeProject: (directory: string) => void
73
+ setMenu: (value: boolean) => void
74
+ setOpen: (value: boolean) => void
75
+ setSuppressHover: (value: boolean) => void
76
+ language: ReturnType<typeof useLanguage>
77
+ }): JSX.Element => {
78
+ const notification = useNotification()
79
+ const layout = useLayout()
80
+ const unseenCount = createMemo(() =>
81
+ props.dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0),
82
+ )
83
+
84
+ const clear = () =>
85
+ props
86
+ .dirs()
87
+ .filter((directory) => notification.project.unseenCount(directory) > 0)
88
+ .forEach((directory) => notification.project.markViewed(directory))
89
+
90
+ return (
91
+ <ContextMenu
92
+ modal={!props.sidebarHovering()}
93
+ onOpenChange={(value) => {
94
+ props.setMenu(value)
95
+ props.setSuppressHover(value)
96
+ if (value) props.setOpen(false)
97
+ }}
98
+ >
99
+ <ContextMenu.Trigger
100
+ as="button"
101
+ type="button"
102
+ aria-label={displayName(props.project)}
103
+ data-action="project-switch"
104
+ data-project={base64Encode(props.project.worktree)}
105
+ classList={{
106
+ "flex items-center justify-center size-10 p-1 rounded-lg overflow-hidden transition-colors cursor-default": true,
107
+ "bg-transparent border-2 border-icon-strong-base hover:bg-surface-base-hover": props.selected(),
108
+ "bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base":
109
+ !props.selected() && !props.active(),
110
+ "bg-surface-base-hover border border-border-weak-base": !props.selected() && props.active(),
111
+ }}
112
+ onPointerDown={(event) => {
113
+ if (event.button === 0 && !event.ctrlKey) {
114
+ props.setOpen(false)
115
+ props.setSuppressHover(true)
116
+ return
117
+ }
118
+ if (!props.overlay()) return
119
+ if (event.button !== 2 && !(event.button === 0 && event.ctrlKey)) return
120
+ props.setOpen(false)
121
+ props.setSuppressHover(true)
122
+ event.preventDefault()
123
+ }}
124
+ onMouseEnter={(event: MouseEvent) => {
125
+ if (!props.overlay()) return
126
+ if (props.suppressHover()) return
127
+ props.onProjectMouseEnter(props.project.worktree, event)
128
+ }}
129
+ onMouseLeave={() => {
130
+ if (props.suppressHover()) props.setSuppressHover(false)
131
+ if (!props.overlay()) return
132
+ props.onProjectMouseLeave(props.project.worktree)
133
+ }}
134
+ onFocus={() => {
135
+ if (!props.overlay()) return
136
+ if (props.suppressHover()) return
137
+ props.onProjectFocus(props.project.worktree)
138
+ }}
139
+ onClick={() => {
140
+ props.setOpen(false)
141
+ if (props.selected()) {
142
+ layout.sidebar.toggle()
143
+ return
144
+ }
145
+ props.navigateToProject(props.project.worktree)
146
+ }}
147
+ onBlur={() => props.setOpen(false)}
148
+ >
149
+ <ProjectIcon project={props.project} notify />
150
+ </ContextMenu.Trigger>
151
+ <ContextMenu.Portal>
152
+ <ContextMenu.Content>
153
+ <ContextMenu.Item onSelect={() => props.showEditProjectDialog(props.project)}>
154
+ <ContextMenu.ItemLabel>{props.language.t("common.edit")}</ContextMenu.ItemLabel>
155
+ </ContextMenu.Item>
156
+ <ContextMenu.Item
157
+ data-action="project-workspaces-toggle"
158
+ data-project={base64Encode(props.project.worktree)}
159
+ disabled={props.project.vcs !== "git" && !props.workspacesEnabled(props.project)}
160
+ onSelect={() => props.toggleProjectWorkspaces(props.project)}
161
+ >
162
+ <ContextMenu.ItemLabel>
163
+ {props.workspacesEnabled(props.project)
164
+ ? props.language.t("sidebar.workspaces.disable")
165
+ : props.language.t("sidebar.workspaces.enable")}
166
+ </ContextMenu.ItemLabel>
167
+ </ContextMenu.Item>
168
+ <ContextMenu.Item
169
+ data-action="project-clear-notifications"
170
+ data-project={base64Encode(props.project.worktree)}
171
+ disabled={unseenCount() === 0}
172
+ onSelect={clear}
173
+ >
174
+ <ContextMenu.ItemLabel>{props.language.t("sidebar.project.clearNotifications")}</ContextMenu.ItemLabel>
175
+ </ContextMenu.Item>
176
+ <ContextMenu.Separator />
177
+ <ContextMenu.Item
178
+ data-action="project-close-menu"
179
+ data-project={base64Encode(props.project.worktree)}
180
+ onSelect={() => props.closeProject(props.project.worktree)}
181
+ >
182
+ <ContextMenu.ItemLabel>{props.language.t("common.close")}</ContextMenu.ItemLabel>
183
+ </ContextMenu.Item>
184
+ </ContextMenu.Content>
185
+ </ContextMenu.Portal>
186
+ </ContextMenu>
187
+ )
188
+ }
189
+
190
+ const ProjectPreviewPanel = (props: {
191
+ project: LocalProject
192
+ mobile?: boolean
193
+ selected: Accessor<boolean>
194
+ workspaceEnabled: Accessor<boolean>
195
+ workspaces: Accessor<string[]>
196
+ label: (directory: string) => string
197
+ projectSessions: Accessor<ReturnType<typeof sortedRootSessions>>
198
+ projectChildren: Accessor<Map<string, string[]>>
199
+ workspaceSessions: (directory: string) => ReturnType<typeof sortedRootSessions>
200
+ workspaceChildren: (directory: string) => Map<string, string[]>
201
+ ctx: ProjectSidebarContext
202
+ language: ReturnType<typeof useLanguage>
203
+ }): JSX.Element => (
204
+ <div class="-m-3 p-2 flex flex-col w-72">
205
+ <div class="px-4 pt-2 pb-1 flex items-center gap-2">
206
+ <div class="text-14-medium text-text-strong truncate grow">{displayName(props.project)}</div>
207
+ </div>
208
+ <div class="px-4 pb-2 text-12-medium text-text-weak">{props.language.t("sidebar.project.recentSessions")}</div>
209
+ <div class="px-2 pb-2 flex flex-col gap-2">
210
+ <Show
211
+ when={props.workspaceEnabled()}
212
+ fallback={
213
+ <For each={props.projectSessions().slice(0, 2)}>
214
+ {(session) => (
215
+ <SessionItem
216
+ {...props.ctx.sessionProps}
217
+ session={session}
218
+ list={props.projectSessions()}
219
+ slug={base64Encode(props.project.worktree)}
220
+ dense
221
+ mobile={props.mobile}
222
+ popover={false}
223
+ children={props.projectChildren()}
224
+ />
225
+ )}
226
+ </For>
227
+ }
228
+ >
229
+ <For each={props.workspaces()}>
230
+ {(directory) => {
231
+ const sessions = createMemo(() => props.workspaceSessions(directory))
232
+ const children = createMemo(() => props.workspaceChildren(directory))
233
+ return (
234
+ <div class="flex flex-col gap-1">
235
+ <div class="px-2 py-0.5 flex items-center gap-1 min-w-0">
236
+ <div class="shrink-0 size-6 flex items-center justify-center">
237
+ <Icon name="branch" size="small" class="text-icon-base" />
238
+ </div>
239
+ <span class="truncate text-14-medium text-text-base">{props.label(directory)}</span>
240
+ </div>
241
+ <For each={sessions().slice(0, 2)}>
242
+ {(session) => (
243
+ <SessionItem
244
+ {...props.ctx.sessionProps}
245
+ session={session}
246
+ list={sessions()}
247
+ slug={base64Encode(directory)}
248
+ dense
249
+ mobile={props.mobile}
250
+ popover={false}
251
+ children={children()}
252
+ />
253
+ )}
254
+ </For>
255
+ </div>
256
+ )
257
+ }}
258
+ </For>
259
+ </Show>
260
+ </div>
261
+ <div class="px-2 py-2 border-t border-border-weak-base">
262
+ <Button
263
+ variant="ghost"
264
+ class="flex w-full text-left justify-start text-text-base px-2 hover:bg-transparent active:bg-transparent"
265
+ onClick={() => {
266
+ props.ctx.openSidebar()
267
+ props.ctx.onHoverOpenChanged(props.project.worktree, false)
268
+ if (props.selected()) return
269
+ props.ctx.navigateToProject(props.project.worktree)
270
+ }}
271
+ >
272
+ {props.language.t("sidebar.project.viewAllSessions")}
273
+ </Button>
274
+ </div>
275
+ </div>
276
+ )
277
+
278
+ export const SortableProject = (props: {
279
+ project: LocalProject
280
+ mobile?: boolean
281
+ ctx: ProjectSidebarContext
282
+ sortNow: Accessor<number>
283
+ }): JSX.Element => {
284
+ const globalSync = useGlobalSync()
285
+ const language = useLanguage()
286
+ const sortable = createSortable(props.project.worktree)
287
+ const selected = createMemo(() => props.ctx.currentProject()?.worktree === props.project.worktree)
288
+ const workspaces = createMemo(() => props.ctx.workspaceIds(props.project).slice(0, 2))
289
+ const workspaceEnabled = createMemo(() => props.ctx.workspacesEnabled(props.project))
290
+ const dirs = createMemo(() => props.ctx.workspaceIds(props.project))
291
+ const [state, setState] = createStore({
292
+ menu: false,
293
+ suppressHover: false,
294
+ })
295
+
296
+ const isHoverProject = () => props.ctx.hoverProject() === props.project.worktree
297
+ const preview = createMemo(() => !props.mobile && props.ctx.sidebarOpened())
298
+ const overlay = createMemo(() => !props.mobile && !props.ctx.sidebarOpened())
299
+ const active = createMemo(() => state.menu || (preview() ? isHoverProject() : overlay() && isHoverProject()))
300
+
301
+ const hoverOpen = () => isHoverProject() && preview() && !selected() && !state.menu
302
+
303
+ const label = (directory: string) => {
304
+ const [data] = globalSync.child(directory, { bootstrap: false })
305
+ const kind =
306
+ directory === props.project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox")
307
+ const name = props.ctx.workspaceLabel(directory, data.vcs?.branch, props.project.id)
308
+ return `${kind} : ${name}`
309
+ }
310
+
311
+ const projectStore = createMemo(() => globalSync.child(props.project.worktree, { bootstrap: false })[0])
312
+ const projectSessions = createMemo(() => sortedRootSessions(projectStore(), props.sortNow()))
313
+ const projectChildren = createMemo(() => childMapByParent(projectStore().session))
314
+ const workspaceSessions = (directory: string) => {
315
+ const [data] = globalSync.child(directory, { bootstrap: false })
316
+ return sortedRootSessions(data, props.sortNow())
317
+ }
318
+ const workspaceChildren = (directory: string) => {
319
+ const [data] = globalSync.child(directory, { bootstrap: false })
320
+ return childMapByParent(data.session)
321
+ }
322
+ const tile = () => (
323
+ <ProjectTile
324
+ project={props.project}
325
+ mobile={props.mobile}
326
+ nav={props.ctx.nav}
327
+ sidebarHovering={props.ctx.sidebarHovering}
328
+ selected={selected}
329
+ active={active}
330
+ overlay={overlay}
331
+ suppressHover={() => state.suppressHover}
332
+ dirs={dirs}
333
+ onProjectMouseEnter={props.ctx.onProjectMouseEnter}
334
+ onProjectMouseLeave={props.ctx.onProjectMouseLeave}
335
+ onProjectFocus={props.ctx.onProjectFocus}
336
+ navigateToProject={props.ctx.navigateToProject}
337
+ showEditProjectDialog={props.ctx.showEditProjectDialog}
338
+ toggleProjectWorkspaces={props.ctx.toggleProjectWorkspaces}
339
+ workspacesEnabled={props.ctx.workspacesEnabled}
340
+ closeProject={props.ctx.closeProject}
341
+ setMenu={(value) => setState("menu", value)}
342
+ setOpen={(value) => props.ctx.onHoverOpenChanged(props.project.worktree, value)}
343
+ setSuppressHover={(value) => setState("suppressHover", value)}
344
+ language={language}
345
+ />
346
+ )
347
+
348
+ return (
349
+ // @ts-ignore
350
+ <div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
351
+ <Show when={preview() && !selected()} fallback={tile()}>
352
+ <HoverCard
353
+ open={!state.suppressHover && hoverOpen() && !state.menu}
354
+ openDelay={0}
355
+ closeDelay={0}
356
+ placement="right-start"
357
+ gutter={6}
358
+ trigger={tile()}
359
+ onOpenChange={(value) => {
360
+ if (state.menu) return
361
+ if (value && state.suppressHover) return
362
+ props.ctx.onHoverOpenChanged(props.project.worktree, value)
363
+ if (value) props.ctx.setHoverSession(undefined)
364
+ }}
365
+ >
366
+ <ProjectPreviewPanel
367
+ project={props.project}
368
+ mobile={props.mobile}
369
+ selected={selected}
370
+ workspaceEnabled={workspaceEnabled}
371
+ workspaces={workspaces}
372
+ label={label}
373
+ projectSessions={projectSessions}
374
+ projectChildren={projectChildren}
375
+ workspaceSessions={workspaceSessions}
376
+ workspaceChildren={workspaceChildren}
377
+ ctx={props.ctx}
378
+ language={language}
379
+ />
380
+ </HoverCard>
381
+ </Show>
382
+ </div>
383
+ )
384
+ }
@@ -0,0 +1,125 @@
1
+ import { createEffect, createMemo, For, Show, type Accessor, type JSX } from "solid-js"
2
+ import {
3
+ DragDropProvider,
4
+ DragDropSensors,
5
+ DragOverlay,
6
+ SortableProvider,
7
+ closestCenter,
8
+ type DragEvent,
9
+ } from "@thisbeyond/solid-dnd"
10
+ import { ConstrainDragXAxis } from "@/utils/solid-dnd"
11
+ import { IconButton } from "@reign-labs/ui/icon-button"
12
+ import { Tooltip, TooltipKeybind } from "@reign-labs/ui/tooltip"
13
+ import { type LocalProject } from "@/context/layout"
14
+
15
+ export const SidebarContent = (props: {
16
+ mobile?: boolean
17
+ opened: Accessor<boolean>
18
+ aimMove: (event: MouseEvent) => void
19
+ projects: Accessor<LocalProject[]>
20
+ renderProject: (project: LocalProject) => JSX.Element
21
+ handleDragStart: (event: unknown) => void
22
+ handleDragEnd: () => void
23
+ handleDragOver: (event: DragEvent) => void
24
+ openProjectLabel: JSX.Element
25
+ openProjectKeybind: Accessor<string | undefined>
26
+ onOpenProject: () => void
27
+ renderProjectOverlay: () => JSX.Element
28
+ settingsLabel: Accessor<string>
29
+ settingsKeybind: Accessor<string | undefined>
30
+ onOpenSettings: () => void
31
+ helpLabel: Accessor<string>
32
+ onOpenHelp: () => void
33
+ renderPanel: () => JSX.Element
34
+ }): JSX.Element => {
35
+ const expanded = createMemo(() => !!props.mobile || props.opened())
36
+ const placement = () => (props.mobile ? "bottom" : "right")
37
+ let panel: HTMLDivElement | undefined
38
+
39
+ createEffect(() => {
40
+ const el = panel
41
+ if (!el) return
42
+ if (expanded()) {
43
+ el.removeAttribute("inert")
44
+ return
45
+ }
46
+ el.setAttribute("inert", "")
47
+ })
48
+
49
+ return (
50
+ <div class="flex h-full w-full min-w-0 overflow-hidden">
51
+ <div
52
+ data-component="sidebar-rail"
53
+ class="w-16 shrink-0 bg-background-base flex flex-col items-center overflow-hidden"
54
+ onMouseMove={props.aimMove}
55
+ >
56
+ <div class="flex-1 min-h-0 w-full">
57
+ <DragDropProvider
58
+ onDragStart={props.handleDragStart}
59
+ onDragEnd={props.handleDragEnd}
60
+ onDragOver={props.handleDragOver}
61
+ collisionDetector={closestCenter}
62
+ >
63
+ <DragDropSensors />
64
+ <ConstrainDragXAxis />
65
+ <div class="h-full w-full flex flex-col items-center gap-3 px-3 py-3 overflow-y-auto no-scrollbar">
66
+ <SortableProvider ids={props.projects().map((p) => p.worktree)}>
67
+ <For each={props.projects()}>{(project) => props.renderProject(project)}</For>
68
+ </SortableProvider>
69
+ <Tooltip
70
+ placement={placement()}
71
+ value={
72
+ <div class="flex items-center gap-2">
73
+ <span>{props.openProjectLabel}</span>
74
+ <Show when={!props.mobile && !!props.openProjectKeybind()}>
75
+ <span class="text-icon-base text-12-medium">{props.openProjectKeybind()}</span>
76
+ </Show>
77
+ </div>
78
+ }
79
+ >
80
+ <IconButton
81
+ icon="plus"
82
+ variant="ghost"
83
+ size="large"
84
+ onClick={props.onOpenProject}
85
+ aria-label={typeof props.openProjectLabel === "string" ? props.openProjectLabel : undefined}
86
+ />
87
+ </Tooltip>
88
+ </div>
89
+ <DragOverlay>{props.renderProjectOverlay()}</DragOverlay>
90
+ </DragDropProvider>
91
+ </div>
92
+ <div class="shrink-0 w-full pt-3 pb-6 flex flex-col items-center gap-2">
93
+ <TooltipKeybind placement={placement()} title={props.settingsLabel()} keybind={props.settingsKeybind() ?? ""}>
94
+ <IconButton
95
+ icon="settings-gear"
96
+ variant="ghost"
97
+ size="large"
98
+ onClick={props.onOpenSettings}
99
+ aria-label={props.settingsLabel()}
100
+ />
101
+ </TooltipKeybind>
102
+ <Tooltip placement={placement()} value={props.helpLabel()}>
103
+ <IconButton
104
+ icon="help"
105
+ variant="ghost"
106
+ size="large"
107
+ onClick={props.onOpenHelp}
108
+ aria-label={props.helpLabel()}
109
+ />
110
+ </Tooltip>
111
+ </div>
112
+ </div>
113
+
114
+ <div
115
+ ref={(el) => {
116
+ panel = el
117
+ }}
118
+ classList={{ "flex-1 flex h-full min-h-0 min-w-0 overflow-hidden": true, "pointer-events-none": !expanded() }}
119
+ aria-hidden={!expanded()}
120
+ >
121
+ {props.renderPanel()}
122
+ </div>
123
+ </div>
124
+ )
125
+ }