@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,66 @@
1
+ // @ts-nocheck
2
+ import { SessionProgressIndicatorV2 } from "./session-progress-indicator-v2"
3
+
4
+ const docs = `### Overview
5
+ Animated 5×5 dot grid loader for in-progress session state.
6
+
7
+ Derived from Figma \`_sessionProgressIndicator\` with 8-frame rotation.
8
+
9
+ ### API
10
+ - Accepts standard SVG props.
11
+
12
+ ### Behavior
13
+ - CSS keyframes drive per-dot opacity across 8 frames (1.2s loop).
14
+ - Center dot stays at full opacity throughout the cycle.
15
+
16
+ ### Accessibility
17
+ - Sets \`aria-hidden="true"\` by default.
18
+
19
+ ### Theming
20
+ - Uses \`currentColor\` via \`--v2-icon-icon-muted\`.
21
+ `
22
+
23
+ export default {
24
+ title: "UI V2/SessionProgressIndicator",
25
+ id: "components-session-progress-indicator-v2",
26
+ component: SessionProgressIndicatorV2,
27
+ tags: ["autodocs"],
28
+ parameters: {
29
+ docs: {
30
+ description: {
31
+ component: docs,
32
+ },
33
+ },
34
+ },
35
+ }
36
+
37
+ export const Basic = {
38
+ render: () => <SessionProgressIndicatorV2 />,
39
+ }
40
+
41
+ export const Sizes = {
42
+ render: () => (
43
+ <div style={{ display: "flex", gap: "16px", "align-items": "center" }}>
44
+ <SessionProgressIndicatorV2 width={12} height={12} />
45
+ <SessionProgressIndicatorV2 />
46
+ <SessionProgressIndicatorV2 width={24} height={24} />
47
+ </div>
48
+ ),
49
+ }
50
+
51
+ export const OnDark = {
52
+ render: () => (
53
+ <div
54
+ style={{
55
+ display: "flex",
56
+ gap: "16px",
57
+ "align-items": "center",
58
+ padding: "16px",
59
+ "background-color": "#171717",
60
+ color: "#c7c7c7",
61
+ }}
62
+ >
63
+ <SessionProgressIndicatorV2 />
64
+ </div>
65
+ ),
66
+ }
@@ -0,0 +1,32 @@
1
+ import { For, splitProps, type ComponentProps } from "solid-js"
2
+ import "./session-progress-indicator-v2.css"
3
+
4
+ const grid = 5
5
+ const dot = 2
6
+ const gap = 1
7
+ const origin = 1.5
8
+ const dots = Array.from({ length: grid * grid }, (_, index) => ({
9
+ index,
10
+ x: origin + (index % grid) * (dot + gap),
11
+ y: origin + Math.floor(index / grid) * (dot + gap),
12
+ }))
13
+
14
+ export function SessionProgressIndicatorV2(props: ComponentProps<"svg">) {
15
+ const [local, rest] = splitProps(props, ["class", "classList", "width", "height"])
16
+ return (
17
+ <svg
18
+ {...rest}
19
+ class={local.class}
20
+ classList={local.classList}
21
+ width={local.width ?? 16}
22
+ height={local.height ?? 16}
23
+ viewBox="0 0 16 16"
24
+ fill="none"
25
+ xmlns="http://www.w3.org/2000/svg"
26
+ data-component="session-progress-indicator-v2"
27
+ aria-hidden={rest["aria-hidden"] ?? "true"}
28
+ >
29
+ <For each={dots}>{(cell) => <rect data-dot={cell.index} x={cell.x} y={cell.y} width={dot} height={dot} />}</For>
30
+ </svg>
31
+ )
32
+ }
@@ -0,0 +1,17 @@
1
+ import { useI18n } from "@neurocode-ai/ui/context/i18n"
2
+ import { Icon } from "@neurocode-ai/ui/v2/icon"
3
+ import "./session-review-v2.css"
4
+
5
+ export function SessionReviewEmptyChangesV2() {
6
+ const i18n = useI18n()
7
+
8
+ return (
9
+ <div data-slot="session-review-v2-empty-changes">
10
+ <Icon name="review" size="large" />
11
+ <div data-slot="session-review-v2-empty-changes-title">{i18n.t("ui.sessionReviewV2.empty.changes.title")}</div>
12
+ <div data-slot="session-review-v2-empty-changes-description">
13
+ {i18n.t("ui.sessionReviewV2.empty.changes.description")}
14
+ </div>
15
+ </div>
16
+ )
17
+ }
@@ -0,0 +1,28 @@
1
+ import { FileIcon } from "@neurocode-ai/ui/file-icon"
2
+ import { useI18n } from "@neurocode-ai/ui/context/i18n"
3
+ import { ButtonV2 } from "@neurocode-ai/ui/v2/button-v2"
4
+ import "./session-review-v2.css"
5
+
6
+ export type SessionReviewEmptyNoGitV2Props = {
7
+ pending: boolean
8
+ onInitGit: () => void
9
+ }
10
+
11
+ export function SessionReviewEmptyNoGitV2(props: SessionReviewEmptyNoGitV2Props) {
12
+ const i18n = useI18n()
13
+
14
+ return (
15
+ <div data-slot="session-review-v2-empty-no-git">
16
+ <FileIcon node={{ path: ".gitignore", type: "file" }} mono />
17
+ <div data-slot="session-review-v2-empty-no-git-title">{i18n.t("ui.sessionReviewV2.empty.noGit.title")}</div>
18
+ <div data-slot="session-review-v2-empty-no-git-description">
19
+ {i18n.t("ui.sessionReviewV2.empty.noGit.description")}
20
+ </div>
21
+ <ButtonV2 variant="neutral" size="normal" disabled={props.pending} onClick={props.onInitGit}>
22
+ {props.pending
23
+ ? i18n.t("ui.sessionReviewV2.empty.noGit.actionLoading")
24
+ : i18n.t("ui.sessionReviewV2.empty.noGit.action")}
25
+ </ButtonV2>
26
+ </div>
27
+ )
28
+ }
@@ -0,0 +1,13 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { shouldVirtualizeReviewDiff } from "./session-review-file-preview-v2-virtualize"
3
+
4
+ describe("shouldVirtualizeReviewDiff", () => {
5
+ test("renders small diffs directly", () => {
6
+ expect(shouldVirtualizeReviewDiff({ additionLines: 500, deletionLines: 500 })).toBe(false)
7
+ })
8
+
9
+ test("virtualizes large diffs", () => {
10
+ expect(shouldVirtualizeReviewDiff({ additionLines: 501, deletionLines: 1 })).toBe(true)
11
+ expect(shouldVirtualizeReviewDiff({ additionLines: 1, deletionLines: 501 })).toBe(true)
12
+ })
13
+ })
@@ -0,0 +1,5 @@
1
+ const lineThreshold = 500
2
+
3
+ export function shouldVirtualizeReviewDiff(input: { additionLines: number; deletionLines: number }) {
4
+ return Math.max(input.additionLines, input.deletionLines) > lineThreshold
5
+ }
@@ -0,0 +1,290 @@
1
+ import { getDirectory, getFilename } from "@neurocode-ai/core/util/path"
2
+ import type { SelectedLineRange } from "@pierre/diffs"
3
+ import { DiffChanges } from "@neurocode-ai/ui/v2/diff-changes-v2"
4
+ import { FileIcon } from "@neurocode-ai/ui/file-icon"
5
+ import { useFileComponent } from "@neurocode-ai/ui/context/file"
6
+ import { useI18n } from "@neurocode-ai/ui/context/i18n"
7
+ import { mediaKindFromPath } from "../../pierre/media"
8
+ import { cloneSelectedLineRange, previewSelectedLines } from "../../pierre/selection-bridge"
9
+ import type { FileContent, SnapshotFileDiff, VcsFileDiff } from "@neurocode-ai/sdk/v2"
10
+ import type { FileDiffInfo } from "@neurocode-ai/client/promise"
11
+ import { createEffect, createMemo, onCleanup, Show, untrack } from "solid-js"
12
+ import { createStore } from "solid-js/store"
13
+ import { Dynamic } from "solid-js/web"
14
+ import { normalize, text, type ViewDiff } from "../../components/session-diff"
15
+ import type {
16
+ SessionReviewComment,
17
+ SessionReviewCommentActions,
18
+ SessionReviewCommentDelete,
19
+ SessionReviewCommentUpdate,
20
+ SessionReviewDiffStyle,
21
+ SessionReviewFocus,
22
+ SessionReviewLineComment,
23
+ } from "../../components/session-review"
24
+ import type { SessionReviewExpandMode } from "./session-review-v2"
25
+ import { createLineCommentControllerV2 } from "./line-comment-annotations-v2"
26
+ import { shouldVirtualizeReviewDiff } from "./session-review-file-preview-v2-virtualize"
27
+ import { LineCommentV2OverflowIcon } from "@neurocode-ai/ui/v2/line-comment-v2"
28
+ import { MenuV2 } from "@neurocode-ai/ui/v2/menu-v2"
29
+ import "./session-review-v2.css"
30
+
31
+ type ReviewDiff = (SnapshotFileDiff & { file: string }) | FileDiffInfo | VcsFileDiff
32
+
33
+ export type SessionReviewFilePreviewV2Props = {
34
+ file: string
35
+ diff: ReviewDiff
36
+ diffStyle: SessionReviewDiffStyle
37
+ expandMode?: SessionReviewExpandMode
38
+ readFile?: (path: string) => Promise<FileContent | undefined>
39
+ onLineComment?: (comment: SessionReviewLineComment) => void
40
+ onLineCommentUpdate?: (comment: SessionReviewCommentUpdate) => void
41
+ onLineCommentDelete?: (comment: SessionReviewCommentDelete) => void
42
+ lineCommentActions?: SessionReviewCommentActions
43
+ comments?: SessionReviewComment[]
44
+ focusedComment?: SessionReviewFocus | null
45
+ onFocusedCommentChange?: (focus: SessionReviewFocus | null) => void
46
+ }
47
+
48
+ function statusLabel(status: ViewDiff["status"]) {
49
+ if (status === "added") return "A"
50
+ if (status === "deleted") return "D"
51
+ return "M"
52
+ }
53
+
54
+ function statusType(status: ViewDiff["status"]) {
55
+ if (status === "added") return "added"
56
+ if (status === "deleted") return "deleted"
57
+ return "modified"
58
+ }
59
+
60
+ function selectionSide(range: SelectedLineRange) {
61
+ return range.endSide ?? range.side ?? "additions"
62
+ }
63
+
64
+ function selectionPreview(diff: ViewDiff, range: SelectedLineRange) {
65
+ const side = selectionSide(range)
66
+ const contents = text(diff, side)
67
+ if (contents.length === 0) return undefined
68
+ return previewSelectedLines(contents, range)
69
+ }
70
+
71
+ function ReviewCommentMenuV2(props: {
72
+ labels: SessionReviewCommentActions
73
+ onEdit: VoidFunction
74
+ onDelete: VoidFunction
75
+ }) {
76
+ return (
77
+ <div onMouseDown={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}>
78
+ <MenuV2 gutter={4}>
79
+ <MenuV2.Trigger
80
+ as="button"
81
+ type="button"
82
+ data-slot="line-comment-v2-overflow"
83
+ aria-label={props.labels.moreLabel}
84
+ >
85
+ <LineCommentV2OverflowIcon />
86
+ </MenuV2.Trigger>
87
+ <MenuV2.Portal>
88
+ <MenuV2.Content>
89
+ <MenuV2.Item onSelect={props.onEdit}>{props.labels.editLabel}</MenuV2.Item>
90
+ <MenuV2.Item onSelect={props.onDelete}>{props.labels.deleteLabel}</MenuV2.Item>
91
+ </MenuV2.Content>
92
+ </MenuV2.Portal>
93
+ </MenuV2>
94
+ </div>
95
+ )
96
+ }
97
+
98
+ export function SessionReviewFilePreviewV2(props: SessionReviewFilePreviewV2Props) {
99
+ const i18n = useI18n()
100
+ const fileComponent = useFileComponent()
101
+ let scrollRef: HTMLDivElement | undefined
102
+ let focusToken = 0
103
+
104
+ const [store, setStore] = createStore({
105
+ selection: null as SelectedLineRange | null,
106
+ commenting: null as SelectedLineRange | null,
107
+ opened: null as string | null,
108
+ })
109
+
110
+ const view = createMemo(() => ({
111
+ ...normalize(props.diff),
112
+ preloaded: "preloaded" in props.diff ? props.diff.preloaded : undefined,
113
+ }))
114
+ const diffCanRender = createMemo(() => view().additions !== 0 || view().deletions !== 0)
115
+ const mediaKind = createMemo(() => mediaKindFromPath(props.file))
116
+ const comments = createMemo(() => (props.comments ?? []).filter((comment) => comment.file === props.file))
117
+ const commentedLines = createMemo(() => comments().map((comment) => comment.selection))
118
+ const lineCommentsEnabled = () => props.onLineComment != null
119
+
120
+ const commentsUi = createLineCommentControllerV2<SessionReviewComment>({
121
+ comments,
122
+ label: i18n.t("ui.lineComment.submit"),
123
+ draftKey: () => props.file,
124
+ state: {
125
+ opened: () => store.opened,
126
+ setOpened: (id) => setStore("opened", id),
127
+ selected: () => store.selection,
128
+ setSelected: (range) => setStore("selection", range),
129
+ commenting: () => store.commenting,
130
+ setCommenting: (range) => setStore("commenting", range),
131
+ },
132
+ getSide: selectionSide,
133
+ onSubmit: ({ comment, selection }) => {
134
+ props.onLineComment?.({
135
+ file: props.file,
136
+ selection,
137
+ comment,
138
+ preview: selectionPreview(view(), selection),
139
+ })
140
+ },
141
+ onUpdate: ({ id, comment, selection }) => {
142
+ props.onLineCommentUpdate?.({
143
+ id,
144
+ file: props.file,
145
+ selection,
146
+ comment,
147
+ preview: selectionPreview(view(), selection),
148
+ })
149
+ },
150
+ onDelete: (comment) => {
151
+ props.onLineCommentDelete?.({
152
+ id: comment.id,
153
+ file: props.file,
154
+ })
155
+ },
156
+ editSubmitLabel: props.lineCommentActions?.saveLabel,
157
+ renderCommentActions: props.lineCommentActions
158
+ ? (comment, controls) => (
159
+ <ReviewCommentMenuV2 labels={props.lineCommentActions!} onEdit={controls.edit} onDelete={controls.remove} />
160
+ )
161
+ : undefined,
162
+ })
163
+
164
+ onCleanup(() => {
165
+ focusToken++
166
+ })
167
+
168
+ createEffect(() => {
169
+ const focus = props.focusedComment
170
+ if (!focus) return
171
+ if (focus.file !== props.file) {
172
+ // The focused file has no mounted preview (e.g. not in the current diff
173
+ // set); clear the focus anyway so it cannot hijack a later diff refresh.
174
+ // V1 clears unconditionally the same way.
175
+ untrack(() => {
176
+ const token = focusToken
177
+ requestAnimationFrame(() => {
178
+ if (token !== focusToken) return
179
+ props.onFocusedCommentChange?.(null)
180
+ })
181
+ })
182
+ return
183
+ }
184
+
185
+ untrack(() => {
186
+ setStore("opened", focus.id)
187
+
188
+ const comment = (props.comments ?? []).find((item) => item.file === focus.file && item.id === focus.id)
189
+ if (comment) setStore("selection", cloneSelectedLineRange(comment.selection))
190
+
191
+ // The diff renders asynchronously, so poll for the comment anchor before
192
+ // scrolling; clear the focus once handled so revisiting the file does not
193
+ // re-open a stale comment (mirrors the v1 review behavior).
194
+ focusToken++
195
+ const token = focusToken
196
+ const scrollTo = (attempt: number) => {
197
+ if (token !== focusToken) return
198
+ const anchor = scrollRef?.querySelector(`[data-comment-id="${focus.id}"]`)
199
+ if (anchor instanceof HTMLElement) {
200
+ anchor.scrollIntoView({ block: "center" })
201
+ return
202
+ }
203
+ if (attempt >= 120) return
204
+ requestAnimationFrame(() => scrollTo(attempt + 1))
205
+ }
206
+ requestAnimationFrame(() => scrollTo(0))
207
+ requestAnimationFrame(() => {
208
+ if (token !== focusToken) return
209
+ props.onFocusedCommentChange?.(null)
210
+ })
211
+ })
212
+ })
213
+
214
+ const expandUnchanged = () => props.expandMode === "expand"
215
+
216
+ const diffViewer = () => (
217
+ <Dynamic
218
+ component={fileComponent}
219
+ mode="diff"
220
+ fileDiff={view().fileDiff}
221
+ preloadedDiff={view().preloaded}
222
+ diffStyle={props.diffStyle}
223
+ expandUnchanged={expandUnchanged()}
224
+ virtualize={shouldVirtualizeReviewDiff({
225
+ additionLines: view().fileDiff.additionLines.length,
226
+ deletionLines: view().fileDiff.deletionLines.length,
227
+ })}
228
+ hunkSeparators={view().fileDiff.isPartial ? "simple" : "line-info-basic"}
229
+ enableLineSelection={lineCommentsEnabled()}
230
+ enableGutterUtility={lineCommentsEnabled()}
231
+ onLineSelected={(range: SelectedLineRange | null) => {
232
+ if (!lineCommentsEnabled()) return
233
+ commentsUi.onLineSelected(range)
234
+ }}
235
+ onLineSelectionEnd={(range: SelectedLineRange | null) => {
236
+ if (!lineCommentsEnabled()) return
237
+ commentsUi.onLineSelectionEnd(range)
238
+ }}
239
+ onLineNumberSelectionEnd={commentsUi.onLineNumberSelectionEnd}
240
+ annotations={commentsUi.annotations()}
241
+ renderAnnotation={commentsUi.renderAnnotation}
242
+ renderGutterUtility={lineCommentsEnabled() ? commentsUi.renderGutterUtility : undefined}
243
+ selectedLines={store.selection}
244
+ commentedLines={commentedLines()}
245
+ media={{
246
+ mode: "auto",
247
+ path: props.file,
248
+ deleted: view().status === "deleted",
249
+ readFile: view().status === "deleted" ? undefined : props.readFile,
250
+ }}
251
+ />
252
+ )
253
+
254
+ return (
255
+ <>
256
+ <div data-slot="session-review-v2-file-header">
257
+ <div data-slot="session-review-v2-file-title">
258
+ <div data-slot="session-review-v2-file-status" data-type={statusType(view().status)}>
259
+ {statusLabel(view().status)}
260
+ </div>
261
+ <FileIcon node={{ path: props.file, type: "file" }} />
262
+ <span data-slot="session-review-v2-file-name">{getFilename(props.file)}</span>
263
+ <Show when={props.file.includes("/")}>
264
+ <span data-slot="session-review-v2-file-path">{getDirectory(props.file)}</span>
265
+ </Show>
266
+ </div>
267
+ <div data-slot="session-review-v2-file-diff">
268
+ <DiffChanges changes={view()} />
269
+ </div>
270
+ </div>
271
+ <div
272
+ ref={(el) => {
273
+ scrollRef = el
274
+ }}
275
+ data-slot="session-review-v2-diff-scroll"
276
+ >
277
+ <Show
278
+ when={diffCanRender() || mediaKind()}
279
+ fallback={
280
+ <div data-slot="session-review-v2-empty">
281
+ <span class="text-12-regular text-text-weak">{i18n.t("ui.fileMedia.binary.title")}</span>
282
+ </div>
283
+ }
284
+ >
285
+ {diffViewer()}
286
+ </Show>
287
+ </div>
288
+ </>
289
+ )
290
+ }