@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.
- package/package.json +71 -0
- package/src/components/apply-patch-file.test.ts +43 -0
- package/src/components/apply-patch-file.ts +78 -0
- package/src/components/basic-tool.css +358 -0
- package/src/components/basic-tool.stories.tsx +133 -0
- package/src/components/basic-tool.tsx +343 -0
- package/src/components/dock-prompt.stories.tsx +62 -0
- package/src/components/dock-prompt.tsx +23 -0
- package/src/components/file-media.tsx +291 -0
- package/src/components/file-search.tsx +72 -0
- package/src/components/file-ssr.tsx +197 -0
- package/src/components/file.css +46 -0
- package/src/components/file.tsx +1202 -0
- package/src/components/line-comment-annotations.tsx +637 -0
- package/src/components/line-comment-styles.ts +292 -0
- package/src/components/line-comment.stories.tsx +115 -0
- package/src/components/line-comment.tsx +439 -0
- package/src/components/markdown-cache.tsx +78 -0
- package/src/components/markdown-code-state.test.ts +32 -0
- package/src/components/markdown-code-state.ts +22 -0
- package/src/components/markdown-inline-code-kind.test.ts +46 -0
- package/src/components/markdown-inline-code-kind.ts +1915 -0
- package/src/components/markdown-preload.test.ts +18 -0
- package/src/components/markdown-shiki.worker.ts +104 -0
- package/src/components/markdown-stream.test.ts +194 -0
- package/src/components/markdown-stream.ts +110 -0
- package/src/components/markdown-worker-protocol.test.ts +81 -0
- package/src/components/markdown-worker-protocol.ts +48 -0
- package/src/components/markdown-worker-queue.test.ts +49 -0
- package/src/components/markdown-worker-queue.ts +64 -0
- package/src/components/markdown-worker-transport.test.ts +56 -0
- package/src/components/markdown-worker-transport.ts +41 -0
- package/src/components/markdown-worker.ts +122 -0
- package/src/components/markdown.css +377 -0
- package/src/components/markdown.stories.tsx +53 -0
- package/src/components/markdown.tsx +678 -0
- package/src/components/message-file.test.ts +63 -0
- package/src/components/message-file.ts +34 -0
- package/src/components/message-nav.css +127 -0
- package/src/components/message-nav.stories.tsx +7 -0
- package/src/components/message-nav.tsx +102 -0
- package/src/components/message-part-text.ts +3 -0
- package/src/components/message-part.css +1569 -0
- package/src/components/message-part.stories.tsx +7 -0
- package/src/components/message-part.test.ts +28 -0
- package/src/components/message-part.tsx +2662 -0
- package/src/components/session-diff.test.ts +135 -0
- package/src/components/session-diff.ts +145 -0
- package/src/components/session-retry.tsx +74 -0
- package/src/components/session-review.css +247 -0
- package/src/components/session-review.stories.tsx +7 -0
- package/src/components/session-review.tsx +657 -0
- package/src/components/session-turn.css +241 -0
- package/src/components/session-turn.stories.tsx +7 -0
- package/src/components/session-turn.tsx +543 -0
- package/src/components/shell-submessage-motion.stories.tsx +346 -0
- package/src/components/shell-submessage.css +23 -0
- package/src/components/timeline-playground.stories.tsx +2090 -0
- package/src/components/tool-count-label.css +57 -0
- package/src/components/tool-count-label.tsx +58 -0
- package/src/components/tool-count-summary.css +102 -0
- package/src/components/tool-count-summary.stories.tsx +238 -0
- package/src/components/tool-count-summary.tsx +52 -0
- package/src/components/tool-error-card.css +91 -0
- package/src/components/tool-error-card.stories.tsx +92 -0
- package/src/components/tool-error-card.tsx +157 -0
- package/src/components/tool-status-title.css +89 -0
- package/src/components/tool-status-title.tsx +133 -0
- package/src/context/data.tsx +64 -0
- package/src/context/index.ts +1 -0
- package/src/pierre/comment-hover.ts +83 -0
- package/src/pierre/commented-lines.ts +91 -0
- package/src/pierre/diff-selection.ts +71 -0
- package/src/pierre/file-find.ts +485 -0
- package/src/pierre/file-runtime.ts +114 -0
- package/src/pierre/file-selection.ts +85 -0
- package/src/pierre/index.ts +186 -0
- package/src/pierre/media.ts +110 -0
- package/src/pierre/selection-bridge.ts +132 -0
- package/src/pierre/virtualizer.ts +100 -0
- package/src/pierre/worker.ts +52 -0
- package/src/styles/index.css +14 -0
- package/src/v2/components/attachment-card-v2.css +70 -0
- package/src/v2/components/attachment-card-v2.tsx +33 -0
- package/src/v2/components/basic-tool-v2.css +163 -0
- package/src/v2/components/basic-tool-v2.stories.tsx +137 -0
- package/src/v2/components/basic-tool-v2.tsx +139 -0
- package/src/v2/components/comment-card-v2.tsx +64 -0
- package/src/v2/components/line-comment-annotations-v2.tsx +220 -0
- package/src/v2/components/prompt-input/attachments.ts +266 -0
- package/src/v2/components/prompt-input/index.tsx +706 -0
- package/src/v2/components/prompt-input/interaction.ts +482 -0
- package/src/v2/components/prompt-input/machine.test.ts +164 -0
- package/src/v2/components/prompt-input/machine.ts +261 -0
- package/src/v2/components/prompt-input/prompt-input.stories.tsx +221 -0
- package/src/v2/components/prompt-input/store.test.ts +116 -0
- package/src/v2/components/prompt-input/store.ts +152 -0
- package/src/v2/components/prompt-input/types.ts +106 -0
- package/src/v2/components/session-file-panel-v2.tsx +43 -0
- package/src/v2/components/session-progress-indicator-v2.css +875 -0
- package/src/v2/components/session-progress-indicator-v2.stories.tsx +66 -0
- package/src/v2/components/session-progress-indicator-v2.tsx +32 -0
- package/src/v2/components/session-review-empty-changes-v2.tsx +17 -0
- package/src/v2/components/session-review-empty-no-git-v2.tsx +28 -0
- package/src/v2/components/session-review-file-preview-v2-virtualize.test.ts +13 -0
- package/src/v2/components/session-review-file-preview-v2-virtualize.ts +5 -0
- package/src/v2/components/session-review-file-preview-v2.tsx +290 -0
- package/src/v2/components/session-review-v2.css +458 -0
- package/src/v2/components/session-review-v2.tsx +340 -0
- package/src/v2/components/tool-error-card-v2.css +200 -0
- package/src/v2/components/tool-error-card-v2.stories.tsx +91 -0
- package/src/v2/components/tool-error-card-v2.tsx +166 -0
- package/sst-env.d.ts +10 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { DiffLineAnnotation, FileContents, FileDiffOptions, type SelectedLineRange } from "@pierre/diffs"
|
|
2
|
+
import { ComponentProps } from "solid-js"
|
|
3
|
+
import { lineCommentStyles } from "../components/line-comment-styles"
|
|
4
|
+
|
|
5
|
+
export type DiffProps<T = {}> = FileDiffOptions<T> & {
|
|
6
|
+
before: FileContents
|
|
7
|
+
after: FileContents
|
|
8
|
+
annotations?: DiffLineAnnotation<T>[]
|
|
9
|
+
selectedLines?: SelectedLineRange | null
|
|
10
|
+
commentedLines?: SelectedLineRange[]
|
|
11
|
+
onLineNumberSelectionEnd?: (selection: SelectedLineRange | null) => void
|
|
12
|
+
onRendered?: () => void
|
|
13
|
+
class?: string
|
|
14
|
+
classList?: ComponentProps<"div">["classList"]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const unsafeCSS = `
|
|
18
|
+
:host {
|
|
19
|
+
--diffs-bg: var(--opencode-diffs-bg, var(--color-background-stronger));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
[data-diff],
|
|
23
|
+
[data-file] {
|
|
24
|
+
/* Pierre 1.2 mixes these override targets at 12% in light mode and 20% in dark mode. */
|
|
25
|
+
--diffs-bg-deletion-override: light-dark(
|
|
26
|
+
color-mix(in lab, var(--diffs-bg) 33.333%, var(--diffs-deletion-base)),
|
|
27
|
+
color-mix(in lab, var(--diffs-bg) 60%, var(--diffs-deletion-base))
|
|
28
|
+
);
|
|
29
|
+
--diffs-bg-addition-override: light-dark(
|
|
30
|
+
color-mix(in lab, var(--diffs-bg) 33.333%, var(--diffs-addition-base)),
|
|
31
|
+
color-mix(in lab, var(--diffs-bg) 60%, var(--diffs-addition-base))
|
|
32
|
+
);
|
|
33
|
+
--diffs-selection-base: var(--surface-warning-strong);
|
|
34
|
+
--diffs-selection-border: var(--border-warning-base);
|
|
35
|
+
--diffs-selection-number-fg: #1c1917;
|
|
36
|
+
/* Use explicit alpha instead of color-mix(..., transparent) to avoid Safari's non-premultiplied interpolation bugs. */
|
|
37
|
+
--diffs-bg-selection: var(--diffs-bg-selection-override, rgb(from var(--surface-warning-base) r g b / 0.65));
|
|
38
|
+
--diffs-bg-selection-number: var(
|
|
39
|
+
--diffs-bg-selection-number-override,
|
|
40
|
+
rgb(from var(--surface-warning-base) r g b / 0.85)
|
|
41
|
+
);
|
|
42
|
+
--diffs-bg-selection-text: rgb(from var(--surface-warning-strong) r g b / 0.2);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
:host([data-color-scheme='dark']) [data-diff],
|
|
46
|
+
:host([data-color-scheme='dark']) [data-file] {
|
|
47
|
+
--diffs-selection-number-fg: #fdfbfb;
|
|
48
|
+
--diffs-bg-selection: var(--diffs-bg-selection-override, rgb(from var(--solaris-dark-6) r g b / 0.65));
|
|
49
|
+
--diffs-bg-selection-number: var(
|
|
50
|
+
--diffs-bg-selection-number-override,
|
|
51
|
+
rgb(from var(--solaris-dark-6) r g b / 0.85)
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
[data-diff] ::selection,
|
|
56
|
+
[data-file] ::selection {
|
|
57
|
+
background-color: var(--diffs-bg-selection-text);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
::highlight(opencode-find) {
|
|
61
|
+
background-color: rgb(from var(--surface-warning-base) r g b / 0.35);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
::highlight(opencode-find-current) {
|
|
65
|
+
background-color: rgb(from var(--surface-warning-strong) r g b / 0.55);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
[data-diff] [data-line][data-comment-selected]:not([data-selected-line]) {
|
|
69
|
+
box-shadow: inset 0 0 0 9999px var(--diffs-bg-selection);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
[data-file] [data-line][data-comment-selected]:not([data-selected-line]) {
|
|
73
|
+
box-shadow: inset 0 0 0 9999px var(--diffs-bg-selection);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
[data-diff] [data-column-number][data-comment-selected]:not([data-selected-line]) {
|
|
77
|
+
box-shadow: inset 0 0 0 9999px var(--diffs-bg-selection-number);
|
|
78
|
+
color: var(--diffs-selection-number-fg);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
[data-file] [data-column-number][data-comment-selected]:not([data-selected-line]) {
|
|
82
|
+
box-shadow: inset 0 0 0 9999px var(--diffs-bg-selection-number);
|
|
83
|
+
color: var(--diffs-selection-number-fg);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
[data-diff] [data-line-annotation][data-comment-selected]:not([data-selected-line]) [data-annotation-content] {
|
|
87
|
+
box-shadow: inset 0 0 0 9999px var(--diffs-bg-selection);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
[data-file] [data-line-annotation][data-comment-selected]:not([data-selected-line]) [data-annotation-content] {
|
|
91
|
+
box-shadow: inset 0 0 0 9999px var(--diffs-bg-selection);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
[data-diff] [data-line][data-selected-line] {
|
|
95
|
+
background-color: var(--diffs-bg-selection);
|
|
96
|
+
box-shadow: inset 2px 0 0 var(--diffs-selection-border);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
[data-file] [data-line][data-selected-line] {
|
|
100
|
+
background-color: var(--diffs-bg-selection);
|
|
101
|
+
box-shadow: inset 2px 0 0 var(--diffs-selection-border);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
[data-diff] [data-column-number][data-selected-line] {
|
|
105
|
+
background-color: var(--diffs-bg-selection-number);
|
|
106
|
+
color: var(--diffs-selection-number-fg);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
[data-file] [data-column-number][data-selected-line] {
|
|
110
|
+
background-color: var(--diffs-bg-selection-number);
|
|
111
|
+
color: var(--diffs-selection-number-fg);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
[data-diff] [data-column-number][data-line-type='context'][data-selected-line],
|
|
115
|
+
[data-diff] [data-column-number][data-line-type='context-expanded'][data-selected-line],
|
|
116
|
+
[data-diff] [data-column-number][data-line-type='change-addition'][data-selected-line],
|
|
117
|
+
[data-diff] [data-column-number][data-line-type='change-deletion'][data-selected-line] {
|
|
118
|
+
color: var(--diffs-selection-number-fg);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* The deletion word-diff emphasis is stronger than additions; soften it while selected so the selection highlight reads consistently. */
|
|
122
|
+
[data-diff] [data-line][data-line-type='change-deletion'][data-selected-line] {
|
|
123
|
+
--diffs-bg-deletion-emphasis: light-dark(
|
|
124
|
+
rgb(from var(--diffs-deletion-base) r g b / 0.07),
|
|
125
|
+
rgb(from var(--diffs-deletion-base) r g b / 0.1)
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
[data-diff-header],
|
|
130
|
+
[data-diff],
|
|
131
|
+
[data-file] {
|
|
132
|
+
[data-separator] {
|
|
133
|
+
height: 24px;
|
|
134
|
+
}
|
|
135
|
+
[data-column-number] {
|
|
136
|
+
background-color: var(--diffs-bg);
|
|
137
|
+
cursor: default !important;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
&[data-interactive-line-numbers] [data-column-number] {
|
|
141
|
+
cursor: default !important;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
&[data-interactive-lines] [data-line] {
|
|
145
|
+
cursor: auto !important;
|
|
146
|
+
}
|
|
147
|
+
[data-code] {
|
|
148
|
+
overflow-x: auto !important;
|
|
149
|
+
overflow-y: clip !important;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
${lineCommentStyles}
|
|
154
|
+
|
|
155
|
+
`
|
|
156
|
+
|
|
157
|
+
export function createDefaultOptions<T>(style: FileDiffOptions<T>["diffStyle"]) {
|
|
158
|
+
return {
|
|
159
|
+
theme: "OpenCode",
|
|
160
|
+
themeType: "system",
|
|
161
|
+
disableLineNumbers: false,
|
|
162
|
+
overflow: "wrap",
|
|
163
|
+
diffStyle: style ?? "unified",
|
|
164
|
+
diffIndicators: "bars",
|
|
165
|
+
lineHoverHighlight: "both",
|
|
166
|
+
disableBackground: false,
|
|
167
|
+
expansionLineCount: 20,
|
|
168
|
+
hunkSeparators: "line-info-basic",
|
|
169
|
+
lineDiffType: style === "split" ? "word-alt" : "none",
|
|
170
|
+
maxLineDiffLength: 1000,
|
|
171
|
+
maxLineLengthForHighlighting: 1000,
|
|
172
|
+
disableFileHeader: true,
|
|
173
|
+
unsafeCSS,
|
|
174
|
+
} as const
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export const styleVariables = {
|
|
178
|
+
"--diffs-font-family": "var(--font-family-mono)",
|
|
179
|
+
"--diffs-font-size": "var(--font-size-small)",
|
|
180
|
+
"--diffs-line-height": "24px",
|
|
181
|
+
"--diffs-tab-size": 2,
|
|
182
|
+
"--diffs-font-features": "var(--font-family-mono--font-feature-settings)",
|
|
183
|
+
"--diffs-header-font-family": "var(--font-family-sans)",
|
|
184
|
+
"--diffs-gap-block": 0,
|
|
185
|
+
"--diffs-min-number-column-width": "4ch",
|
|
186
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { FileContent } from "@neurocode-ai/sdk/v2"
|
|
2
|
+
|
|
3
|
+
export type MediaKind = "image" | "audio" | "svg"
|
|
4
|
+
|
|
5
|
+
const imageExtensions = new Set(["png", "jpg", "jpeg", "gif", "webp", "avif", "bmp", "ico", "tif", "tiff", "heic"])
|
|
6
|
+
const audioExtensions = new Set(["mp3", "wav", "ogg", "m4a", "aac", "flac", "opus"])
|
|
7
|
+
|
|
8
|
+
type MediaValue = unknown
|
|
9
|
+
|
|
10
|
+
function mediaRecord(value: unknown) {
|
|
11
|
+
if (!value || typeof value !== "object") return
|
|
12
|
+
return value as Partial<FileContent> & {
|
|
13
|
+
content?: unknown
|
|
14
|
+
encoding?: unknown
|
|
15
|
+
mimeType?: unknown
|
|
16
|
+
type?: unknown
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function normalizeMimeType(type: string | undefined) {
|
|
21
|
+
if (!type) return
|
|
22
|
+
const mime = type.split(";", 1)[0]?.trim().toLowerCase()
|
|
23
|
+
if (!mime) return
|
|
24
|
+
if (mime === "audio/x-aac") return "audio/aac"
|
|
25
|
+
if (mime === "audio/x-m4a") return "audio/mp4"
|
|
26
|
+
return mime
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function fileExtension(path: string | undefined) {
|
|
30
|
+
if (!path) return ""
|
|
31
|
+
const idx = path.lastIndexOf(".")
|
|
32
|
+
if (idx === -1) return ""
|
|
33
|
+
return path.slice(idx + 1).toLowerCase()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function mediaKindFromPath(path: string | undefined): MediaKind | undefined {
|
|
37
|
+
const ext = fileExtension(path)
|
|
38
|
+
if (ext === "svg") return "svg"
|
|
39
|
+
if (imageExtensions.has(ext)) return "image"
|
|
40
|
+
if (audioExtensions.has(ext)) return "audio"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function isBinaryContent(value: MediaValue) {
|
|
44
|
+
return mediaRecord(value)?.type === "binary"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function validDataUrl(value: string, kind: MediaKind) {
|
|
48
|
+
if (kind === "svg") return value.startsWith("data:image/svg+xml") ? value : undefined
|
|
49
|
+
if (kind === "image") return value.startsWith("data:image/") ? value : undefined
|
|
50
|
+
if (value.startsWith("data:audio/x-aac;")) return value.replace("data:audio/x-aac;", "data:audio/aac;")
|
|
51
|
+
if (value.startsWith("data:audio/x-m4a;")) return value.replace("data:audio/x-m4a;", "data:audio/mp4;")
|
|
52
|
+
if (value.startsWith("data:audio/")) return value
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function dataUrlFromMediaValue(value: MediaValue, kind: MediaKind) {
|
|
56
|
+
if (!value) return
|
|
57
|
+
|
|
58
|
+
if (typeof value === "string") {
|
|
59
|
+
return validDataUrl(value, kind)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const record = mediaRecord(value)
|
|
63
|
+
if (!record) return
|
|
64
|
+
|
|
65
|
+
if (typeof record.content !== "string") return
|
|
66
|
+
|
|
67
|
+
const mime = normalizeMimeType(typeof record.mimeType === "string" ? record.mimeType : undefined)
|
|
68
|
+
if (!mime) return
|
|
69
|
+
|
|
70
|
+
if (kind === "svg") {
|
|
71
|
+
if (mime !== "image/svg+xml") return
|
|
72
|
+
if (record.encoding === "base64") return `data:image/svg+xml;base64,${record.content}`
|
|
73
|
+
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(record.content)}`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (kind === "image" && !mime.startsWith("image/")) return
|
|
77
|
+
if (kind === "audio" && !mime.startsWith("audio/")) return
|
|
78
|
+
if (record.encoding !== "base64") return
|
|
79
|
+
|
|
80
|
+
return `data:${mime};base64,${record.content}`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function decodeBase64Utf8(value: string) {
|
|
84
|
+
if (typeof atob !== "function") return
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const raw = atob(value)
|
|
88
|
+
const bytes = Uint8Array.from(raw, (x) => x.charCodeAt(0))
|
|
89
|
+
if (typeof TextDecoder === "function") return new TextDecoder().decode(bytes)
|
|
90
|
+
return raw
|
|
91
|
+
} catch {}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function svgTextFromValue(value: MediaValue) {
|
|
95
|
+
const record = mediaRecord(value)
|
|
96
|
+
if (!record) return
|
|
97
|
+
if (typeof record.content !== "string") return
|
|
98
|
+
|
|
99
|
+
const mime = normalizeMimeType(typeof record.mimeType === "string" ? record.mimeType : undefined)
|
|
100
|
+
if (mime !== "image/svg+xml") return
|
|
101
|
+
if (record.encoding === "base64") return decodeBase64Utf8(record.content)
|
|
102
|
+
return record.content
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function hasMediaValue(value: MediaValue) {
|
|
106
|
+
if (typeof value === "string") return value.length > 0
|
|
107
|
+
const record = mediaRecord(value)
|
|
108
|
+
if (!record) return false
|
|
109
|
+
return typeof record.content === "string" && record.content.length > 0
|
|
110
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { type SelectedLineRange } from "@pierre/diffs"
|
|
2
|
+
|
|
3
|
+
type SelectionKey = "ui.sessionReview.selection.line" | "ui.sessionReview.selection.lines"
|
|
4
|
+
type SelectionVars = Record<string, string | number>
|
|
5
|
+
|
|
6
|
+
type PointerMode = "none" | "text" | "numbers"
|
|
7
|
+
type Side = SelectedLineRange["side"]
|
|
8
|
+
type LineSpan = Pick<SelectedLineRange, "start" | "end">
|
|
9
|
+
|
|
10
|
+
export function formatSelectedLineLabel(range: LineSpan, t: (key: SelectionKey, params: SelectionVars) => string) {
|
|
11
|
+
const start = Math.min(range.start, range.end)
|
|
12
|
+
const end = Math.max(range.start, range.end)
|
|
13
|
+
if (start === end) return t("ui.sessionReview.selection.line", { line: start })
|
|
14
|
+
return t("ui.sessionReview.selection.lines", { start, end })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function previewSelectedLines(source: string, range: LineSpan) {
|
|
18
|
+
const start = Math.max(1, Math.min(range.start, range.end))
|
|
19
|
+
const end = Math.max(range.start, range.end)
|
|
20
|
+
const lines = source.split("\n").slice(start - 1, end)
|
|
21
|
+
if (lines.length === 0) return
|
|
22
|
+
return lines.slice(0, 2).join("\n")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function cloneSelectedLineRange(range: SelectedLineRange): SelectedLineRange {
|
|
26
|
+
const next: SelectedLineRange = {
|
|
27
|
+
start: range.start,
|
|
28
|
+
end: range.end,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (range.side) next.side = range.side
|
|
32
|
+
if (range.endSide) next.endSide = range.endSide
|
|
33
|
+
return next
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function lineInSelectedRange(range: SelectedLineRange | null | undefined, line: number, side?: Side) {
|
|
37
|
+
if (!range) return false
|
|
38
|
+
|
|
39
|
+
const start = Math.min(range.start, range.end)
|
|
40
|
+
const end = Math.max(range.start, range.end)
|
|
41
|
+
if (line < start || line > end) return false
|
|
42
|
+
if (!side) return true
|
|
43
|
+
|
|
44
|
+
const first = range.side
|
|
45
|
+
const last = range.endSide ?? first
|
|
46
|
+
if (!first && !last) return true
|
|
47
|
+
if (!first || !last) return (first ?? last) === side
|
|
48
|
+
if (first === last) return first === side
|
|
49
|
+
if (line === start) return first === side
|
|
50
|
+
if (line === end) return last === side
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isSingleLineSelection(range: SelectedLineRange | null) {
|
|
55
|
+
if (!range) return false
|
|
56
|
+
return range.start === range.end && (range.endSide == null || range.endSide === range.side)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function toRange(source: Range | StaticRange): Range {
|
|
60
|
+
if (source instanceof Range) return source
|
|
61
|
+
const range = new Range()
|
|
62
|
+
range.setStart(source.startContainer, source.startOffset)
|
|
63
|
+
range.setEnd(source.endContainer, source.endOffset)
|
|
64
|
+
return range
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function restoreShadowTextSelection(root: ShadowRoot | undefined, range: Range | undefined) {
|
|
68
|
+
if (!root || !range) return
|
|
69
|
+
|
|
70
|
+
requestAnimationFrame(() => {
|
|
71
|
+
const selection =
|
|
72
|
+
(root as unknown as { getSelection?: () => Selection | null }).getSelection?.() ?? window.getSelection()
|
|
73
|
+
if (!selection) return
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
selection.removeAllRanges()
|
|
77
|
+
selection.addRange(range)
|
|
78
|
+
} catch {}
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function createLineNumberSelectionBridge() {
|
|
83
|
+
let mode: PointerMode = "none"
|
|
84
|
+
let line: number | undefined
|
|
85
|
+
let moved = false
|
|
86
|
+
let pending = false
|
|
87
|
+
|
|
88
|
+
const clear = () => {
|
|
89
|
+
mode = "none"
|
|
90
|
+
line = undefined
|
|
91
|
+
moved = false
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
begin(numberColumn: boolean, next: number | undefined) {
|
|
96
|
+
if (!numberColumn) {
|
|
97
|
+
mode = "text"
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
mode = "numbers"
|
|
102
|
+
line = next
|
|
103
|
+
moved = false
|
|
104
|
+
},
|
|
105
|
+
track(buttons: number, next: number | undefined) {
|
|
106
|
+
if (mode !== "numbers") return false
|
|
107
|
+
|
|
108
|
+
if ((buttons & 1) === 0) {
|
|
109
|
+
clear()
|
|
110
|
+
return true
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (next !== undefined && line !== undefined && next !== line) moved = true
|
|
114
|
+
return true
|
|
115
|
+
},
|
|
116
|
+
finish() {
|
|
117
|
+
const current = mode
|
|
118
|
+
pending = current === "numbers" && moved
|
|
119
|
+
clear()
|
|
120
|
+
return current
|
|
121
|
+
},
|
|
122
|
+
consume(range: SelectedLineRange | null) {
|
|
123
|
+
const result = pending && !isSingleLineSelection(range)
|
|
124
|
+
pending = false
|
|
125
|
+
return result
|
|
126
|
+
},
|
|
127
|
+
reset() {
|
|
128
|
+
pending = false
|
|
129
|
+
clear()
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { type VirtualFileMetrics, Virtualizer } from "@pierre/diffs"
|
|
2
|
+
|
|
3
|
+
type Target = {
|
|
4
|
+
key: Document | HTMLElement
|
|
5
|
+
root: Document | HTMLElement
|
|
6
|
+
content: HTMLElement | undefined
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type Entry = {
|
|
10
|
+
virtualizer: Virtualizer
|
|
11
|
+
refs: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const cache = new WeakMap<Document | HTMLElement, Entry>()
|
|
15
|
+
|
|
16
|
+
export const virtualMetrics: Partial<VirtualFileMetrics> = {
|
|
17
|
+
lineHeight: 24,
|
|
18
|
+
hunkSeparatorHeight: 24,
|
|
19
|
+
spacing: 0,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function scrollable(value: string) {
|
|
23
|
+
return value === "auto" || value === "scroll" || value === "overlay"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function scrollRoot(container: HTMLElement) {
|
|
27
|
+
let node = container.parentElement
|
|
28
|
+
while (node) {
|
|
29
|
+
const style = getComputedStyle(node)
|
|
30
|
+
if (scrollable(style.overflowY)) return node
|
|
31
|
+
node = node.parentElement
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function target(container: HTMLElement): Target | undefined {
|
|
36
|
+
if (typeof document === "undefined") return
|
|
37
|
+
|
|
38
|
+
const review = container.closest("[data-component='session-review']")
|
|
39
|
+
if (review instanceof HTMLElement) {
|
|
40
|
+
const root = scrollRoot(container) ?? review
|
|
41
|
+
const content = review.querySelector("[data-slot='session-review-container']")
|
|
42
|
+
return {
|
|
43
|
+
key: review,
|
|
44
|
+
root,
|
|
45
|
+
content: content instanceof HTMLElement ? content : undefined,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const root = scrollRoot(container)
|
|
50
|
+
if (root) {
|
|
51
|
+
const content = root.querySelector("[role='log']")
|
|
52
|
+
return {
|
|
53
|
+
key: root,
|
|
54
|
+
root,
|
|
55
|
+
content: content instanceof HTMLElement ? content : undefined,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
key: document,
|
|
61
|
+
root: document,
|
|
62
|
+
content: undefined,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function acquireVirtualizer(container: HTMLElement) {
|
|
67
|
+
const resolved = target(container)
|
|
68
|
+
if (!resolved) return
|
|
69
|
+
|
|
70
|
+
let entry = cache.get(resolved.key)
|
|
71
|
+
if (!entry) {
|
|
72
|
+
const virtualizer = new Virtualizer()
|
|
73
|
+
virtualizer.setup(resolved.root, resolved.content)
|
|
74
|
+
entry = {
|
|
75
|
+
virtualizer,
|
|
76
|
+
refs: 0,
|
|
77
|
+
}
|
|
78
|
+
cache.set(resolved.key, entry)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
entry.refs += 1
|
|
82
|
+
let done = false
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
virtualizer: entry.virtualizer,
|
|
86
|
+
release() {
|
|
87
|
+
if (done) return
|
|
88
|
+
done = true
|
|
89
|
+
|
|
90
|
+
const current = cache.get(resolved.key)
|
|
91
|
+
if (!current) return
|
|
92
|
+
|
|
93
|
+
current.refs -= 1
|
|
94
|
+
if (current.refs > 0) return
|
|
95
|
+
|
|
96
|
+
current.virtualizer.cleanUp()
|
|
97
|
+
cache.delete(resolved.key)
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { WorkerPoolManager } from "@pierre/diffs/worker"
|
|
2
|
+
import ShikiWorkerUrl from "@pierre/diffs/worker/worker.js?worker&url"
|
|
3
|
+
|
|
4
|
+
export type WorkerPoolStyle = "unified" | "split"
|
|
5
|
+
|
|
6
|
+
export function workerFactory(): Worker {
|
|
7
|
+
return new Worker(ShikiWorkerUrl, { type: "module" })
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function createPool(lineDiffType: "none" | "word-alt") {
|
|
11
|
+
const pool = new WorkerPoolManager(
|
|
12
|
+
{
|
|
13
|
+
workerFactory,
|
|
14
|
+
// poolSize defaults to 8. More workers = more parallelism but
|
|
15
|
+
// also more memory. Too many can actually slow things down.
|
|
16
|
+
// NOTE: 2 is probably better for OpenCode, as I think 8 might be
|
|
17
|
+
// a bit overkill, especially because Safari has a significantly slower
|
|
18
|
+
// boot up time for workers
|
|
19
|
+
poolSize: 2,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
theme: "OpenCode",
|
|
23
|
+
lineDiffType,
|
|
24
|
+
preferredHighlighter: "shiki-wasm",
|
|
25
|
+
},
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
void pool.initialize()
|
|
29
|
+
return pool
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let unified: WorkerPoolManager | undefined
|
|
33
|
+
let split: WorkerPoolManager | undefined
|
|
34
|
+
|
|
35
|
+
export function getWorkerPool(style: WorkerPoolStyle | undefined): WorkerPoolManager | undefined {
|
|
36
|
+
if (typeof window === "undefined") return
|
|
37
|
+
|
|
38
|
+
if (style === "split") {
|
|
39
|
+
if (!split) split = createPool("word-alt")
|
|
40
|
+
return split
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!unified) unified = createPool("none")
|
|
44
|
+
return unified
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getWorkerPools() {
|
|
48
|
+
return {
|
|
49
|
+
unified: getWorkerPool("unified"),
|
|
50
|
+
split: getWorkerPool("split"),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
@layer theme, base, components, utilities;
|
|
2
|
+
|
|
3
|
+
@import "../components/basic-tool.css" layer(components);
|
|
4
|
+
@import "../components/file.css" layer(components);
|
|
5
|
+
@import "../components/markdown.css" layer(components);
|
|
6
|
+
@import "../components/message-part.css" layer(components);
|
|
7
|
+
@import "../components/message-nav.css" layer(components);
|
|
8
|
+
@import "../components/session-review.css" layer(components);
|
|
9
|
+
@import "../components/session-turn.css" layer(components);
|
|
10
|
+
@import "../components/shell-submessage.css" layer(components);
|
|
11
|
+
@import "../components/tool-count-label.css" layer(components);
|
|
12
|
+
@import "../components/tool-count-summary.css" layer(components);
|
|
13
|
+
@import "../components/tool-error-card.css" layer(components);
|
|
14
|
+
@import "../components/tool-status-title.css" layer(components);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
[data-component="attachment-card-v2"] {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: 6px;
|
|
5
|
+
box-sizing: border-box;
|
|
6
|
+
width: 160px;
|
|
7
|
+
min-width: 160px;
|
|
8
|
+
max-width: 160px;
|
|
9
|
+
padding: 8px;
|
|
10
|
+
border-radius: 6px;
|
|
11
|
+
background: var(--v2-overlay-simple-overlay-hover);
|
|
12
|
+
box-shadow: inset 0 0 0 0.5px var(--v2-border-border-base);
|
|
13
|
+
cursor: default;
|
|
14
|
+
|
|
15
|
+
&[data-wide] {
|
|
16
|
+
width: 100%;
|
|
17
|
+
min-width: 0;
|
|
18
|
+
max-width: none;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
&[data-surface="base"] {
|
|
22
|
+
background: var(--v2-background-bg-base);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
&[data-active] {
|
|
26
|
+
box-shadow: inset 0 0 0 0.5px var(--v2-border-border-strong);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
&[data-clickable] {
|
|
30
|
+
cursor: pointer;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
[data-slot="attachment-card-v2-title"],
|
|
34
|
+
[data-slot="attachment-card-v2-subtitle"] {
|
|
35
|
+
max-width: 100%;
|
|
36
|
+
font-family: var(--v2-font-family-sans, "Inter", sans-serif);
|
|
37
|
+
font-style: normal;
|
|
38
|
+
font-size: 11px;
|
|
39
|
+
line-height: 12px;
|
|
40
|
+
letter-spacing: 0.05px;
|
|
41
|
+
font-variation-settings: "slnt" 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
[data-slot="attachment-card-v2-title"] {
|
|
45
|
+
width: 100%;
|
|
46
|
+
min-width: 0;
|
|
47
|
+
overflow: hidden;
|
|
48
|
+
text-overflow: ellipsis;
|
|
49
|
+
white-space: nowrap;
|
|
50
|
+
font-weight: 530;
|
|
51
|
+
color: var(--v2-text-text-base);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
[data-slot="attachment-card-v2-subtitle"] {
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
gap: 4px;
|
|
58
|
+
min-width: 0;
|
|
59
|
+
overflow: hidden;
|
|
60
|
+
white-space: nowrap;
|
|
61
|
+
font-weight: 440;
|
|
62
|
+
color: var(--v2-text-text-muted);
|
|
63
|
+
|
|
64
|
+
[data-component="file-icon"] {
|
|
65
|
+
width: 12px;
|
|
66
|
+
height: 12px;
|
|
67
|
+
flex: none;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { JSX } from "solid-js"
|
|
2
|
+
import "./attachment-card-v2.css"
|
|
3
|
+
|
|
4
|
+
/** Shared 160px two-line card used by v2 file and comment attachments in the composer and timeline. */
|
|
5
|
+
export function AttachmentCardV2(props: {
|
|
6
|
+
title: string
|
|
7
|
+
active?: boolean
|
|
8
|
+
clickable?: boolean
|
|
9
|
+
wide?: boolean
|
|
10
|
+
surface?: "base"
|
|
11
|
+
/** native title attribute */
|
|
12
|
+
hover?: string
|
|
13
|
+
titleRef?: (element: HTMLSpanElement) => void
|
|
14
|
+
onClick?: () => void
|
|
15
|
+
children: JSX.Element
|
|
16
|
+
}) {
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
data-component="attachment-card-v2"
|
|
20
|
+
data-active={props.active ? "true" : undefined}
|
|
21
|
+
data-clickable={props.clickable ? "true" : undefined}
|
|
22
|
+
data-wide={props.wide ? "true" : undefined}
|
|
23
|
+
data-surface={props.surface}
|
|
24
|
+
title={props.hover}
|
|
25
|
+
onClick={() => props.onClick?.()}
|
|
26
|
+
>
|
|
27
|
+
<span ref={(element) => props.titleRef?.(element)} data-slot="attachment-card-v2-title">
|
|
28
|
+
{props.title}
|
|
29
|
+
</span>
|
|
30
|
+
<span data-slot="attachment-card-v2-subtitle">{props.children}</span>
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
}
|