@workerdeck/ui 0.23.0 → 1.0.0
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/build/{SessionPanel-DUt2VzXG.mjs → SessionPanel-DDgBkHsi.mjs} +2151 -3357
- package/build/SessionPanel-DDgBkHsi.mjs.map +1 -0
- package/build/SessionPanel-E_L_ldQ5.d.mts +207 -0
- package/build/format-B8ZM9LPy.d.mts +16 -0
- package/build/format.d.mts +8 -75
- package/build/format.mjs +2 -2
- package/build/index.d.mts +63 -1138
- package/build/index.mjs +4 -300
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +15 -23
- package/build/{status-BE-zg88x.mjs → status-C0HSLZas.mjs} +14 -85
- package/build/status-C0HSLZas.mjs.map +1 -0
- package/build/workspace.d.mts +5 -126
- package/build/workspace.mjs +11 -143
- package/build/workspace.mjs.map +1 -1
- package/package.json +4 -4
- package/src/components/agent/CodeEditor.tsx +31 -103
- package/src/components/agent/Composer.tsx +114 -333
- package/src/components/agent/ContextDialog.tsx +20 -41
- package/src/components/agent/ContextRing.tsx +6 -28
- package/src/components/agent/Conversation.tsx +21 -33
- package/src/components/agent/EditorTabs.tsx +39 -67
- package/src/components/agent/EngineIcon.tsx +29 -72
- package/src/components/agent/EntryIcon.tsx +16 -0
- package/src/components/agent/FileCard.tsx +10 -16
- package/src/components/agent/FileTree.tsx +52 -125
- package/src/components/agent/FileViewer.tsx +30 -60
- package/src/components/agent/HostFilesDialog.tsx +40 -69
- package/src/components/agent/Loader.tsx +3 -24
- package/src/components/agent/McpDialog.tsx +64 -137
- package/src/components/agent/Message.tsx +2 -21
- package/src/components/agent/ModelSelect.tsx +33 -62
- package/src/components/agent/PermissionModeSelect.tsx +21 -79
- package/src/components/agent/PermissionPrompt.tsx +59 -63
- package/src/components/agent/ProjectIcon.tsx +228 -93
- package/src/components/agent/PromptTokenText.tsx +10 -16
- package/src/components/agent/QuestionPrompt.tsx +43 -64
- package/src/components/agent/Reasoning.tsx +13 -16
- package/src/components/agent/Response.tsx +3 -9
- package/src/components/agent/Scrubber.tsx +31 -80
- package/src/components/agent/SessionBrowser.tsx +64 -243
- package/src/components/agent/SessionEmptyState.tsx +10 -40
- package/src/components/agent/SessionInfoDialog.tsx +46 -78
- package/src/components/agent/SessionItem.tsx +53 -320
- package/src/components/agent/SessionList.tsx +23 -39
- package/src/components/agent/SessionPanel.tsx +315 -996
- package/src/components/agent/SessionStatusIcon.tsx +14 -28
- package/src/components/agent/SessionSteps.tsx +34 -187
- package/src/components/agent/SessionWorkspace.tsx +104 -219
- package/src/components/agent/SkillsDialog.tsx +35 -80
- package/src/components/agent/StatusBar.tsx +51 -136
- package/src/components/agent/SubagentStrip.tsx +12 -77
- package/src/components/agent/ToolCallCard.tsx +94 -135
- package/src/components/agent/Transcript.tsx +103 -1288
- package/src/components/agent/TranscriptItemView.tsx +155 -0
- package/src/components/agent/TranscriptRows.tsx +418 -0
- package/src/components/agent/UsageDialog.tsx +7 -29
- package/src/components/agent/UsageMeters.tsx +18 -73
- package/src/components/agent/pulse.tsx +9 -32
- package/src/components/agent/scrubber-marks.ts +33 -103
- package/src/components/agent/status.ts +1 -5
- package/src/components/agent/tool-result-fetch.tsx +3 -25
- package/src/components/agent/tool-result-image.tsx +32 -111
- package/src/components/agent/transcript-rows.ts +41 -118
- package/src/components/agent/transcript-variant.tsx +3 -80
- package/src/components/agent/use-height-epoch.ts +10 -24
- package/src/components/agent/use-path-links.ts +45 -63
- package/src/components/agent/use-subagent-frame.ts +106 -0
- package/src/components/agent/use-transcript-jumps.ts +49 -107
- package/src/components/prompt-area/animated-placeholder.tsx +6 -5
- package/src/components/prompt-area/clipboard-helpers.ts +26 -21
- package/src/components/prompt-area/cursor-helpers.ts +79 -37
- package/src/components/prompt-area/dom-helpers.ts +97 -69
- package/src/components/prompt-area/file-strip.tsx +90 -80
- package/src/components/prompt-area/html-to-markdown.ts +67 -46
- package/src/components/prompt-area/image-strip.tsx +8 -18
- package/src/components/prompt-area/index.ts +2 -15
- package/src/components/prompt-area/prompt-area-engine.ts +100 -115
- package/src/components/prompt-area/prompt-area-list-ops.ts +66 -59
- package/src/components/prompt-area/prompt-area.tsx +31 -38
- package/src/components/prompt-area/remove-button.tsx +3 -9
- package/src/components/prompt-area/segment-helpers.ts +4 -12
- package/src/components/prompt-area/trigger-popover.tsx +15 -27
- package/src/components/prompt-area/trigger-presets.ts +0 -10
- package/src/components/prompt-area/types.ts +3 -8
- package/src/components/prompt-area/use-chip-editing.ts +305 -0
- package/src/components/prompt-area/use-markdown-mode.ts +7 -17
- package/src/components/prompt-area/use-prompt-area-events.ts +80 -86
- package/src/components/prompt-area/use-prompt-area-keydown.ts +598 -0
- package/src/components/prompt-area/use-prompt-area-state.ts +5 -23
- package/src/components/prompt-area/use-prompt-area.ts +170 -896
- package/src/components/prompt-area/use-trigger-search.ts +21 -19
- package/src/components/terminal/PermissionPrompt.tsx +56 -59
- package/src/components/terminal/QuestionPrompt.tsx +28 -79
- package/src/components/terminal/StatusLine.tsx +13 -63
- package/src/components/terminal/TerminalTranscript.tsx +44 -160
- package/src/components/terminal/affordances.tsx +68 -98
- package/src/components/terminal/blocks.ts +20 -156
- package/src/components/terminal/diff.tsx +19 -67
- package/src/components/terminal/height.ts +163 -313
- package/src/components/terminal/image-box.ts +0 -44
- package/src/components/terminal/items.tsx +150 -305
- package/src/components/terminal/markdown.tsx +36 -95
- package/src/components/terminal/press.tsx +26 -53
- package/src/components/terminal/prompt.tsx +40 -170
- package/src/components/terminal/result-preview.ts +8 -56
- package/src/components/terminal/row.tsx +11 -98
- package/src/components/terminal/scrubber.tsx +108 -401
- package/src/components/terminal/surface.tsx +6 -54
- package/src/components/terminal/todos.ts +66 -0
- package/src/components/terminal/tool-run.ts +21 -192
- package/src/components/ui/AlertDialog.tsx +8 -23
- package/src/components/ui/Badge.tsx +18 -29
- package/src/components/ui/Button.tsx +7 -15
- package/src/components/ui/Card.tsx +2 -13
- package/src/components/ui/CodeBlock.tsx +10 -37
- package/src/components/ui/CopyButton.tsx +10 -23
- package/src/components/ui/Dialog.tsx +23 -36
- package/src/components/ui/Empty.tsx +6 -29
- package/src/components/ui/Input.tsx +14 -16
- package/src/components/ui/Menu.tsx +6 -18
- package/src/components/ui/PortalScope.tsx +1 -21
- package/src/components/ui/ProgressRing.tsx +8 -19
- package/src/components/ui/Select.tsx +18 -31
- package/src/components/ui/Sonner.tsx +1 -2
- package/src/components/ui/Spinner.tsx +1 -1
- package/src/components/ui/Splitter.tsx +30 -64
- package/src/components/ui/Textarea.tsx +2 -5
- package/src/components/ui/Tooltip.tsx +3 -13
- package/src/format.ts +0 -9
- package/src/index.ts +16 -113
- package/src/lib/clipboard.ts +8 -28
- package/src/lib/css.ts +8 -0
- package/src/lib/format.ts +101 -86
- package/src/lib/plan-request.ts +16 -0
- package/src/lib/status.ts +45 -95
- package/src/lib/tool-icon.ts +39 -47
- package/src/lib/utils.ts +1 -3
- package/src/styles/terminal.css +81 -49
- package/src/styles/theme.css +96 -32
- package/src/workspace.ts +1 -24
- package/build/SessionPanel-BobynUo4.d.mts +0 -669
- package/build/SessionPanel-DUt2VzXG.mjs.map +0 -1
- package/build/format-ljc3lKpA.d.mts +0 -59
- package/build/status-BE-zg88x.mjs.map +0 -1
|
@@ -28,17 +28,22 @@ export type SavedCursor = {
|
|
|
28
28
|
|
|
29
29
|
export function saveCursorPosition(editor: HTMLElement): SavedCursor | null {
|
|
30
30
|
const range = getSelectionRange()
|
|
31
|
-
if (!range)
|
|
32
|
-
|
|
31
|
+
if (!range) {
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
if (!editor.contains(range.startContainer)) {
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
33
37
|
|
|
34
38
|
const node = range.startContainer
|
|
35
39
|
if (node === editor) {
|
|
36
40
|
return { nodeIndex: range.startOffset, offset: 0 }
|
|
37
41
|
}
|
|
38
42
|
|
|
39
|
-
// Walk up to find the direct child of editor using type-safe helper
|
|
40
43
|
const directChild = getDirectChildContaining(editor, node)
|
|
41
|
-
if (!directChild)
|
|
44
|
+
if (!directChild) {
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
42
47
|
|
|
43
48
|
const nodeIndex = indexOfChildNode(editor, directChild)
|
|
44
49
|
return { nodeIndex, offset: range.startOffset }
|
|
@@ -46,10 +51,14 @@ export function saveCursorPosition(editor: HTMLElement): SavedCursor | null {
|
|
|
46
51
|
|
|
47
52
|
export function restoreCursorPosition(editor: HTMLElement, saved: SavedCursor): void {
|
|
48
53
|
const sel = window.getSelection()
|
|
49
|
-
if (!sel)
|
|
54
|
+
if (!sel) {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
50
57
|
|
|
51
58
|
const childNodes = editor.childNodes
|
|
52
|
-
if (childNodes.length === 0)
|
|
59
|
+
if (childNodes.length === 0) {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
53
62
|
|
|
54
63
|
const range = document.createRange()
|
|
55
64
|
|
|
@@ -77,8 +86,12 @@ export function restoreCursorPosition(editor: HTMLElement, saved: SavedCursor):
|
|
|
77
86
|
|
|
78
87
|
export function getCursorOffset(editor: HTMLElement): number | null {
|
|
79
88
|
const range = getSelectionRange()
|
|
80
|
-
if (!range)
|
|
81
|
-
|
|
89
|
+
if (!range) {
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
if (!editor.contains(range.startContainer)) {
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
82
95
|
|
|
83
96
|
const preRange = document.createRange()
|
|
84
97
|
preRange.selectNodeContents(editor)
|
|
@@ -93,7 +106,9 @@ export function getCursorOffset(editor: HTMLElement): number | null {
|
|
|
93
106
|
*/
|
|
94
107
|
export function createRangeAtOffset(editor: HTMLElement, targetOffset: number): Range | null {
|
|
95
108
|
const pos = findDOMPosition(editor, targetOffset)
|
|
96
|
-
if (!pos)
|
|
109
|
+
if (!pos) {
|
|
110
|
+
return null
|
|
111
|
+
}
|
|
97
112
|
|
|
98
113
|
const range = document.createRange()
|
|
99
114
|
range.setStart(pos.node, pos.offset)
|
|
@@ -118,18 +133,26 @@ export function createRangeAtOffset(editor: HTMLElement, targetOffset: number):
|
|
|
118
133
|
*/
|
|
119
134
|
function caretRect(range: Range): DOMRect | null {
|
|
120
135
|
const direct = range.getBoundingClientRect()
|
|
121
|
-
if (direct.height > 0)
|
|
136
|
+
if (direct.height > 0) {
|
|
137
|
+
return direct
|
|
138
|
+
}
|
|
122
139
|
|
|
123
140
|
const node = range.startContainer
|
|
124
|
-
if (node.nodeType !== Node.ELEMENT_NODE)
|
|
141
|
+
if (node.nodeType !== Node.ELEMENT_NODE) {
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
125
144
|
const children = (node as Element).childNodes
|
|
126
145
|
const neighbours = [children[range.startOffset], children[range.startOffset - 1]]
|
|
127
146
|
for (const neighbour of neighbours) {
|
|
128
|
-
if (!neighbour)
|
|
147
|
+
if (!neighbour) {
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
129
150
|
const probe = document.createRange()
|
|
130
151
|
probe.selectNode(neighbour)
|
|
131
152
|
const rect = probe.getBoundingClientRect()
|
|
132
|
-
if (rect.height > 0)
|
|
153
|
+
if (rect.height > 0) {
|
|
154
|
+
return rect
|
|
155
|
+
}
|
|
133
156
|
}
|
|
134
157
|
return null
|
|
135
158
|
}
|
|
@@ -152,21 +175,32 @@ function caretRect(range: Range): DOMRect | null {
|
|
|
152
175
|
*/
|
|
153
176
|
export function scrollCaretIntoView(editor: HTMLElement): void {
|
|
154
177
|
const sel = window.getSelection()
|
|
155
|
-
if (!sel || sel.rangeCount === 0)
|
|
178
|
+
if (!sel || sel.rangeCount === 0) {
|
|
179
|
+
return
|
|
180
|
+
}
|
|
156
181
|
const range = sel.getRangeAt(0)
|
|
157
|
-
if (!editor.contains(range.startContainer))
|
|
182
|
+
if (!editor.contains(range.startContainer)) {
|
|
183
|
+
return
|
|
184
|
+
}
|
|
158
185
|
|
|
159
186
|
const rect = caretRect(range)
|
|
160
|
-
if (!rect)
|
|
187
|
+
if (!rect) {
|
|
188
|
+
return
|
|
189
|
+
}
|
|
161
190
|
|
|
162
191
|
const box = editor.getBoundingClientRect()
|
|
163
|
-
if (rect.bottom > box.bottom)
|
|
164
|
-
|
|
192
|
+
if (rect.bottom > box.bottom) {
|
|
193
|
+
editor.scrollTop += rect.bottom - box.bottom
|
|
194
|
+
} else if (rect.top < box.top) {
|
|
195
|
+
editor.scrollTop -= box.top - rect.top
|
|
196
|
+
}
|
|
165
197
|
}
|
|
166
198
|
|
|
167
199
|
export function setCursorAtOffset(editor: HTMLElement, targetOffset: number): void {
|
|
168
200
|
const sel = window.getSelection()
|
|
169
|
-
if (!sel)
|
|
201
|
+
if (!sel) {
|
|
202
|
+
return
|
|
203
|
+
}
|
|
170
204
|
|
|
171
205
|
const pos = findDOMPosition(editor, targetOffset)
|
|
172
206
|
if (pos) {
|
|
@@ -198,7 +232,9 @@ export function getTextLengthInRange(range: Range): number {
|
|
|
198
232
|
} else if (isChipElement(node)) {
|
|
199
233
|
length += chipNodeTextLength(node)
|
|
200
234
|
} else if (isHTMLElement(node) && node.tagName === 'BR') {
|
|
201
|
-
if (node.dataset.sentinel)
|
|
235
|
+
if (node.dataset.sentinel) {
|
|
236
|
+
return
|
|
237
|
+
} // skip sentinel <br>
|
|
202
238
|
length += 1
|
|
203
239
|
} else if (isHTMLElement(node)) {
|
|
204
240
|
node.childNodes.forEach(walk)
|
|
@@ -215,15 +251,21 @@ export function getTextLengthInRange(range: Range): number {
|
|
|
215
251
|
*/
|
|
216
252
|
export function getSelectionOffsets(editor: HTMLElement): { start: number; end: number } | null {
|
|
217
253
|
const range = getSelectionRange()
|
|
218
|
-
if (!range)
|
|
219
|
-
|
|
254
|
+
if (!range) {
|
|
255
|
+
return null
|
|
256
|
+
}
|
|
257
|
+
if (!editor.contains(range.startContainer)) {
|
|
258
|
+
return null
|
|
259
|
+
}
|
|
220
260
|
|
|
221
261
|
const startRange = document.createRange()
|
|
222
262
|
startRange.selectNodeContents(editor)
|
|
223
263
|
startRange.setEnd(range.startContainer, range.startOffset)
|
|
224
264
|
const start = getTextLengthInRange(startRange)
|
|
225
265
|
|
|
226
|
-
if (range.collapsed)
|
|
266
|
+
if (range.collapsed) {
|
|
267
|
+
return { start, end: start }
|
|
268
|
+
}
|
|
227
269
|
|
|
228
270
|
const endRange = document.createRange()
|
|
229
271
|
endRange.selectNodeContents(editor)
|
|
@@ -237,13 +279,11 @@ export function getSelectionOffsets(editor: HTMLElement): { start: number; end:
|
|
|
237
279
|
* Sets a (potentially non-collapsed) selection at the given plain-text offsets.
|
|
238
280
|
* Used to restore selection after markdown wrap/unwrap operations.
|
|
239
281
|
*/
|
|
240
|
-
export function setSelectionAtOffsets(
|
|
241
|
-
editor: HTMLElement,
|
|
242
|
-
startOffset: number,
|
|
243
|
-
endOffset: number,
|
|
244
|
-
): void {
|
|
282
|
+
export function setSelectionAtOffsets(editor: HTMLElement, startOffset: number, endOffset: number): void {
|
|
245
283
|
const sel = window.getSelection()
|
|
246
|
-
if (!sel)
|
|
284
|
+
if (!sel) {
|
|
285
|
+
return
|
|
286
|
+
}
|
|
247
287
|
|
|
248
288
|
if (startOffset === endOffset) {
|
|
249
289
|
setCursorAtOffset(editor, startOffset)
|
|
@@ -252,7 +292,9 @@ export function setSelectionAtOffsets(
|
|
|
252
292
|
|
|
253
293
|
const startPos = findDOMPosition(editor, startOffset)
|
|
254
294
|
const endPos = findDOMPosition(editor, endOffset)
|
|
255
|
-
if (!startPos || !endPos)
|
|
295
|
+
if (!startPos || !endPos) {
|
|
296
|
+
return
|
|
297
|
+
}
|
|
256
298
|
|
|
257
299
|
const range = document.createRange()
|
|
258
300
|
range.setStart(startPos.node, startPos.offset)
|
|
@@ -265,10 +307,7 @@ export function setSelectionAtOffsets(
|
|
|
265
307
|
* Maps a plain-text offset to a DOM node + offset pair.
|
|
266
308
|
* Recurses into decoration elements (markdown spans, URL anchors).
|
|
267
309
|
*/
|
|
268
|
-
export function findDOMPosition(
|
|
269
|
-
container: HTMLElement,
|
|
270
|
-
targetOffset: number,
|
|
271
|
-
): { node: Node; offset: number } | null {
|
|
310
|
+
export function findDOMPosition(container: HTMLElement, targetOffset: number): { node: Node; offset: number } | null {
|
|
272
311
|
let remaining = targetOffset
|
|
273
312
|
|
|
274
313
|
for (let i = 0; i < container.childNodes.length; i++) {
|
|
@@ -283,12 +322,13 @@ export function findDOMPosition(
|
|
|
283
322
|
} else if (isChipElement(child)) {
|
|
284
323
|
const chipLen = chipNodeTextLength(child)
|
|
285
324
|
if (remaining <= chipLen) {
|
|
286
|
-
// Position after the chip element
|
|
287
325
|
return { node: container, offset: i + 1 }
|
|
288
326
|
}
|
|
289
327
|
remaining -= chipLen
|
|
290
328
|
} else if (isBRElement(child)) {
|
|
291
|
-
if (child.dataset.sentinel)
|
|
329
|
+
if (child.dataset.sentinel) {
|
|
330
|
+
continue
|
|
331
|
+
} // skip sentinel <br>
|
|
292
332
|
if (remaining <= 1) {
|
|
293
333
|
return { node: container, offset: i + 1 }
|
|
294
334
|
}
|
|
@@ -298,7 +338,9 @@ export function findDOMPosition(
|
|
|
298
338
|
const textLen = (child.textContent ?? '').length
|
|
299
339
|
if (remaining <= textLen) {
|
|
300
340
|
const result = findDOMPosition(child, remaining)
|
|
301
|
-
if (result)
|
|
341
|
+
if (result) {
|
|
342
|
+
return result
|
|
343
|
+
}
|
|
302
344
|
}
|
|
303
345
|
remaining -= textLen
|
|
304
346
|
}
|
|
@@ -7,9 +7,17 @@
|
|
|
7
7
|
|
|
8
8
|
import type { ChipSegment } from './types.ts'
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
/** URL pattern for detecting URLs in text content. */
|
|
11
|
+
const URL_PATTERN = /https?:\/\/[^\s),]+/g
|
|
12
|
+
/** Matches ***bold-italic***, **bold**, and *italic* spans. */
|
|
13
|
+
const MARKDOWN_INLINE_PATTERN = /(\*{3})(.+?)\*{3}|(\*{2})(.+?)\*{2}|(\*)(.+?)\*/g
|
|
14
|
+
/** Matches a `•` bullet glyph at the start of a line (optionally indented). */
|
|
15
|
+
const LIST_BULLET_PATTERN = /(^|\n)([ \t]*)•/g
|
|
16
|
+
/** A list line's leading indentation; the lookahead keeps the prefix itself out of the
|
|
17
|
+
* capture, so only the indentation is wrapped. */
|
|
18
|
+
const LIST_INDENT_PATTERN = /(^|\n)([ \t]+)(?=(?:[•\-*] |\d+\. ))/g
|
|
19
|
+
|
|
11
20
|
// Type Guards
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
21
|
|
|
14
22
|
/**
|
|
15
23
|
* Type guard: checks if a DOM node is an HTMLElement.
|
|
@@ -56,9 +64,7 @@ export function isLinkElement(node: Node): node is HTMLAnchorElement {
|
|
|
56
64
|
return node instanceof HTMLAnchorElement && node.dataset.url === 'true'
|
|
57
65
|
}
|
|
58
66
|
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
67
|
// Safe JSON
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
68
|
|
|
63
69
|
/**
|
|
64
70
|
* Safely parses a JSON string, returning `unknown` instead of `any`.
|
|
@@ -86,16 +92,16 @@ export function safeJsonStringify(value: unknown): string | undefined {
|
|
|
86
92
|
}
|
|
87
93
|
}
|
|
88
94
|
|
|
89
|
-
// ---------------------------------------------------------------------------
|
|
90
95
|
// DOM reading helpers
|
|
91
|
-
// ---------------------------------------------------------------------------
|
|
92
96
|
|
|
93
97
|
/**
|
|
94
98
|
* Reads the chip trigger character from a chip element's dataset.
|
|
95
99
|
* Returns undefined if the node is not a chip element.
|
|
96
100
|
*/
|
|
97
101
|
export function getChipTrigger(node: Node): string | undefined {
|
|
98
|
-
if (!isChipElement(node))
|
|
102
|
+
if (!isChipElement(node)) {
|
|
103
|
+
return undefined
|
|
104
|
+
}
|
|
99
105
|
return node.dataset.chipTrigger
|
|
100
106
|
}
|
|
101
107
|
|
|
@@ -103,7 +109,9 @@ export function getChipTrigger(node: Node): string | undefined {
|
|
|
103
109
|
* Reads the chip value from a chip element's dataset.
|
|
104
110
|
*/
|
|
105
111
|
export function getChipValue(node: Node): string | undefined {
|
|
106
|
-
if (!isChipElement(node))
|
|
112
|
+
if (!isChipElement(node)) {
|
|
113
|
+
return undefined
|
|
114
|
+
}
|
|
107
115
|
return node.dataset.chipValue
|
|
108
116
|
}
|
|
109
117
|
|
|
@@ -111,7 +119,9 @@ export function getChipValue(node: Node): string | undefined {
|
|
|
111
119
|
* Reads the chip display text from a chip element's dataset.
|
|
112
120
|
*/
|
|
113
121
|
export function getChipDisplay(node: Node): string | undefined {
|
|
114
|
-
if (!isChipElement(node))
|
|
122
|
+
if (!isChipElement(node)) {
|
|
123
|
+
return undefined
|
|
124
|
+
}
|
|
115
125
|
return node.dataset.chipDisplay ?? node.textContent ?? undefined
|
|
116
126
|
}
|
|
117
127
|
|
|
@@ -119,9 +129,13 @@ export function getChipDisplay(node: Node): string | undefined {
|
|
|
119
129
|
* Reads and safely parses the chip data from a chip element's dataset.
|
|
120
130
|
*/
|
|
121
131
|
export function getChipData(node: Node): unknown {
|
|
122
|
-
if (!isChipElement(node))
|
|
132
|
+
if (!isChipElement(node)) {
|
|
133
|
+
return undefined
|
|
134
|
+
}
|
|
123
135
|
const raw = node.dataset.chipData
|
|
124
|
-
if (!raw)
|
|
136
|
+
if (!raw) {
|
|
137
|
+
return undefined
|
|
138
|
+
}
|
|
125
139
|
return safeJsonParse(raw)
|
|
126
140
|
}
|
|
127
141
|
|
|
@@ -149,12 +163,16 @@ export function chipNodeTextLength(node: HTMLElement): number {
|
|
|
149
163
|
* fields are required or how optional `data` / `autoResolved` are attached.
|
|
150
164
|
*/
|
|
151
165
|
export function chipNodeToSegment(node: Node): ChipSegment | null {
|
|
152
|
-
if (!isChipElement(node))
|
|
166
|
+
if (!isChipElement(node)) {
|
|
167
|
+
return null
|
|
168
|
+
}
|
|
153
169
|
|
|
154
170
|
const trigger = getChipTrigger(node)
|
|
155
171
|
const value = getChipValue(node)
|
|
156
172
|
const displayText = getChipDisplay(node)
|
|
157
|
-
if (!trigger || value === undefined || !displayText)
|
|
173
|
+
if (!trigger || value === undefined || !displayText) {
|
|
174
|
+
return null
|
|
175
|
+
}
|
|
158
176
|
|
|
159
177
|
const data = getChipData(node)
|
|
160
178
|
const autoResolved = getChipAutoResolved(node)
|
|
@@ -169,9 +187,7 @@ export function chipNodeToSegment(node: Node): ChipSegment | null {
|
|
|
169
187
|
}
|
|
170
188
|
}
|
|
171
189
|
|
|
172
|
-
// ---------------------------------------------------------------------------
|
|
173
190
|
// DOM manipulation helpers
|
|
174
|
-
// ---------------------------------------------------------------------------
|
|
175
191
|
|
|
176
192
|
/**
|
|
177
193
|
* Finds the index of a direct child node within a parent element.
|
|
@@ -180,7 +196,9 @@ export function chipNodeToSegment(node: Node): ChipSegment | null {
|
|
|
180
196
|
export function indexOfChildNode(parent: HTMLElement, child: Node): number {
|
|
181
197
|
const children = parent.childNodes
|
|
182
198
|
for (let i = 0; i < children.length; i++) {
|
|
183
|
-
if (children[i] === child)
|
|
199
|
+
if (children[i] === child) {
|
|
200
|
+
return i
|
|
201
|
+
}
|
|
184
202
|
}
|
|
185
203
|
return -1
|
|
186
204
|
}
|
|
@@ -198,10 +216,18 @@ export function indexOfChildNode(parent: HTMLElement, child: Node): number {
|
|
|
198
216
|
* chip missing a required attribute (trigger/value/display), so this must too.
|
|
199
217
|
*/
|
|
200
218
|
export function childProducesSegment(child: Node): boolean {
|
|
201
|
-
if (child.nodeType === Node.TEXT_NODE)
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (
|
|
219
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
220
|
+
return (child.textContent ?? '') !== ''
|
|
221
|
+
}
|
|
222
|
+
if (isBRElement(child)) {
|
|
223
|
+
return !child.dataset.sentinel
|
|
224
|
+
}
|
|
225
|
+
if (isChipElement(child)) {
|
|
226
|
+
return chipNodeToSegment(child) !== null
|
|
227
|
+
}
|
|
228
|
+
if (isHTMLElement(child)) {
|
|
229
|
+
return (child.textContent ?? '') !== ''
|
|
230
|
+
}
|
|
205
231
|
return false
|
|
206
232
|
}
|
|
207
233
|
|
|
@@ -216,7 +242,9 @@ export function childProducesSegment(child: Node): boolean {
|
|
|
216
242
|
export function domChildIndexToSegmentIndex(editor: HTMLElement, childIndex: number): number {
|
|
217
243
|
let segIdx = 0
|
|
218
244
|
for (let i = 0; i < childIndex; i++) {
|
|
219
|
-
if (childProducesSegment(editor.childNodes[i]))
|
|
245
|
+
if (childProducesSegment(editor.childNodes[i])) {
|
|
246
|
+
segIdx++
|
|
247
|
+
}
|
|
220
248
|
}
|
|
221
249
|
return segIdx
|
|
222
250
|
}
|
|
@@ -229,7 +257,9 @@ export function domChildIndexToSegmentIndex(editor: HTMLElement, childIndex: num
|
|
|
229
257
|
export function getDirectChildContaining(ancestor: HTMLElement, descendant: Node): Node | null {
|
|
230
258
|
let node: Node | null = descendant
|
|
231
259
|
while (node !== null) {
|
|
232
|
-
if (node.parentNode === ancestor)
|
|
260
|
+
if (node.parentNode === ancestor) {
|
|
261
|
+
return node
|
|
262
|
+
}
|
|
233
263
|
node = node.parentNode
|
|
234
264
|
}
|
|
235
265
|
return null
|
|
@@ -242,12 +272,10 @@ export function getDirectChildContaining(ancestor: HTMLElement, descendant: Node
|
|
|
242
272
|
export function unwrapBlockElement(parent: HTMLElement, block: HTMLElement): void {
|
|
243
273
|
const fragment = document.createDocumentFragment()
|
|
244
274
|
|
|
245
|
-
// Move all children to fragment
|
|
246
275
|
while (block.firstChild) {
|
|
247
276
|
fragment.appendChild(block.firstChild)
|
|
248
277
|
}
|
|
249
278
|
|
|
250
|
-
// Add a BR after the unwrapped content
|
|
251
279
|
fragment.appendChild(document.createElement('br'))
|
|
252
280
|
|
|
253
281
|
parent.replaceChild(fragment, block)
|
|
@@ -274,10 +302,15 @@ export function normalizeEditorDOM(editor: HTMLElement): boolean {
|
|
|
274
302
|
for (let i = editor.childNodes.length - 1; i >= 0; i--) {
|
|
275
303
|
const child = editor.childNodes[i]
|
|
276
304
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (child
|
|
305
|
+
if (!(child instanceof HTMLElement)) {
|
|
306
|
+
continue
|
|
307
|
+
}
|
|
308
|
+
if (child.dataset.chipTrigger !== undefined) {
|
|
309
|
+
continue
|
|
310
|
+
}
|
|
311
|
+
if (child instanceof HTMLBRElement) {
|
|
312
|
+
continue
|
|
313
|
+
}
|
|
281
314
|
|
|
282
315
|
const tag = child.tagName
|
|
283
316
|
if (blockTags.has(tag)) {
|
|
@@ -304,18 +337,12 @@ export function normalizeEditorDOM(editor: HTMLElement): boolean {
|
|
|
304
337
|
}
|
|
305
338
|
}
|
|
306
339
|
|
|
307
|
-
// Merge adjacent text nodes
|
|
308
340
|
editor.normalize()
|
|
309
341
|
|
|
310
342
|
return changed
|
|
311
343
|
}
|
|
312
344
|
|
|
313
|
-
// ---------------------------------------------------------------------------
|
|
314
345
|
// URL decoration
|
|
315
|
-
// ---------------------------------------------------------------------------
|
|
316
|
-
|
|
317
|
-
/** URL pattern for detecting URLs in text content */
|
|
318
|
-
const URL_PATTERN = /https?:\/\/[^\s),]+/g
|
|
319
346
|
|
|
320
347
|
/**
|
|
321
348
|
* Walks direct-child text nodes in the editor and wraps URL text in
|
|
@@ -357,10 +384,14 @@ export function decorateURLsInEditor(editor: HTMLElement): boolean {
|
|
|
357
384
|
}
|
|
358
385
|
}
|
|
359
386
|
|
|
360
|
-
if (matches.length === 0)
|
|
387
|
+
if (matches.length === 0) {
|
|
388
|
+
continue
|
|
389
|
+
}
|
|
361
390
|
|
|
362
391
|
const parent = textNode.parentNode
|
|
363
|
-
if (!parent)
|
|
392
|
+
if (!parent) {
|
|
393
|
+
continue
|
|
394
|
+
}
|
|
364
395
|
|
|
365
396
|
// Validate URLs upfront – only keep those with safe protocols (CWE-79)
|
|
366
397
|
const safeMatches: Array<{ url: string; href: string; index: number }> = []
|
|
@@ -375,19 +406,19 @@ export function decorateURLsInEditor(editor: HTMLElement): boolean {
|
|
|
375
406
|
}
|
|
376
407
|
}
|
|
377
408
|
|
|
378
|
-
if (safeMatches.length === 0)
|
|
409
|
+
if (safeMatches.length === 0) {
|
|
410
|
+
continue
|
|
411
|
+
}
|
|
379
412
|
|
|
380
413
|
decorated = true
|
|
381
414
|
const fragment = document.createDocumentFragment()
|
|
382
415
|
let lastIndex = 0
|
|
383
416
|
|
|
384
417
|
for (const { url, href, index } of safeMatches) {
|
|
385
|
-
// Text before this URL
|
|
386
418
|
if (index > lastIndex) {
|
|
387
419
|
fragment.appendChild(document.createTextNode(text.slice(lastIndex, index)))
|
|
388
420
|
}
|
|
389
421
|
|
|
390
|
-
// Create the link element
|
|
391
422
|
const anchor = document.createElement('a')
|
|
392
423
|
anchor.href = href
|
|
393
424
|
anchor.target = '_blank'
|
|
@@ -400,7 +431,6 @@ export function decorateURLsInEditor(editor: HTMLElement): boolean {
|
|
|
400
431
|
lastIndex = index + url.length
|
|
401
432
|
}
|
|
402
433
|
|
|
403
|
-
// Text after the last URL
|
|
404
434
|
if (lastIndex < text.length) {
|
|
405
435
|
fragment.appendChild(document.createTextNode(text.slice(lastIndex)))
|
|
406
436
|
}
|
|
@@ -411,12 +441,7 @@ export function decorateURLsInEditor(editor: HTMLElement): boolean {
|
|
|
411
441
|
return decorated
|
|
412
442
|
}
|
|
413
443
|
|
|
414
|
-
// ---------------------------------------------------------------------------
|
|
415
444
|
// Markdown inline decoration
|
|
416
|
-
// ---------------------------------------------------------------------------
|
|
417
|
-
|
|
418
|
-
/** Pattern to find ***bold-italic***, **bold**, and *italic* markdown spans */
|
|
419
|
-
const MARKDOWN_INLINE_PATTERN = /(\*{3})(.+?)\*{3}|(\*{2})(.+?)\*{2}|(\*)(.+?)\*/g
|
|
420
445
|
|
|
421
446
|
/**
|
|
422
447
|
* Walks direct-child text nodes in the editor and wraps markdown-formatted
|
|
@@ -486,17 +511,20 @@ export function decorateMarkdownInEditor(editor: HTMLElement): boolean {
|
|
|
486
511
|
}
|
|
487
512
|
}
|
|
488
513
|
|
|
489
|
-
if (matches.length === 0)
|
|
514
|
+
if (matches.length === 0) {
|
|
515
|
+
continue
|
|
516
|
+
}
|
|
490
517
|
|
|
491
518
|
decorated = true
|
|
492
519
|
const parent = textNode.parentNode
|
|
493
|
-
if (!parent)
|
|
520
|
+
if (!parent) {
|
|
521
|
+
continue
|
|
522
|
+
}
|
|
494
523
|
|
|
495
524
|
const fragment = document.createDocumentFragment()
|
|
496
525
|
let lastIndex = 0
|
|
497
526
|
|
|
498
527
|
for (const { fullMatch, marker, content, index, className } of matches) {
|
|
499
|
-
// Text before this match
|
|
500
528
|
if (index > lastIndex) {
|
|
501
529
|
fragment.appendChild(document.createTextNode(text.slice(lastIndex, index)))
|
|
502
530
|
}
|
|
@@ -510,7 +538,6 @@ export function decorateMarkdownInEditor(editor: HTMLElement): boolean {
|
|
|
510
538
|
openMarker.className = 'prompt-area-md-marker'
|
|
511
539
|
openMarker.textContent = marker
|
|
512
540
|
|
|
513
|
-
// Styled content
|
|
514
541
|
const styledContent = document.createElement('span')
|
|
515
542
|
styledContent.className = className
|
|
516
543
|
styledContent.textContent = content
|
|
@@ -528,7 +555,6 @@ export function decorateMarkdownInEditor(editor: HTMLElement): boolean {
|
|
|
528
555
|
lastIndex = index + fullMatch.length
|
|
529
556
|
}
|
|
530
557
|
|
|
531
|
-
// Text after the last match
|
|
532
558
|
if (lastIndex < text.length) {
|
|
533
559
|
fragment.appendChild(document.createTextNode(text.slice(lastIndex)))
|
|
534
560
|
}
|
|
@@ -539,9 +565,6 @@ export function decorateMarkdownInEditor(editor: HTMLElement): boolean {
|
|
|
539
565
|
return decorated
|
|
540
566
|
}
|
|
541
567
|
|
|
542
|
-
/** Matches a `•` bullet glyph at the start of a line (optionally indented). */
|
|
543
|
-
const LIST_BULLET_PATTERN = /(^|\n)([ \t]*)•/g
|
|
544
|
-
|
|
545
568
|
/**
|
|
546
569
|
* Walks direct-child text nodes and wraps each line-leading `•` bullet glyph in
|
|
547
570
|
* a `<span class="prompt-area-list-bullet">` so CSS can size it up (the raw
|
|
@@ -574,11 +597,15 @@ export function decorateBulletsInEditor(editor: HTMLElement): boolean {
|
|
|
574
597
|
bulletIndices.push(match.index + match[1].length + match[2].length)
|
|
575
598
|
}
|
|
576
599
|
|
|
577
|
-
if (bulletIndices.length === 0)
|
|
600
|
+
if (bulletIndices.length === 0) {
|
|
601
|
+
continue
|
|
602
|
+
}
|
|
578
603
|
|
|
579
604
|
decorated = true
|
|
580
605
|
const parent = textNode.parentNode
|
|
581
|
-
if (!parent)
|
|
606
|
+
if (!parent) {
|
|
607
|
+
continue
|
|
608
|
+
}
|
|
582
609
|
|
|
583
610
|
const fragment = document.createDocumentFragment()
|
|
584
611
|
let lastIndex = 0
|
|
@@ -605,13 +632,6 @@ export function decorateBulletsInEditor(editor: HTMLElement): boolean {
|
|
|
605
632
|
return decorated
|
|
606
633
|
}
|
|
607
634
|
|
|
608
|
-
/**
|
|
609
|
-
* Matches the line-leading whitespace run of an indented list line (bullet or
|
|
610
|
-
* numbered). The lookahead keeps the list prefix itself out of the capture, so
|
|
611
|
-
* only the indentation is wrapped.
|
|
612
|
-
*/
|
|
613
|
-
const LIST_INDENT_PATTERN = /(^|\n)([ \t]+)(?=(?:[•\-*] |\d+\. ))/g
|
|
614
|
-
|
|
615
635
|
/**
|
|
616
636
|
* Wraps each list line's leading indentation in an inline-block
|
|
617
637
|
* `<span class="prompt-area-list-indent">` sized per nesting level, so nested
|
|
@@ -634,7 +654,9 @@ export function decorateListIndentInEditor(editor: HTMLElement): boolean {
|
|
|
634
654
|
const textNodes: Text[] = []
|
|
635
655
|
for (let i = 0; i < editor.childNodes.length; i++) {
|
|
636
656
|
const node = editor.childNodes[i]
|
|
637
|
-
if (isTextNode(node))
|
|
657
|
+
if (isTextNode(node)) {
|
|
658
|
+
textNodes.push(node)
|
|
659
|
+
}
|
|
638
660
|
}
|
|
639
661
|
|
|
640
662
|
for (const textNode of textNodes) {
|
|
@@ -647,11 +669,15 @@ export function decorateListIndentInEditor(editor: HTMLElement): boolean {
|
|
|
647
669
|
runs.push({ start, end: start + match[2].length })
|
|
648
670
|
}
|
|
649
671
|
|
|
650
|
-
if (runs.length === 0)
|
|
672
|
+
if (runs.length === 0) {
|
|
673
|
+
continue
|
|
674
|
+
}
|
|
651
675
|
|
|
652
676
|
decorated = true
|
|
653
677
|
const parent = textNode.parentNode
|
|
654
|
-
if (!parent)
|
|
678
|
+
if (!parent) {
|
|
679
|
+
continue
|
|
680
|
+
}
|
|
655
681
|
|
|
656
682
|
const fragment = document.createDocumentFragment()
|
|
657
683
|
let lastIndex = 0
|
|
@@ -703,12 +729,12 @@ export function decorateEditor(editor: HTMLElement, markdownEnabled: boolean): v
|
|
|
703
729
|
decorateBulletsInEditor(editor)
|
|
704
730
|
}
|
|
705
731
|
decorateURLsInEditor(editor)
|
|
706
|
-
if (markdownEnabled)
|
|
732
|
+
if (markdownEnabled) {
|
|
733
|
+
decorateMarkdownInEditor(editor)
|
|
734
|
+
}
|
|
707
735
|
}
|
|
708
736
|
|
|
709
|
-
// ---------------------------------------------------------------------------
|
|
710
737
|
// Selection helpers
|
|
711
|
-
// ---------------------------------------------------------------------------
|
|
712
738
|
|
|
713
739
|
/**
|
|
714
740
|
* Returns the first Range from the current window selection, or null if
|
|
@@ -716,6 +742,8 @@ export function decorateEditor(editor: HTMLElement, markdownEnabled: boolean): v
|
|
|
716
742
|
*/
|
|
717
743
|
export function getSelectionRange(): Range | null {
|
|
718
744
|
const sel = window.getSelection()
|
|
719
|
-
if (!sel || sel.rangeCount === 0)
|
|
745
|
+
if (!sel || sel.rangeCount === 0) {
|
|
746
|
+
return null
|
|
747
|
+
}
|
|
720
748
|
return sel.getRangeAt(0)
|
|
721
749
|
}
|