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,1013 @@
1
+ import { For, createEffect, createMemo, on, onCleanup, Show, Index, type JSX, createSignal } from "solid-js"
2
+ import { createStore, produce } from "solid-js/store"
3
+ import { useNavigate } from "@solidjs/router"
4
+ import { useMutation } from "@tanstack/solid-query"
5
+ import { Button } from "@reign-labs/ui/button"
6
+ import { FileIcon } from "@reign-labs/ui/file-icon"
7
+ import { Icon } from "@reign-labs/ui/icon"
8
+ import { IconButton } from "@reign-labs/ui/icon-button"
9
+ import { DropdownMenu } from "@reign-labs/ui/dropdown-menu"
10
+ import { Dialog } from "@reign-labs/ui/dialog"
11
+ import { InlineInput } from "@reign-labs/ui/inline-input"
12
+ import { Spinner } from "@reign-labs/ui/spinner"
13
+ import { SessionTurn } from "@reign-labs/ui/session-turn"
14
+ import { ScrollView } from "@reign-labs/ui/scroll-view"
15
+ import { TextField } from "@reign-labs/ui/text-field"
16
+ import type { AssistantMessage, Message as MessageType, Part, TextPart, UserMessage } from "@reign-labs/sdk/v2"
17
+ import { showToast } from "@reign-labs/ui/toast"
18
+ import { Binary } from "@reign-labs/util/binary"
19
+ import { getFilename } from "@reign-labs/util/path"
20
+ import { Popover as KobaltePopover } from "@kobalte/core/popover"
21
+ import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture"
22
+ import { SessionContextUsage } from "@/components/session-context-usage"
23
+ import { useDialog } from "@reign-labs/ui/context/dialog"
24
+ import { useLanguage } from "@/context/language"
25
+ import { useSessionKey } from "@/pages/session/session-layout"
26
+ import { useGlobalSDK } from "@/context/global-sdk"
27
+ import { usePlatform } from "@/context/platform"
28
+ import { useSettings } from "@/context/settings"
29
+ import { useSDK } from "@/context/sdk"
30
+ import { useSync } from "@/context/sync"
31
+ import { messageAgentColor } from "@/utils/agent"
32
+ import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note"
33
+ import { makeTimer } from "@solid-primitives/timer"
34
+
35
+ type MessageComment = {
36
+ path: string
37
+ comment: string
38
+ selection?: {
39
+ startLine: number
40
+ endLine: number
41
+ }
42
+ }
43
+
44
+ const emptyMessages: MessageType[] = []
45
+ const idle = { type: "idle" as const }
46
+
47
+ type UserActions = {
48
+ fork?: (input: { sessionID: string; messageID: string }) => Promise<void> | void
49
+ revert?: (input: { sessionID: string; messageID: string }) => Promise<void> | void
50
+ }
51
+
52
+ const messageComments = (parts: Part[]): MessageComment[] =>
53
+ parts.flatMap((part) => {
54
+ if (part.type !== "text" || !(part as TextPart).synthetic) return []
55
+ const next = readCommentMetadata(part.metadata) ?? parseCommentNote(part.text)
56
+ if (!next) return []
57
+ return [
58
+ {
59
+ path: next.path,
60
+ comment: next.comment,
61
+ selection: next.selection
62
+ ? {
63
+ startLine: next.selection.startLine,
64
+ endLine: next.selection.endLine,
65
+ }
66
+ : undefined,
67
+ },
68
+ ]
69
+ })
70
+
71
+ const boundaryTarget = (root: HTMLElement, target: EventTarget | null) => {
72
+ const current = target instanceof Element ? target : undefined
73
+ const nested = current?.closest("[data-scrollable]")
74
+ if (!nested || nested === root) return root
75
+ if (!(nested instanceof HTMLElement)) return root
76
+ return nested
77
+ }
78
+
79
+ const markBoundaryGesture = (input: {
80
+ root: HTMLDivElement
81
+ target: EventTarget | null
82
+ delta: number
83
+ onMarkScrollGesture: (target?: EventTarget | null) => void
84
+ }) => {
85
+ const target = boundaryTarget(input.root, input.target)
86
+ if (target === input.root) {
87
+ input.onMarkScrollGesture(input.root)
88
+ return
89
+ }
90
+ if (
91
+ shouldMarkBoundaryGesture({
92
+ delta: input.delta,
93
+ scrollTop: target.scrollTop,
94
+ scrollHeight: target.scrollHeight,
95
+ clientHeight: target.clientHeight,
96
+ })
97
+ ) {
98
+ input.onMarkScrollGesture(input.root)
99
+ }
100
+ }
101
+
102
+ type StageConfig = {
103
+ init: number
104
+ batch: number
105
+ }
106
+
107
+ type TimelineStageInput = {
108
+ sessionKey: () => string
109
+ turnStart: () => number
110
+ messages: () => UserMessage[]
111
+ config: StageConfig
112
+ }
113
+
114
+ /**
115
+ * Defer-mounts small timeline windows so revealing older turns does not
116
+ * block first paint with a large DOM mount.
117
+ *
118
+ * Once staging completes for a session it never re-stages — backfill and
119
+ * new messages render immediately.
120
+ */
121
+ function createTimelineStaging(input: TimelineStageInput) {
122
+ const [state, setState] = createStore({
123
+ activeSession: "",
124
+ completedSession: "",
125
+ count: 0,
126
+ })
127
+
128
+ const stagedCount = createMemo(() => {
129
+ const total = input.messages().length
130
+ if (input.turnStart() <= 0) return total
131
+ if (state.completedSession === input.sessionKey()) return total
132
+ const init = Math.min(total, input.config.init)
133
+ if (state.count <= init) return init
134
+ if (state.count >= total) return total
135
+ return state.count
136
+ })
137
+
138
+ const stagedUserMessages = createMemo(() => {
139
+ const list = input.messages()
140
+ const count = stagedCount()
141
+ if (count >= list.length) return list
142
+ return list.slice(Math.max(0, list.length - count))
143
+ })
144
+
145
+ let frame: number | undefined
146
+ const cancel = () => {
147
+ if (frame === undefined) return
148
+ cancelAnimationFrame(frame)
149
+ frame = undefined
150
+ }
151
+
152
+ createEffect(
153
+ on(
154
+ () => [input.sessionKey(), input.turnStart() > 0, input.messages().length] as const,
155
+ ([sessionKey, isWindowed, total]) => {
156
+ cancel()
157
+ const shouldStage =
158
+ isWindowed &&
159
+ total > input.config.init &&
160
+ state.completedSession !== sessionKey &&
161
+ state.activeSession !== sessionKey
162
+ if (!shouldStage) {
163
+ setState({ activeSession: "", count: total })
164
+ return
165
+ }
166
+
167
+ let count = Math.min(total, input.config.init)
168
+ setState({ activeSession: sessionKey, count })
169
+
170
+ const step = () => {
171
+ if (input.sessionKey() !== sessionKey) {
172
+ frame = undefined
173
+ return
174
+ }
175
+ const currentTotal = input.messages().length
176
+ count = Math.min(currentTotal, count + input.config.batch)
177
+ setState("count", count)
178
+ if (count >= currentTotal) {
179
+ setState({ completedSession: sessionKey, activeSession: "" })
180
+ frame = undefined
181
+ return
182
+ }
183
+ frame = requestAnimationFrame(step)
184
+ }
185
+ frame = requestAnimationFrame(step)
186
+ },
187
+ ),
188
+ )
189
+
190
+ const isStaging = createMemo(() => {
191
+ const key = input.sessionKey()
192
+ return state.activeSession === key && state.completedSession !== key
193
+ })
194
+
195
+ onCleanup(cancel)
196
+ return { messages: stagedUserMessages, isStaging }
197
+ }
198
+
199
+ export function MessageTimeline(props: {
200
+ mobileChanges: boolean
201
+ mobileFallback: JSX.Element
202
+ actions?: UserActions
203
+ scroll: { overflow: boolean; bottom: boolean }
204
+ onResumeScroll: () => void
205
+ setScrollRef: (el: HTMLDivElement | undefined) => void
206
+ onScheduleScrollState: (el: HTMLDivElement) => void
207
+ onAutoScrollHandleScroll: () => void
208
+ onMarkScrollGesture: (target?: EventTarget | null) => void
209
+ hasScrollGesture: () => boolean
210
+ onUserScroll: () => void
211
+ onTurnBackfillScroll: () => void
212
+ onAutoScrollInteraction: (event: MouseEvent) => void
213
+ centered: boolean
214
+ setContentRef: (el: HTMLDivElement) => void
215
+ turnStart: number
216
+ historyMore: boolean
217
+ historyLoading: boolean
218
+ onLoadEarlier: () => void
219
+ renderedUserMessages: UserMessage[]
220
+ anchor: (id: string) => string
221
+ }) {
222
+ let touchGesture: number | undefined
223
+
224
+ const navigate = useNavigate()
225
+ const globalSDK = useGlobalSDK()
226
+ const sdk = useSDK()
227
+ const sync = useSync()
228
+ const settings = useSettings()
229
+ const dialog = useDialog()
230
+ const language = useLanguage()
231
+ const { params, sessionKey } = useSessionKey()
232
+ const platform = usePlatform()
233
+
234
+ const rendered = createMemo(() => props.renderedUserMessages.map((message) => message.id))
235
+ const sessionID = createMemo(() => params.id)
236
+ const sessionMessages = createMemo(() => {
237
+ const id = sessionID()
238
+ if (!id) return emptyMessages
239
+ return sync.data.message[id] ?? emptyMessages
240
+ })
241
+ const pending = createMemo(() =>
242
+ sessionMessages().findLast(
243
+ (item): item is AssistantMessage => item.role === "assistant" && typeof item.time.completed !== "number",
244
+ ),
245
+ )
246
+ const sessionStatus = createMemo(() => {
247
+ const id = sessionID()
248
+ if (!id) return idle
249
+ return sync.data.session_status[id] ?? idle
250
+ })
251
+ const working = createMemo(() => !!pending() || sessionStatus().type !== "idle")
252
+ const tint = createMemo(() => messageAgentColor(sessionMessages(), sync.data.agent))
253
+
254
+ const [timeoutDone, setTimeoutDone] = createSignal(true)
255
+
256
+ const workingStatus = createMemo<"hidden" | "showing" | "hiding">((prev) => {
257
+ if (working()) return "showing"
258
+ if (prev === "showing" || !timeoutDone()) return "hiding"
259
+ return "hidden"
260
+ })
261
+
262
+ createEffect(() => {
263
+ if (workingStatus() !== "hiding") return
264
+
265
+ setTimeoutDone(false)
266
+ makeTimer(() => setTimeoutDone(true), 260, setTimeout)
267
+ })
268
+
269
+ const activeMessageID = createMemo(() => {
270
+ const parentID = pending()?.parentID
271
+ if (parentID) {
272
+ const messages = sessionMessages()
273
+ const result = Binary.search(messages, parentID, (message) => message.id)
274
+ const message = result.found ? messages[result.index] : messages.find((item) => item.id === parentID)
275
+ if (message && message.role === "user") return message.id
276
+ }
277
+
278
+ const status = sessionStatus()
279
+ if (status.type !== "idle") {
280
+ const messages = sessionMessages()
281
+ for (let i = messages.length - 1; i >= 0; i--) {
282
+ if (messages[i].role === "user") return messages[i].id
283
+ }
284
+ }
285
+
286
+ return undefined
287
+ })
288
+ const info = createMemo(() => {
289
+ const id = sessionID()
290
+ if (!id) return
291
+ return sync.session.get(id)
292
+ })
293
+ const titleValue = createMemo(() => info()?.title)
294
+ const shareUrl = createMemo(() => info()?.share?.url)
295
+ const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
296
+ const parentID = createMemo(() => info()?.parentID)
297
+ const showHeader = createMemo(() => !!(titleValue() || parentID()))
298
+ const stageCfg = { init: 1, batch: 3 }
299
+ const staging = createTimelineStaging({
300
+ sessionKey,
301
+ turnStart: () => props.turnStart,
302
+ messages: () => props.renderedUserMessages,
303
+ config: stageCfg,
304
+ })
305
+
306
+ const [title, setTitle] = createStore({
307
+ draft: "",
308
+ editing: false,
309
+ menuOpen: false,
310
+ pendingRename: false,
311
+ pendingShare: false,
312
+ })
313
+ let titleRef: HTMLInputElement | undefined
314
+
315
+ const [share, setShare] = createStore({
316
+ open: false,
317
+ dismiss: null as "escape" | "outside" | null,
318
+ })
319
+
320
+ let more: HTMLButtonElement | undefined
321
+
322
+ const viewShare = () => {
323
+ const url = shareUrl()
324
+ if (!url) return
325
+ platform.openLink(url)
326
+ }
327
+
328
+ const errorMessage = (err: unknown) => {
329
+ if (err && typeof err === "object" && "data" in err) {
330
+ const data = (err as { data?: { message?: string } }).data
331
+ if (data?.message) return data.message
332
+ }
333
+ if (err instanceof Error) return err.message
334
+ return language.t("common.requestFailed")
335
+ }
336
+
337
+ const shareMutation = useMutation(() => ({
338
+ mutationFn: (id: string) => globalSDK.client.session.share({ sessionID: id, directory: sdk.directory }),
339
+ onError: (err) => {
340
+ console.error("Failed to share session", err)
341
+ },
342
+ }))
343
+
344
+ const unshareMutation = useMutation(() => ({
345
+ mutationFn: (id: string) => globalSDK.client.session.unshare({ sessionID: id, directory: sdk.directory }),
346
+ onError: (err) => {
347
+ console.error("Failed to unshare session", err)
348
+ },
349
+ }))
350
+
351
+ const titleMutation = useMutation(() => ({
352
+ mutationFn: (input: { id: string; title: string }) =>
353
+ sdk.client.session.update({ sessionID: input.id, title: input.title }),
354
+ onSuccess: (_, input) => {
355
+ sync.set(
356
+ produce((draft) => {
357
+ const index = draft.session.findIndex((s) => s.id === input.id)
358
+ if (index !== -1) draft.session[index].title = input.title
359
+ }),
360
+ )
361
+ setTitle("editing", false)
362
+ },
363
+ onError: (err) => {
364
+ showToast({
365
+ title: language.t("common.requestFailed"),
366
+ description: errorMessage(err),
367
+ })
368
+ },
369
+ }))
370
+
371
+ const shareSession = () => {
372
+ const id = sessionID()
373
+ if (!id || shareMutation.isPending) return
374
+ if (!shareEnabled()) return
375
+ shareMutation.mutate(id)
376
+ }
377
+
378
+ const unshareSession = () => {
379
+ const id = sessionID()
380
+ if (!id || unshareMutation.isPending) return
381
+ if (!shareEnabled()) return
382
+ unshareMutation.mutate(id)
383
+ }
384
+
385
+ createEffect(
386
+ on(
387
+ sessionKey,
388
+ () =>
389
+ setTitle({
390
+ draft: "",
391
+ editing: false,
392
+ menuOpen: false,
393
+ pendingRename: false,
394
+ pendingShare: false,
395
+ }),
396
+ { defer: true },
397
+ ),
398
+ )
399
+
400
+ const openTitleEditor = () => {
401
+ if (!sessionID()) return
402
+ setTitle({ editing: true, draft: titleValue() ?? "" })
403
+ requestAnimationFrame(() => {
404
+ titleRef?.focus()
405
+ titleRef?.select()
406
+ })
407
+ }
408
+
409
+ const closeTitleEditor = () => {
410
+ if (titleMutation.isPending) return
411
+ setTitle("editing", false)
412
+ }
413
+
414
+ const saveTitleEditor = () => {
415
+ const id = sessionID()
416
+ if (!id) return
417
+ if (titleMutation.isPending) return
418
+
419
+ const next = title.draft.trim()
420
+ if (!next || next === (titleValue() ?? "")) {
421
+ setTitle("editing", false)
422
+ return
423
+ }
424
+
425
+ titleMutation.mutate({ id, title: next })
426
+ }
427
+
428
+ const navigateAfterSessionRemoval = (sessionID: string, parentID?: string, nextSessionID?: string) => {
429
+ if (params.id !== sessionID) return
430
+ if (parentID) {
431
+ navigate(`/${params.dir}/session/${parentID}`)
432
+ return
433
+ }
434
+ if (nextSessionID) {
435
+ navigate(`/${params.dir}/session/${nextSessionID}`)
436
+ return
437
+ }
438
+ navigate(`/${params.dir}/session`)
439
+ }
440
+
441
+ const archiveSession = async (sessionID: string) => {
442
+ const session = sync.session.get(sessionID)
443
+ if (!session) return
444
+
445
+ const sessions = sync.data.session ?? []
446
+ const index = sessions.findIndex((s) => s.id === sessionID)
447
+ const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
448
+
449
+ await sdk.client.session
450
+ .update({ sessionID, time: { archived: Date.now() } })
451
+ .then(() => {
452
+ sync.set(
453
+ produce((draft) => {
454
+ const index = draft.session.findIndex((s) => s.id === sessionID)
455
+ if (index !== -1) draft.session.splice(index, 1)
456
+ }),
457
+ )
458
+ navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
459
+ })
460
+ .catch((err) => {
461
+ showToast({
462
+ title: language.t("common.requestFailed"),
463
+ description: errorMessage(err),
464
+ })
465
+ })
466
+ }
467
+
468
+ const deleteSession = async (sessionID: string) => {
469
+ const session = sync.session.get(sessionID)
470
+ if (!session) return false
471
+
472
+ const sessions = (sync.data.session ?? []).filter((s) => !s.parentID && !s.time?.archived)
473
+ const index = sessions.findIndex((s) => s.id === sessionID)
474
+ const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
475
+
476
+ const result = await sdk.client.session
477
+ .delete({ sessionID })
478
+ .then((x) => x.data)
479
+ .catch((err) => {
480
+ showToast({
481
+ title: language.t("session.delete.failed.title"),
482
+ description: errorMessage(err),
483
+ })
484
+ return false
485
+ })
486
+
487
+ if (!result) return false
488
+
489
+ sync.set(
490
+ produce((draft) => {
491
+ const removed = new Set<string>([sessionID])
492
+
493
+ const byParent = new Map<string, string[]>()
494
+ for (const item of draft.session) {
495
+ const parentID = item.parentID
496
+ if (!parentID) continue
497
+ const existing = byParent.get(parentID)
498
+ if (existing) {
499
+ existing.push(item.id)
500
+ continue
501
+ }
502
+ byParent.set(parentID, [item.id])
503
+ }
504
+
505
+ const stack = [sessionID]
506
+ while (stack.length) {
507
+ const parentID = stack.pop()
508
+ if (!parentID) continue
509
+
510
+ const children = byParent.get(parentID)
511
+ if (!children) continue
512
+
513
+ for (const child of children) {
514
+ if (removed.has(child)) continue
515
+ removed.add(child)
516
+ stack.push(child)
517
+ }
518
+ }
519
+
520
+ draft.session = draft.session.filter((s) => !removed.has(s.id))
521
+ }),
522
+ )
523
+
524
+ navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
525
+ return true
526
+ }
527
+
528
+ const navigateParent = () => {
529
+ const id = parentID()
530
+ if (!id) return
531
+ navigate(`/${params.dir}/session/${id}`)
532
+ }
533
+
534
+ function DialogDeleteSession(props: { sessionID: string }) {
535
+ const name = createMemo(() => sync.session.get(props.sessionID)?.title ?? language.t("command.session.new"))
536
+ const handleDelete = async () => {
537
+ await deleteSession(props.sessionID)
538
+ dialog.close()
539
+ }
540
+
541
+ return (
542
+ <Dialog title={language.t("session.delete.title")} fit>
543
+ <div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
544
+ <div class="flex flex-col gap-1">
545
+ <span class="text-14-regular text-text-strong">
546
+ {language.t("session.delete.confirm", { name: name() })}
547
+ </span>
548
+ </div>
549
+ <div class="flex justify-end gap-2">
550
+ <Button variant="ghost" size="large" onClick={() => dialog.close()}>
551
+ {language.t("common.cancel")}
552
+ </Button>
553
+ <Button variant="primary" size="large" onClick={handleDelete}>
554
+ {language.t("session.delete.button")}
555
+ </Button>
556
+ </div>
557
+ </div>
558
+ </Dialog>
559
+ )
560
+ }
561
+
562
+ return (
563
+ <Show
564
+ when={!props.mobileChanges}
565
+ fallback={<div class="relative h-full overflow-hidden">{props.mobileFallback}</div>}
566
+ >
567
+ <div class="relative w-full h-full min-w-0">
568
+ <div
569
+ class="absolute left-1/2 -translate-x-1/2 bottom-6 z-[60] pointer-events-none transition-all duration-200 ease-out"
570
+ classList={{
571
+ "opacity-100 translate-y-0 scale-100":
572
+ props.scroll.overflow && !props.scroll.bottom && !staging.isStaging(),
573
+ "opacity-0 translate-y-2 scale-95 pointer-events-none":
574
+ !props.scroll.overflow || props.scroll.bottom || staging.isStaging(),
575
+ }}
576
+ >
577
+ <button
578
+ class="pointer-events-auto size-8 flex items-center justify-center rounded-full bg-background-base border border-border-base shadow-sm text-text-base hover:bg-background-stronger transition-colors"
579
+ onClick={props.onResumeScroll}
580
+ >
581
+ <Icon name="arrow-down-to-line" />
582
+ </button>
583
+ </div>
584
+ <ScrollView
585
+ viewportRef={props.setScrollRef}
586
+ onWheel={(e) => {
587
+ const root = e.currentTarget
588
+ const delta = normalizeWheelDelta({
589
+ deltaY: e.deltaY,
590
+ deltaMode: e.deltaMode,
591
+ rootHeight: root.clientHeight,
592
+ })
593
+ if (!delta) return
594
+ markBoundaryGesture({ root, target: e.target, delta, onMarkScrollGesture: props.onMarkScrollGesture })
595
+ }}
596
+ onTouchStart={(e) => {
597
+ touchGesture = e.touches[0]?.clientY
598
+ }}
599
+ onTouchMove={(e) => {
600
+ const next = e.touches[0]?.clientY
601
+ const prev = touchGesture
602
+ touchGesture = next
603
+ if (next === undefined || prev === undefined) return
604
+
605
+ const delta = prev - next
606
+ if (!delta) return
607
+
608
+ const root = e.currentTarget
609
+ markBoundaryGesture({ root, target: e.target, delta, onMarkScrollGesture: props.onMarkScrollGesture })
610
+ }}
611
+ onTouchEnd={() => {
612
+ touchGesture = undefined
613
+ }}
614
+ onTouchCancel={() => {
615
+ touchGesture = undefined
616
+ }}
617
+ onPointerDown={(e) => {
618
+ if (e.target !== e.currentTarget) return
619
+ props.onMarkScrollGesture(e.currentTarget)
620
+ }}
621
+ onScroll={(e) => {
622
+ props.onScheduleScrollState(e.currentTarget)
623
+ props.onTurnBackfillScroll()
624
+ if (!props.hasScrollGesture()) return
625
+ props.onUserScroll()
626
+ props.onAutoScrollHandleScroll()
627
+ props.onMarkScrollGesture(e.currentTarget)
628
+ }}
629
+ onClick={props.onAutoScrollInteraction}
630
+ class="relative min-w-0 w-full h-full"
631
+ style={{
632
+ "--session-title-height": showHeader() ? "40px" : "0px",
633
+ "--sticky-accordion-top": showHeader() ? "48px" : "0px",
634
+ }}
635
+ >
636
+ <div ref={props.setContentRef} class="min-w-0 w-full">
637
+ <Show when={showHeader()}>
638
+ <div
639
+ data-session-title
640
+ classList={{
641
+ "sticky top-0 z-30 bg-[linear-gradient(to_bottom,var(--background-stronger)_48px,transparent)]": true,
642
+ "w-full": true,
643
+ "pb-4": true,
644
+ "pl-2 pr-3 md:pl-4 md:pr-3": true,
645
+ "md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
646
+ }}
647
+ >
648
+ <div class="h-12 w-full flex items-center justify-between gap-2">
649
+ <div class="flex items-center gap-1 min-w-0 flex-1 pr-3">
650
+ <Show when={parentID()}>
651
+ <IconButton
652
+ tabIndex={-1}
653
+ icon="arrow-left"
654
+ variant="ghost"
655
+ onClick={navigateParent}
656
+ aria-label={language.t("common.goBack")}
657
+ />
658
+ </Show>
659
+ <div class="flex items-center min-w-0 grow-1">
660
+ <div
661
+ class="shrink-0 flex items-center justify-center overflow-hidden transition-[width,margin] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)]"
662
+ style={{
663
+ width: working() ? "16px" : "0px",
664
+ "margin-right": working() ? "8px" : "0px",
665
+ }}
666
+ aria-hidden="true"
667
+ >
668
+ <Show when={workingStatus() !== "hidden"}>
669
+ <div
670
+ class="transition-opacity duration-200 ease-out"
671
+ classList={{ "opacity-0": workingStatus() === "hiding" }}
672
+ >
673
+ <Spinner class="size-4" style={{ color: tint() ?? "var(--icon-interactive-base)" }} />
674
+ </div>
675
+ </Show>
676
+ </div>
677
+ <Show when={titleValue() || title.editing}>
678
+ <Show
679
+ when={title.editing}
680
+ fallback={
681
+ <h1
682
+ class="text-14-medium text-text-strong truncate grow-1 min-w-0"
683
+ onDblClick={openTitleEditor}
684
+ >
685
+ {titleValue()}
686
+ </h1>
687
+ }
688
+ >
689
+ <InlineInput
690
+ ref={(el) => {
691
+ titleRef = el
692
+ }}
693
+ value={title.draft}
694
+ disabled={titleMutation.isPending}
695
+ class="text-14-medium text-text-strong grow-1 min-w-0 rounded-[6px]"
696
+ style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }}
697
+ onInput={(event) => setTitle("draft", event.currentTarget.value)}
698
+ onKeyDown={(event) => {
699
+ event.stopPropagation()
700
+ if (event.key === "Enter") {
701
+ event.preventDefault()
702
+ void saveTitleEditor()
703
+ return
704
+ }
705
+ if (event.key === "Escape") {
706
+ event.preventDefault()
707
+ closeTitleEditor()
708
+ }
709
+ }}
710
+ onBlur={closeTitleEditor}
711
+ />
712
+ </Show>
713
+ </Show>
714
+ </div>
715
+ </div>
716
+ <Show when={sessionID()}>
717
+ {(id) => (
718
+ <div class="shrink-0 flex items-center gap-3">
719
+ <SessionContextUsage placement="bottom" />
720
+ <DropdownMenu
721
+ gutter={4}
722
+ placement="bottom-end"
723
+ open={title.menuOpen}
724
+ onOpenChange={(open) => {
725
+ setTitle("menuOpen", open)
726
+ if (open) return
727
+ }}
728
+ >
729
+ <DropdownMenu.Trigger
730
+ as={IconButton}
731
+ icon="dot-grid"
732
+ variant="ghost"
733
+ class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
734
+ classList={{
735
+ "bg-surface-base-active": share.open || title.pendingShare,
736
+ }}
737
+ aria-label={language.t("common.moreOptions")}
738
+ aria-expanded={title.menuOpen || share.open || title.pendingShare}
739
+ ref={(el: HTMLButtonElement) => {
740
+ more = el
741
+ }}
742
+ />
743
+ <DropdownMenu.Portal>
744
+ <DropdownMenu.Content
745
+ style={{ "min-width": "104px" }}
746
+ onCloseAutoFocus={(event) => {
747
+ if (title.pendingRename) {
748
+ event.preventDefault()
749
+ setTitle("pendingRename", false)
750
+ openTitleEditor()
751
+ return
752
+ }
753
+ if (title.pendingShare) {
754
+ event.preventDefault()
755
+ requestAnimationFrame(() => {
756
+ setShare({ open: true, dismiss: null })
757
+ setTitle("pendingShare", false)
758
+ })
759
+ }
760
+ }}
761
+ >
762
+ <DropdownMenu.Item
763
+ onSelect={() => {
764
+ setTitle("pendingRename", true)
765
+ setTitle("menuOpen", false)
766
+ }}
767
+ >
768
+ <DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
769
+ </DropdownMenu.Item>
770
+ <Show when={shareEnabled()}>
771
+ <DropdownMenu.Item
772
+ onSelect={() => {
773
+ setTitle({ pendingShare: true, menuOpen: false })
774
+ }}
775
+ >
776
+ <DropdownMenu.ItemLabel>
777
+ {language.t("session.share.action.share")}
778
+ </DropdownMenu.ItemLabel>
779
+ </DropdownMenu.Item>
780
+ </Show>
781
+ <DropdownMenu.Item onSelect={() => void archiveSession(id())}>
782
+ <DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
783
+ </DropdownMenu.Item>
784
+ <DropdownMenu.Separator />
785
+ <DropdownMenu.Item
786
+ onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id()} />)}
787
+ >
788
+ <DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
789
+ </DropdownMenu.Item>
790
+ </DropdownMenu.Content>
791
+ </DropdownMenu.Portal>
792
+ </DropdownMenu>
793
+
794
+ <KobaltePopover
795
+ open={share.open}
796
+ anchorRef={() => more}
797
+ placement="bottom-end"
798
+ gutter={4}
799
+ modal={false}
800
+ onOpenChange={(open) => {
801
+ if (open) setShare("dismiss", null)
802
+ setShare("open", open)
803
+ }}
804
+ >
805
+ <KobaltePopover.Portal>
806
+ <KobaltePopover.Content
807
+ data-component="popover-content"
808
+ style={{ "min-width": "320px" }}
809
+ onEscapeKeyDown={(event) => {
810
+ setShare({ dismiss: "escape", open: false })
811
+ event.preventDefault()
812
+ event.stopPropagation()
813
+ }}
814
+ onPointerDownOutside={() => {
815
+ setShare({ dismiss: "outside", open: false })
816
+ }}
817
+ onFocusOutside={() => {
818
+ setShare({ dismiss: "outside", open: false })
819
+ }}
820
+ onCloseAutoFocus={(event) => {
821
+ if (share.dismiss === "outside") event.preventDefault()
822
+ setShare("dismiss", null)
823
+ }}
824
+ >
825
+ <div class="flex flex-col p-3">
826
+ <div class="flex flex-col gap-1">
827
+ <div class="text-13-medium text-text-strong">
828
+ {language.t("session.share.popover.title")}
829
+ </div>
830
+ <div class="text-12-regular text-text-weak">
831
+ {shareUrl()
832
+ ? language.t("session.share.popover.description.shared")
833
+ : language.t("session.share.popover.description.unshared")}
834
+ </div>
835
+ </div>
836
+ <div class="mt-3 flex flex-col gap-2">
837
+ <Show
838
+ when={shareUrl()}
839
+ fallback={
840
+ <Button
841
+ size="large"
842
+ variant="primary"
843
+ class="w-full"
844
+ onClick={shareSession}
845
+ disabled={shareMutation.isPending}
846
+ >
847
+ {shareMutation.isPending
848
+ ? language.t("session.share.action.publishing")
849
+ : language.t("session.share.action.publish")}
850
+ </Button>
851
+ }
852
+ >
853
+ <div class="flex flex-col gap-2">
854
+ <TextField
855
+ value={shareUrl() ?? ""}
856
+ readOnly
857
+ copyable
858
+ copyKind="link"
859
+ tabIndex={-1}
860
+ class="w-full"
861
+ />
862
+ <div class="grid grid-cols-2 gap-2">
863
+ <Button
864
+ size="large"
865
+ variant="secondary"
866
+ class="w-full shadow-none border border-border-weak-base"
867
+ onClick={unshareSession}
868
+ disabled={unshareMutation.isPending}
869
+ >
870
+ {unshareMutation.isPending
871
+ ? language.t("session.share.action.unpublishing")
872
+ : language.t("session.share.action.unpublish")}
873
+ </Button>
874
+ <Button
875
+ size="large"
876
+ variant="primary"
877
+ class="w-full"
878
+ onClick={viewShare}
879
+ disabled={unshareMutation.isPending}
880
+ >
881
+ {language.t("session.share.action.view")}
882
+ </Button>
883
+ </div>
884
+ </div>
885
+ </Show>
886
+ </div>
887
+ </div>
888
+ </KobaltePopover.Content>
889
+ </KobaltePopover.Portal>
890
+ </KobaltePopover>
891
+ </div>
892
+ )}
893
+ </Show>
894
+ </div>
895
+ </div>
896
+ </Show>
897
+ <div
898
+ role="log"
899
+ class="flex flex-col gap-12 items-start justify-start pb-16 transition-[margin]"
900
+ classList={{
901
+ "w-full": true,
902
+ "md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
903
+ "mt-0.5": props.centered,
904
+ "mt-0": !props.centered,
905
+ }}
906
+ >
907
+ <Show when={props.turnStart > 0 || props.historyMore}>
908
+ <div class="w-full flex justify-center">
909
+ <Button
910
+ variant="ghost"
911
+ size="large"
912
+ class="text-12-medium opacity-50"
913
+ disabled={props.historyLoading}
914
+ onClick={props.onLoadEarlier}
915
+ >
916
+ {props.historyLoading
917
+ ? language.t("session.messages.loadingEarlier")
918
+ : language.t("session.messages.loadEarlier")}
919
+ </Button>
920
+ </div>
921
+ </Show>
922
+ <For each={rendered()}>
923
+ {(messageID) => {
924
+ const active = createMemo(() => activeMessageID() === messageID)
925
+ const comments = createMemo(() => messageComments(sync.data.part[messageID] ?? []), [], {
926
+ equals: (a, b) =>
927
+ a.length === b.length &&
928
+ a.every(
929
+ (c, i) =>
930
+ c.path === b[i].path &&
931
+ c.comment === b[i].comment &&
932
+ c.selection?.startLine === b[i].selection?.startLine &&
933
+ c.selection?.endLine === b[i].selection?.endLine,
934
+ ),
935
+ })
936
+ const commentCount = createMemo(() => comments().length)
937
+ return (
938
+ <div
939
+ id={props.anchor(messageID)}
940
+ data-message-id={messageID}
941
+ classList={{
942
+ "min-w-0 w-full max-w-full": true,
943
+ "md:max-w-200 2xl:max-w-[1000px]": props.centered,
944
+ }}
945
+ style={{ "content-visibility": "auto", "contain-intrinsic-size": "auto 500px" }}
946
+ >
947
+ <Show when={commentCount() > 0}>
948
+ <div class="w-full px-4 md:px-5 pb-2">
949
+ <div class="ml-auto max-w-[82%] overflow-x-auto no-scrollbar">
950
+ <div class="flex w-max min-w-full justify-end gap-2">
951
+ <Index each={comments()}>
952
+ {(commentAccessor: () => MessageComment) => {
953
+ const comment = createMemo(() => commentAccessor())
954
+ return (
955
+ <Show when={comment()}>
956
+ {(c) => (
957
+ <div class="shrink-0 max-w-[260px] rounded-[6px] border border-border-weak-base bg-background-stronger px-2.5 py-2">
958
+ <div class="flex items-center gap-1.5 min-w-0 text-11-medium text-text-strong">
959
+ <FileIcon
960
+ node={{ path: c().path, type: "file" }}
961
+ class="size-3.5 shrink-0"
962
+ />
963
+ <span class="truncate">{getFilename(c().path)}</span>
964
+ <Show when={c().selection}>
965
+ {(selection) => (
966
+ <span class="shrink-0 text-text-weak">
967
+ {selection().startLine === selection().endLine
968
+ ? `:${selection().startLine}`
969
+ : `:${selection().startLine}-${selection().endLine}`}
970
+ </span>
971
+ )}
972
+ </Show>
973
+ </div>
974
+ <div class="pt-1 text-12-regular text-text-strong whitespace-pre-wrap break-words">
975
+ {c().comment}
976
+ </div>
977
+ </div>
978
+ )}
979
+ </Show>
980
+ )
981
+ }}
982
+ </Index>
983
+ </div>
984
+ </div>
985
+ </div>
986
+ </Show>
987
+ <SessionTurn
988
+ sessionID={sessionID() ?? ""}
989
+ messageID={messageID}
990
+ messages={sessionMessages()}
991
+ actions={props.actions}
992
+ active={active()}
993
+ status={active() ? sessionStatus() : undefined}
994
+ showReasoningSummaries={settings.general.showReasoningSummaries()}
995
+ shellToolDefaultOpen={settings.general.shellToolPartsExpanded()}
996
+ editToolDefaultOpen={settings.general.editToolPartsExpanded()}
997
+ classes={{
998
+ root: "min-w-0 w-full relative",
999
+ content: "flex flex-col justify-between !overflow-visible",
1000
+ container: "w-full px-4 md:px-5",
1001
+ }}
1002
+ />
1003
+ </div>
1004
+ )
1005
+ }}
1006
+ </For>
1007
+ </div>
1008
+ </div>
1009
+ </ScrollView>
1010
+ </div>
1011
+ </Show>
1012
+ )
1013
+ }