@neurocode-ai/session-ui 1.18.8

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 (114) hide show
  1. package/package.json +71 -0
  2. package/src/components/apply-patch-file.test.ts +43 -0
  3. package/src/components/apply-patch-file.ts +78 -0
  4. package/src/components/basic-tool.css +358 -0
  5. package/src/components/basic-tool.stories.tsx +133 -0
  6. package/src/components/basic-tool.tsx +343 -0
  7. package/src/components/dock-prompt.stories.tsx +62 -0
  8. package/src/components/dock-prompt.tsx +23 -0
  9. package/src/components/file-media.tsx +291 -0
  10. package/src/components/file-search.tsx +72 -0
  11. package/src/components/file-ssr.tsx +197 -0
  12. package/src/components/file.css +46 -0
  13. package/src/components/file.tsx +1202 -0
  14. package/src/components/line-comment-annotations.tsx +637 -0
  15. package/src/components/line-comment-styles.ts +292 -0
  16. package/src/components/line-comment.stories.tsx +115 -0
  17. package/src/components/line-comment.tsx +439 -0
  18. package/src/components/markdown-cache.tsx +78 -0
  19. package/src/components/markdown-code-state.test.ts +32 -0
  20. package/src/components/markdown-code-state.ts +22 -0
  21. package/src/components/markdown-inline-code-kind.test.ts +46 -0
  22. package/src/components/markdown-inline-code-kind.ts +1915 -0
  23. package/src/components/markdown-preload.test.ts +18 -0
  24. package/src/components/markdown-shiki.worker.ts +104 -0
  25. package/src/components/markdown-stream.test.ts +194 -0
  26. package/src/components/markdown-stream.ts +110 -0
  27. package/src/components/markdown-worker-protocol.test.ts +81 -0
  28. package/src/components/markdown-worker-protocol.ts +48 -0
  29. package/src/components/markdown-worker-queue.test.ts +49 -0
  30. package/src/components/markdown-worker-queue.ts +64 -0
  31. package/src/components/markdown-worker-transport.test.ts +56 -0
  32. package/src/components/markdown-worker-transport.ts +41 -0
  33. package/src/components/markdown-worker.ts +122 -0
  34. package/src/components/markdown.css +377 -0
  35. package/src/components/markdown.stories.tsx +53 -0
  36. package/src/components/markdown.tsx +678 -0
  37. package/src/components/message-file.test.ts +63 -0
  38. package/src/components/message-file.ts +34 -0
  39. package/src/components/message-nav.css +127 -0
  40. package/src/components/message-nav.stories.tsx +7 -0
  41. package/src/components/message-nav.tsx +102 -0
  42. package/src/components/message-part-text.ts +3 -0
  43. package/src/components/message-part.css +1569 -0
  44. package/src/components/message-part.stories.tsx +7 -0
  45. package/src/components/message-part.test.ts +28 -0
  46. package/src/components/message-part.tsx +2662 -0
  47. package/src/components/session-diff.test.ts +135 -0
  48. package/src/components/session-diff.ts +145 -0
  49. package/src/components/session-retry.tsx +74 -0
  50. package/src/components/session-review.css +247 -0
  51. package/src/components/session-review.stories.tsx +7 -0
  52. package/src/components/session-review.tsx +657 -0
  53. package/src/components/session-turn.css +241 -0
  54. package/src/components/session-turn.stories.tsx +7 -0
  55. package/src/components/session-turn.tsx +543 -0
  56. package/src/components/shell-submessage-motion.stories.tsx +346 -0
  57. package/src/components/shell-submessage.css +23 -0
  58. package/src/components/timeline-playground.stories.tsx +2090 -0
  59. package/src/components/tool-count-label.css +57 -0
  60. package/src/components/tool-count-label.tsx +58 -0
  61. package/src/components/tool-count-summary.css +102 -0
  62. package/src/components/tool-count-summary.stories.tsx +238 -0
  63. package/src/components/tool-count-summary.tsx +52 -0
  64. package/src/components/tool-error-card.css +91 -0
  65. package/src/components/tool-error-card.stories.tsx +92 -0
  66. package/src/components/tool-error-card.tsx +157 -0
  67. package/src/components/tool-status-title.css +89 -0
  68. package/src/components/tool-status-title.tsx +133 -0
  69. package/src/context/data.tsx +64 -0
  70. package/src/context/index.ts +1 -0
  71. package/src/pierre/comment-hover.ts +83 -0
  72. package/src/pierre/commented-lines.ts +91 -0
  73. package/src/pierre/diff-selection.ts +71 -0
  74. package/src/pierre/file-find.ts +485 -0
  75. package/src/pierre/file-runtime.ts +114 -0
  76. package/src/pierre/file-selection.ts +85 -0
  77. package/src/pierre/index.ts +186 -0
  78. package/src/pierre/media.ts +110 -0
  79. package/src/pierre/selection-bridge.ts +132 -0
  80. package/src/pierre/virtualizer.ts +100 -0
  81. package/src/pierre/worker.ts +52 -0
  82. package/src/styles/index.css +14 -0
  83. package/src/v2/components/attachment-card-v2.css +70 -0
  84. package/src/v2/components/attachment-card-v2.tsx +33 -0
  85. package/src/v2/components/basic-tool-v2.css +163 -0
  86. package/src/v2/components/basic-tool-v2.stories.tsx +137 -0
  87. package/src/v2/components/basic-tool-v2.tsx +139 -0
  88. package/src/v2/components/comment-card-v2.tsx +64 -0
  89. package/src/v2/components/line-comment-annotations-v2.tsx +220 -0
  90. package/src/v2/components/prompt-input/attachments.ts +266 -0
  91. package/src/v2/components/prompt-input/index.tsx +706 -0
  92. package/src/v2/components/prompt-input/interaction.ts +482 -0
  93. package/src/v2/components/prompt-input/machine.test.ts +164 -0
  94. package/src/v2/components/prompt-input/machine.ts +261 -0
  95. package/src/v2/components/prompt-input/prompt-input.stories.tsx +221 -0
  96. package/src/v2/components/prompt-input/store.test.ts +116 -0
  97. package/src/v2/components/prompt-input/store.ts +152 -0
  98. package/src/v2/components/prompt-input/types.ts +106 -0
  99. package/src/v2/components/session-file-panel-v2.tsx +43 -0
  100. package/src/v2/components/session-progress-indicator-v2.css +875 -0
  101. package/src/v2/components/session-progress-indicator-v2.stories.tsx +66 -0
  102. package/src/v2/components/session-progress-indicator-v2.tsx +32 -0
  103. package/src/v2/components/session-review-empty-changes-v2.tsx +17 -0
  104. package/src/v2/components/session-review-empty-no-git-v2.tsx +28 -0
  105. package/src/v2/components/session-review-file-preview-v2-virtualize.test.ts +13 -0
  106. package/src/v2/components/session-review-file-preview-v2-virtualize.ts +5 -0
  107. package/src/v2/components/session-review-file-preview-v2.tsx +290 -0
  108. package/src/v2/components/session-review-v2.css +458 -0
  109. package/src/v2/components/session-review-v2.tsx +340 -0
  110. package/src/v2/components/tool-error-card-v2.css +200 -0
  111. package/src/v2/components/tool-error-card-v2.stories.tsx +91 -0
  112. package/src/v2/components/tool-error-card-v2.tsx +166 -0
  113. package/sst-env.d.ts +10 -0
  114. package/tsconfig.json +19 -0
@@ -0,0 +1,637 @@
1
+ import { type DiffLineAnnotation, type SelectedLineRange } from "@pierre/diffs"
2
+ import { createEffect, createMemo, createSignal, onCleanup, Show, type Accessor, type JSX } from "solid-js"
3
+ import { createStore } from "solid-js/store"
4
+ import { render as renderSolid } from "solid-js/web"
5
+ import { useI18n } from "@neurocode-ai/ui/context/i18n"
6
+ import { createHoverCommentUtility } from "../pierre/comment-hover"
7
+ import { cloneSelectedLineRange, formatSelectedLineLabel, lineInSelectedRange } from "../pierre/selection-bridge"
8
+ import { LineComment, LineCommentEditor, type LineCommentEditorProps } from "./line-comment"
9
+
10
+ export type LineCommentAnnotationMeta<T> =
11
+ | { kind: "comment"; key: string; comment: T }
12
+ | { kind: "draft"; key: string; range: SelectedLineRange }
13
+
14
+ export type LineCommentAnnotation<T> = {
15
+ lineNumber: number
16
+ side?: "additions" | "deletions"
17
+ metadata: LineCommentAnnotationMeta<T>
18
+ }
19
+
20
+ type LineCommentAnnotationsProps<T> = {
21
+ comments: Accessor<T[]>
22
+ getCommentId: (comment: T) => string
23
+ getCommentSelection: (comment: T) => SelectedLineRange
24
+ draftRange: Accessor<SelectedLineRange | null>
25
+ draftKey: Accessor<string>
26
+ }
27
+
28
+ type LineCommentAnnotationsWithSideProps<T> = LineCommentAnnotationsProps<T> & {
29
+ getSide: (range: SelectedLineRange) => "additions" | "deletions"
30
+ }
31
+
32
+ type HoverCommentLine = {
33
+ lineNumber: number
34
+ side?: "additions" | "deletions"
35
+ }
36
+
37
+ export type LineCommentStateProps<T> = {
38
+ opened: Accessor<T | null>
39
+ setOpened: (id: T | null) => void
40
+ selected: Accessor<SelectedLineRange | null>
41
+ setSelected: (range: SelectedLineRange | null) => void
42
+ commenting: Accessor<SelectedLineRange | null>
43
+ setCommenting: (range: SelectedLineRange | null) => void
44
+ syncSelected?: (range: SelectedLineRange | null) => void
45
+ hoverSelected?: (range: SelectedLineRange) => void
46
+ }
47
+
48
+ export type LineCommentShape = {
49
+ id: string
50
+ selection: SelectedLineRange
51
+ comment: string
52
+ }
53
+
54
+ type LineCommentControllerProps<T extends LineCommentShape> = {
55
+ comments: Accessor<T[]>
56
+ draftKey: Accessor<string>
57
+ label: string
58
+ mention?: LineCommentEditorProps["mention"]
59
+ state: LineCommentStateProps<string>
60
+ onSubmit: (input: { comment: string; selection: SelectedLineRange }) => void
61
+ onUpdate?: (input: { id: string; comment: string; selection: SelectedLineRange }) => void
62
+ onDelete?: (comment: T) => void
63
+ renderCommentActions?: (comment: T, controls: { edit: VoidFunction; remove: VoidFunction }) => JSX.Element
64
+ editSubmitLabel?: string
65
+ onDraftPopoverFocusOut?: JSX.EventHandlerUnion<HTMLDivElement, FocusEvent>
66
+ getHoverSelectedRange?: Accessor<SelectedLineRange | null>
67
+ cancelDraftOnCommentToggle?: boolean
68
+ clearSelectionOnSelectionEndNull?: boolean
69
+ }
70
+
71
+ type LineCommentControllerWithSideProps<T extends LineCommentShape> = LineCommentControllerProps<T> & {
72
+ getSide: (range: SelectedLineRange) => "additions" | "deletions"
73
+ }
74
+
75
+ type CommentProps = {
76
+ id?: string
77
+ open: boolean
78
+ comment: JSX.Element
79
+ selection: JSX.Element
80
+ actions?: JSX.Element
81
+ editor?: DraftProps
82
+ onClick?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>
83
+ onMouseEnter?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>
84
+ }
85
+
86
+ type DraftProps = {
87
+ value: string
88
+ selection: JSX.Element
89
+ mention?: LineCommentEditorProps["mention"]
90
+ onInput: (value: string) => void
91
+ onCancel: VoidFunction
92
+ onSubmit: (value: string) => void
93
+ onPopoverFocusOut?: JSX.EventHandlerUnion<HTMLDivElement, FocusEvent>
94
+ cancelLabel?: string
95
+ submitLabel?: string
96
+ }
97
+
98
+ // Generic host machinery shared by the v1 and v2 annotation renderers: each
99
+ // annotation key gets a detached DOM host with its own Solid root, updated in
100
+ // place through a signal so Pierre can reparent the host without re-rendering.
101
+ export function createLineCommentAnnotationRenderer<T, C, D>(props: {
102
+ renderComment: (comment: T) => C
103
+ renderDraft: (range: SelectedLineRange) => D
104
+ commentElement: (view: Accessor<C>) => JSX.Element
105
+ draftElement: (view: Accessor<D>) => JSX.Element
106
+ }) {
107
+ const nodes = new Map<
108
+ string,
109
+ {
110
+ host: HTMLDivElement
111
+ dispose: VoidFunction
112
+ setMeta: (meta: LineCommentAnnotationMeta<T>) => void
113
+ }
114
+ >()
115
+
116
+ const mount = (meta: LineCommentAnnotationMeta<T>) => {
117
+ if (typeof document === "undefined") return
118
+
119
+ const host = document.createElement("div")
120
+ host.setAttribute("data-prevent-autofocus", "")
121
+ const [current, setCurrent] = createSignal(meta)
122
+
123
+ const dispose = renderSolid(() => {
124
+ const active = current()
125
+ if (active.kind === "comment") {
126
+ const view = createMemo(() => {
127
+ const next = current()
128
+ if (next.kind !== "comment") return props.renderComment(active.comment)
129
+ return props.renderComment(next.comment)
130
+ })
131
+ return props.commentElement(view)
132
+ }
133
+
134
+ const view = createMemo(() => {
135
+ const next = current()
136
+ if (next.kind !== "draft") return props.renderDraft(active.range)
137
+ return props.renderDraft(next.range)
138
+ })
139
+ return props.draftElement(view)
140
+ }, host)
141
+
142
+ const node = { host, dispose, setMeta: setCurrent }
143
+ nodes.set(meta.key, node)
144
+ return node
145
+ }
146
+
147
+ const render = <A extends { metadata: LineCommentAnnotationMeta<T> }>(annotation: A) => {
148
+ const meta = annotation.metadata
149
+ const node = nodes.get(meta.key) ?? mount(meta)
150
+ if (!node) return
151
+ node.setMeta(meta)
152
+ return node.host
153
+ }
154
+
155
+ const reconcile = <A extends { metadata: LineCommentAnnotationMeta<T> }>(annotations: A[]) => {
156
+ const next = new Set(annotations.map((annotation) => annotation.metadata.key))
157
+ for (const [key, node] of nodes) {
158
+ if (next.has(key)) continue
159
+ node.dispose()
160
+ nodes.delete(key)
161
+ }
162
+ }
163
+
164
+ const cleanup = () => {
165
+ for (const [, node] of nodes) node.dispose()
166
+ nodes.clear()
167
+ }
168
+
169
+ return { render, reconcile, cleanup }
170
+ }
171
+
172
+ function lineCommentElement(view: Accessor<CommentProps>) {
173
+ return (
174
+ <Show
175
+ when={view().editor}
176
+ fallback={
177
+ <LineComment
178
+ inline
179
+ id={view().id}
180
+ open={view().open}
181
+ comment={view().comment}
182
+ selection={view().selection}
183
+ actions={view().actions}
184
+ onClick={view().onClick}
185
+ onMouseEnter={view().onMouseEnter}
186
+ />
187
+ }
188
+ >
189
+ <LineCommentEditor
190
+ inline
191
+ id={view().id}
192
+ value={view().editor!.value}
193
+ selection={view().editor!.selection}
194
+ onInput={view().editor!.onInput}
195
+ onCancel={view().editor!.onCancel}
196
+ onSubmit={view().editor!.onSubmit}
197
+ onPopoverFocusOut={view().editor!.onPopoverFocusOut}
198
+ cancelLabel={view().editor!.cancelLabel}
199
+ submitLabel={view().editor!.submitLabel}
200
+ mention={view().editor!.mention}
201
+ />
202
+ </Show>
203
+ )
204
+ }
205
+
206
+ function lineCommentDraftElement(view: Accessor<DraftProps>) {
207
+ return (
208
+ <LineCommentEditor
209
+ inline
210
+ value={view().value}
211
+ selection={view().selection}
212
+ onInput={view().onInput}
213
+ onCancel={view().onCancel}
214
+ onSubmit={view().onSubmit}
215
+ onPopoverFocusOut={view().onPopoverFocusOut}
216
+ mention={view().mention}
217
+ />
218
+ )
219
+ }
220
+
221
+ export function createLineCommentState<T>(props: LineCommentStateProps<T>) {
222
+ const [state, setState] = createStore({
223
+ draft: "",
224
+ editing: null as T | null,
225
+ })
226
+ const draft = () => state.draft
227
+ const setDraft = (value: string) => setState("draft", value)
228
+ const editing = () => state.editing
229
+ const setEditing = (value: T | null) => setState("editing", typeof value === "function" ? () => value : value)
230
+
231
+ const toRange = (range: SelectedLineRange | null) => (range ? cloneSelectedLineRange(range) : null)
232
+ const setSelected = (range: SelectedLineRange | null) => {
233
+ const next = toRange(range)
234
+ props.setSelected(next)
235
+ props.syncSelected?.(toRange(next))
236
+ return next
237
+ }
238
+
239
+ const setCommenting = (range: SelectedLineRange | null) => {
240
+ const next = toRange(range)
241
+ props.setCommenting(next)
242
+ return next
243
+ }
244
+
245
+ const closeComment = () => {
246
+ props.setOpened(null)
247
+ }
248
+
249
+ const cancelDraft = () => {
250
+ setDraft("")
251
+ setEditing(null)
252
+ setCommenting(null)
253
+ }
254
+
255
+ const reset = () => {
256
+ setDraft("")
257
+ setEditing(null)
258
+ props.setOpened(null)
259
+ props.setSelected(null)
260
+ props.setCommenting(null)
261
+ }
262
+
263
+ const openComment = (id: T, range: SelectedLineRange, options?: { cancelDraft?: boolean }) => {
264
+ if (options?.cancelDraft) cancelDraft()
265
+ props.setOpened(id)
266
+ setSelected(range)
267
+ }
268
+
269
+ const toggleComment = (id: T, range: SelectedLineRange, options?: { cancelDraft?: boolean }) => {
270
+ if (options?.cancelDraft) cancelDraft()
271
+ const next = props.opened() === id ? null : id
272
+ props.setOpened(next)
273
+ setSelected(range)
274
+ }
275
+
276
+ const openDraft = (range: SelectedLineRange) => {
277
+ const next = toRange(range)
278
+ setDraft("")
279
+ setEditing(null)
280
+ closeComment()
281
+ setSelected(next)
282
+ setCommenting(next)
283
+ }
284
+
285
+ const openEditor = (id: T, range: SelectedLineRange, value: string) => {
286
+ closeComment()
287
+ setSelected(range)
288
+ props.setCommenting(null)
289
+ setEditing(id)
290
+ setDraft(value)
291
+ }
292
+
293
+ const hoverComment = (range: SelectedLineRange) => {
294
+ const next = toRange(range)
295
+ if (!next) return
296
+ if (props.hoverSelected) {
297
+ props.hoverSelected(next)
298
+ return
299
+ }
300
+
301
+ setSelected(next)
302
+ }
303
+
304
+ return {
305
+ draft,
306
+ setDraft,
307
+ editing,
308
+ opened: props.opened,
309
+ selected: props.selected,
310
+ commenting: props.commenting,
311
+ isOpen: (id: T) => props.opened() === id,
312
+ isEditing: (id: T) => editing() === id,
313
+ closeComment,
314
+ openComment,
315
+ toggleComment,
316
+ openDraft,
317
+ openEditor,
318
+ hoverComment,
319
+ cancelDraft,
320
+ select: setSelected,
321
+ reset,
322
+ }
323
+ }
324
+
325
+ export function createLineCommentController<T extends LineCommentShape>(
326
+ props: LineCommentControllerWithSideProps<T>,
327
+ ): {
328
+ note: ReturnType<typeof createLineCommentState<string>>
329
+ annotations: Accessor<DiffLineAnnotation<LineCommentAnnotationMeta<T>>[]>
330
+ renderAnnotation: ReturnType<
331
+ typeof createManagedLineCommentAnnotationRenderer<T, CommentProps, DraftProps>
332
+ >["renderAnnotation"]
333
+ renderGutterUtility: ReturnType<typeof createLineCommentGutterRenderer>
334
+ onLineSelected: (range: SelectedLineRange | null) => void
335
+ onLineSelectionEnd: (range: SelectedLineRange | null) => void
336
+ }
337
+ export function createLineCommentController<T extends LineCommentShape>(
338
+ props: LineCommentControllerProps<T>,
339
+ ): {
340
+ note: ReturnType<typeof createLineCommentState<string>>
341
+ annotations: Accessor<LineCommentAnnotation<T>[]>
342
+ renderAnnotation: ReturnType<
343
+ typeof createManagedLineCommentAnnotationRenderer<T, CommentProps, DraftProps>
344
+ >["renderAnnotation"]
345
+ renderGutterUtility: ReturnType<typeof createLineCommentGutterRenderer>
346
+ onLineSelected: (range: SelectedLineRange | null) => void
347
+ onLineSelectionEnd: (range: SelectedLineRange | null) => void
348
+ }
349
+ export function createLineCommentController<T extends LineCommentShape>(
350
+ props: LineCommentControllerProps<T> | LineCommentControllerWithSideProps<T>,
351
+ ) {
352
+ const i18n = useI18n()
353
+ const note = createLineCommentState<string>(props.state)
354
+
355
+ const annotations =
356
+ "getSide" in props
357
+ ? createLineCommentAnnotations({
358
+ comments: props.comments,
359
+ getCommentId: (comment) => comment.id,
360
+ getCommentSelection: (comment) => comment.selection,
361
+ draftRange: note.commenting,
362
+ draftKey: props.draftKey,
363
+ getSide: props.getSide,
364
+ })
365
+ : createLineCommentAnnotations({
366
+ comments: props.comments,
367
+ getCommentId: (comment) => comment.id,
368
+ getCommentSelection: (comment) => comment.selection,
369
+ draftRange: note.commenting,
370
+ draftKey: props.draftKey,
371
+ })
372
+
373
+ const { renderAnnotation } = createManagedLineCommentAnnotationRenderer<T, CommentProps, DraftProps>({
374
+ annotations,
375
+ commentElement: lineCommentElement,
376
+ draftElement: lineCommentDraftElement,
377
+ renderComment: (comment) => {
378
+ const edit = () => note.openEditor(comment.id, comment.selection, comment.comment)
379
+ const remove = () => {
380
+ note.reset()
381
+ props.onDelete?.(comment)
382
+ }
383
+
384
+ return {
385
+ id: comment.id,
386
+ get open() {
387
+ return note.isOpen(comment.id) || note.isEditing(comment.id)
388
+ },
389
+ comment: comment.comment,
390
+ selection: formatSelectedLineLabel(comment.selection, i18n.t),
391
+ get actions() {
392
+ return props.renderCommentActions?.(comment, { edit, remove })
393
+ },
394
+ get editor() {
395
+ return note.isEditing(comment.id)
396
+ ? {
397
+ get value() {
398
+ return note.draft()
399
+ },
400
+ selection: formatSelectedLineLabel(comment.selection, i18n.t),
401
+ mention: props.mention,
402
+ onInput: note.setDraft,
403
+ onCancel: note.cancelDraft,
404
+ onSubmit: (value: string) => {
405
+ props.onUpdate?.({
406
+ id: comment.id,
407
+ comment: value,
408
+ selection: cloneSelectedLineRange(comment.selection),
409
+ })
410
+ note.cancelDraft()
411
+ },
412
+ submitLabel: props.editSubmitLabel,
413
+ }
414
+ : undefined
415
+ },
416
+ onMouseEnter: () => note.hoverComment(comment.selection),
417
+ onClick: () => {
418
+ if (note.isEditing(comment.id)) return
419
+ note.toggleComment(comment.id, comment.selection, { cancelDraft: props.cancelDraftOnCommentToggle })
420
+ },
421
+ }
422
+ },
423
+ renderDraft: (range) => ({
424
+ get value() {
425
+ return note.draft()
426
+ },
427
+ selection: formatSelectedLineLabel(range, i18n.t),
428
+ mention: props.mention,
429
+ onInput: note.setDraft,
430
+ onCancel: () => {
431
+ note.cancelDraft()
432
+ note.select(null)
433
+ },
434
+ onSubmit: (comment) => {
435
+ props.onSubmit({ comment, selection: cloneSelectedLineRange(range) })
436
+ note.cancelDraft()
437
+ },
438
+ onPopoverFocusOut: props.onDraftPopoverFocusOut,
439
+ }),
440
+ })
441
+
442
+ const renderGutterUtility = createLineCommentGutterRenderer({
443
+ label: props.label,
444
+ getSelectedRange: () => {
445
+ if (note.opened()) return null
446
+ return props.getHoverSelectedRange?.() ?? note.selected()
447
+ },
448
+ onOpenDraft: note.openDraft,
449
+ })
450
+
451
+ const onLineSelected = (range: SelectedLineRange | null) => {
452
+ if (!range) {
453
+ note.select(null)
454
+ note.cancelDraft()
455
+ return
456
+ }
457
+
458
+ note.select(range)
459
+ }
460
+
461
+ const onLineSelectionEnd = (range: SelectedLineRange | null) => {
462
+ if (!range) {
463
+ if (props.clearSelectionOnSelectionEndNull) note.select(null)
464
+ note.cancelDraft()
465
+ return
466
+ }
467
+
468
+ note.openDraft(range)
469
+ }
470
+
471
+ return {
472
+ note,
473
+ annotations,
474
+ renderAnnotation,
475
+ renderGutterUtility,
476
+ onLineSelected,
477
+ onLineSelectionEnd,
478
+ }
479
+ }
480
+
481
+ export function createLineCommentAnnotations<T>(
482
+ props: LineCommentAnnotationsWithSideProps<T>,
483
+ ): Accessor<DiffLineAnnotation<LineCommentAnnotationMeta<T>>[]>
484
+ export function createLineCommentAnnotations<T>(
485
+ props: LineCommentAnnotationsProps<T>,
486
+ ): Accessor<LineCommentAnnotation<T>[]>
487
+ export function createLineCommentAnnotations<T>(
488
+ props: LineCommentAnnotationsProps<T> | LineCommentAnnotationsWithSideProps<T>,
489
+ ) {
490
+ const line = (range: SelectedLineRange) => Math.max(range.start, range.end)
491
+
492
+ if ("getSide" in props) {
493
+ return createMemo<DiffLineAnnotation<LineCommentAnnotationMeta<T>>[]>(
494
+ () => {
495
+ const list = props.comments().map((comment) => {
496
+ const range = props.getCommentSelection(comment)
497
+ return {
498
+ side: props.getSide(range),
499
+ lineNumber: line(range),
500
+ metadata: {
501
+ kind: "comment",
502
+ key: `comment:${props.getCommentId(comment)}`,
503
+ comment,
504
+ } satisfies LineCommentAnnotationMeta<T>,
505
+ }
506
+ })
507
+
508
+ const range = props.draftRange()
509
+ if (!range) return list
510
+
511
+ return [
512
+ ...list,
513
+ {
514
+ side: props.getSide(range),
515
+ lineNumber: line(range),
516
+ metadata: {
517
+ kind: "draft",
518
+ key: `draft:${props.draftKey()}`,
519
+ range,
520
+ } satisfies LineCommentAnnotationMeta<T>,
521
+ },
522
+ ]
523
+ },
524
+ [],
525
+ // Stable identity for unchanged annotations avoids no-op diff rerenders downstream.
526
+ { equals: sameAnnotationLists },
527
+ )
528
+ }
529
+
530
+ return createMemo<LineCommentAnnotation<T>[]>(
531
+ () => {
532
+ const list = props.comments().map((comment) => {
533
+ const range = props.getCommentSelection(comment)
534
+ const entry: LineCommentAnnotation<T> = {
535
+ lineNumber: line(range),
536
+ metadata: {
537
+ kind: "comment",
538
+ key: `comment:${props.getCommentId(comment)}`,
539
+ comment,
540
+ },
541
+ }
542
+
543
+ return entry
544
+ })
545
+
546
+ const range = props.draftRange()
547
+ if (!range) return list
548
+
549
+ const draft: LineCommentAnnotation<T> = {
550
+ lineNumber: line(range),
551
+ metadata: {
552
+ kind: "draft",
553
+ key: `draft:${props.draftKey()}`,
554
+ range,
555
+ },
556
+ }
557
+
558
+ return [...list, draft]
559
+ },
560
+ [],
561
+ { equals: sameAnnotationLists },
562
+ )
563
+ }
564
+
565
+ type AnnotationListItem = {
566
+ lineNumber: number
567
+ side?: unknown
568
+ metadata: { kind: string; key: string; comment?: unknown; range?: unknown }
569
+ }
570
+
571
+ function sameAnnotationLists(previous: AnnotationListItem[], next: AnnotationListItem[]) {
572
+ if (previous.length !== next.length) return false
573
+ return previous.every((item, index) => {
574
+ const other = next[index]!
575
+ return (
576
+ item.lineNumber === other.lineNumber &&
577
+ item.side === other.side &&
578
+ item.metadata.kind === other.metadata.kind &&
579
+ item.metadata.key === other.metadata.key &&
580
+ item.metadata.comment === other.metadata.comment &&
581
+ item.metadata.range === other.metadata.range
582
+ )
583
+ })
584
+ }
585
+
586
+ export function createManagedLineCommentAnnotationRenderer<T, C, D>(props: {
587
+ annotations: Accessor<LineCommentAnnotation<T>[]>
588
+ renderComment: (comment: T) => C
589
+ renderDraft: (range: SelectedLineRange) => D
590
+ commentElement: (view: Accessor<C>) => JSX.Element
591
+ draftElement: (view: Accessor<D>) => JSX.Element
592
+ }) {
593
+ const renderer = createLineCommentAnnotationRenderer<T, C, D>({
594
+ renderComment: props.renderComment,
595
+ renderDraft: props.renderDraft,
596
+ commentElement: props.commentElement,
597
+ draftElement: props.draftElement,
598
+ })
599
+
600
+ createEffect(() => {
601
+ renderer.reconcile(props.annotations())
602
+ })
603
+
604
+ onCleanup(() => {
605
+ renderer.cleanup()
606
+ })
607
+
608
+ return {
609
+ renderAnnotation: renderer.render,
610
+ }
611
+ }
612
+
613
+ export function createLineCommentGutterRenderer(props: {
614
+ label: string
615
+ getSelectedRange: Accessor<SelectedLineRange | null>
616
+ onOpenDraft: (range: SelectedLineRange) => void
617
+ }) {
618
+ return (getHoveredLine: () => HoverCommentLine | undefined) =>
619
+ createHoverCommentUtility({
620
+ label: props.label,
621
+ getHoveredLine,
622
+ onSelect: (hovered) => {
623
+ const current = props.getSelectedRange()
624
+ if (current && lineInSelectedRange(current, hovered.lineNumber, hovered.side)) {
625
+ props.onOpenDraft(cloneSelectedLineRange(current))
626
+ return
627
+ }
628
+
629
+ const range: SelectedLineRange = {
630
+ start: hovered.lineNumber,
631
+ end: hovered.lineNumber,
632
+ }
633
+ if (hovered.side) range.side = hovered.side
634
+ props.onOpenDraft(range)
635
+ },
636
+ })
637
+ }