@workerdeck/ui 0.22.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.
Files changed (145) hide show
  1. package/build/{SessionPanel-DgwjH4Ve.mjs → SessionPanel-DDgBkHsi.mjs} +2196 -3364
  2. package/build/SessionPanel-DDgBkHsi.mjs.map +1 -0
  3. package/build/SessionPanel-E_L_ldQ5.d.mts +207 -0
  4. package/build/format-B8ZM9LPy.d.mts +16 -0
  5. package/build/format.d.mts +8 -75
  6. package/build/format.mjs +2 -2
  7. package/build/index.d.mts +63 -1138
  8. package/build/index.mjs +4 -300
  9. package/build/index.mjs.map +1 -1
  10. package/build/scoped.css +80 -24
  11. package/build/{status-BE-zg88x.mjs → status-C0HSLZas.mjs} +14 -85
  12. package/build/status-C0HSLZas.mjs.map +1 -0
  13. package/build/workspace.d.mts +9 -124
  14. package/build/workspace.mjs +13 -143
  15. package/build/workspace.mjs.map +1 -1
  16. package/package.json +4 -4
  17. package/src/components/agent/CodeEditor.tsx +31 -103
  18. package/src/components/agent/Composer.tsx +114 -333
  19. package/src/components/agent/ContextDialog.tsx +20 -41
  20. package/src/components/agent/ContextRing.tsx +6 -28
  21. package/src/components/agent/Conversation.tsx +21 -33
  22. package/src/components/agent/EditorTabs.tsx +39 -67
  23. package/src/components/agent/EngineIcon.tsx +29 -72
  24. package/src/components/agent/EntryIcon.tsx +16 -0
  25. package/src/components/agent/FileCard.tsx +10 -16
  26. package/src/components/agent/FileTree.tsx +52 -125
  27. package/src/components/agent/FileViewer.tsx +30 -60
  28. package/src/components/agent/HostFilesDialog.tsx +40 -69
  29. package/src/components/agent/Loader.tsx +3 -24
  30. package/src/components/agent/McpDialog.tsx +64 -137
  31. package/src/components/agent/Message.tsx +2 -21
  32. package/src/components/agent/ModelSelect.tsx +33 -62
  33. package/src/components/agent/PermissionModeSelect.tsx +21 -79
  34. package/src/components/agent/PermissionPrompt.tsx +59 -63
  35. package/src/components/agent/ProjectIcon.tsx +228 -93
  36. package/src/components/agent/PromptTokenText.tsx +10 -16
  37. package/src/components/agent/QuestionPrompt.tsx +43 -64
  38. package/src/components/agent/Reasoning.tsx +13 -16
  39. package/src/components/agent/Response.tsx +4 -6
  40. package/src/components/agent/Scrubber.tsx +31 -80
  41. package/src/components/agent/SessionBrowser.tsx +64 -243
  42. package/src/components/agent/SessionEmptyState.tsx +10 -40
  43. package/src/components/agent/SessionInfoDialog.tsx +46 -78
  44. package/src/components/agent/SessionItem.tsx +53 -320
  45. package/src/components/agent/SessionList.tsx +23 -39
  46. package/src/components/agent/SessionPanel.tsx +345 -923
  47. package/src/components/agent/SessionStatusIcon.tsx +14 -28
  48. package/src/components/agent/SessionSteps.tsx +34 -187
  49. package/src/components/agent/SessionWorkspace.tsx +108 -215
  50. package/src/components/agent/SkillsDialog.tsx +35 -80
  51. package/src/components/agent/StatusBar.tsx +51 -136
  52. package/src/components/agent/SubagentStrip.tsx +12 -77
  53. package/src/components/agent/ToolCallCard.tsx +94 -135
  54. package/src/components/agent/Transcript.tsx +103 -1288
  55. package/src/components/agent/TranscriptItemView.tsx +155 -0
  56. package/src/components/agent/TranscriptRows.tsx +418 -0
  57. package/src/components/agent/UsageDialog.tsx +7 -29
  58. package/src/components/agent/UsageMeters.tsx +18 -73
  59. package/src/components/agent/pulse.tsx +9 -32
  60. package/src/components/agent/scrubber-marks.ts +33 -103
  61. package/src/components/agent/status.ts +1 -5
  62. package/src/components/agent/tool-result-fetch.tsx +3 -25
  63. package/src/components/agent/tool-result-image.tsx +32 -111
  64. package/src/components/agent/transcript-rows.ts +41 -118
  65. package/src/components/agent/transcript-variant.tsx +3 -80
  66. package/src/components/agent/use-height-epoch.ts +10 -24
  67. package/src/components/agent/use-path-links.ts +45 -63
  68. package/src/components/agent/use-subagent-frame.ts +106 -0
  69. package/src/components/agent/use-transcript-jumps.ts +49 -107
  70. package/src/components/prompt-area/animated-placeholder.tsx +6 -5
  71. package/src/components/prompt-area/clipboard-helpers.ts +26 -21
  72. package/src/components/prompt-area/cursor-helpers.ts +79 -37
  73. package/src/components/prompt-area/dom-helpers.ts +97 -69
  74. package/src/components/prompt-area/file-strip.tsx +90 -80
  75. package/src/components/prompt-area/html-to-markdown.ts +67 -46
  76. package/src/components/prompt-area/image-strip.tsx +8 -18
  77. package/src/components/prompt-area/index.ts +2 -15
  78. package/src/components/prompt-area/prompt-area-engine.ts +100 -115
  79. package/src/components/prompt-area/prompt-area-list-ops.ts +66 -59
  80. package/src/components/prompt-area/prompt-area.tsx +31 -38
  81. package/src/components/prompt-area/remove-button.tsx +3 -9
  82. package/src/components/prompt-area/segment-helpers.ts +4 -12
  83. package/src/components/prompt-area/trigger-popover.tsx +15 -27
  84. package/src/components/prompt-area/trigger-presets.ts +0 -10
  85. package/src/components/prompt-area/types.ts +3 -8
  86. package/src/components/prompt-area/use-chip-editing.ts +305 -0
  87. package/src/components/prompt-area/use-markdown-mode.ts +7 -17
  88. package/src/components/prompt-area/use-prompt-area-events.ts +80 -86
  89. package/src/components/prompt-area/use-prompt-area-keydown.ts +598 -0
  90. package/src/components/prompt-area/use-prompt-area-state.ts +5 -23
  91. package/src/components/prompt-area/use-prompt-area.ts +170 -896
  92. package/src/components/prompt-area/use-trigger-search.ts +21 -19
  93. package/src/components/terminal/PermissionPrompt.tsx +56 -59
  94. package/src/components/terminal/QuestionPrompt.tsx +28 -79
  95. package/src/components/terminal/StatusLine.tsx +13 -63
  96. package/src/components/terminal/TerminalTranscript.tsx +44 -160
  97. package/src/components/terminal/affordances.tsx +68 -98
  98. package/src/components/terminal/blocks.ts +20 -156
  99. package/src/components/terminal/diff.tsx +19 -67
  100. package/src/components/terminal/height.ts +163 -313
  101. package/src/components/terminal/image-box.ts +0 -44
  102. package/src/components/terminal/items.tsx +150 -305
  103. package/src/components/terminal/markdown.tsx +36 -95
  104. package/src/components/terminal/press.tsx +26 -53
  105. package/src/components/terminal/prompt.tsx +40 -170
  106. package/src/components/terminal/result-preview.ts +8 -56
  107. package/src/components/terminal/row.tsx +11 -98
  108. package/src/components/terminal/scrubber.tsx +108 -401
  109. package/src/components/terminal/surface.tsx +6 -54
  110. package/src/components/terminal/todos.ts +66 -0
  111. package/src/components/terminal/tool-run.ts +21 -192
  112. package/src/components/ui/AlertDialog.tsx +8 -23
  113. package/src/components/ui/Badge.tsx +18 -29
  114. package/src/components/ui/Button.tsx +7 -15
  115. package/src/components/ui/Card.tsx +2 -13
  116. package/src/components/ui/CodeBlock.tsx +10 -37
  117. package/src/components/ui/CopyButton.tsx +10 -23
  118. package/src/components/ui/Dialog.tsx +23 -36
  119. package/src/components/ui/Empty.tsx +6 -29
  120. package/src/components/ui/Input.tsx +14 -16
  121. package/src/components/ui/Menu.tsx +6 -18
  122. package/src/components/ui/PortalScope.tsx +1 -21
  123. package/src/components/ui/ProgressRing.tsx +8 -19
  124. package/src/components/ui/Select.tsx +18 -31
  125. package/src/components/ui/Sonner.tsx +1 -2
  126. package/src/components/ui/Spinner.tsx +1 -1
  127. package/src/components/ui/Splitter.tsx +30 -64
  128. package/src/components/ui/Textarea.tsx +2 -5
  129. package/src/components/ui/Tooltip.tsx +3 -13
  130. package/src/format.ts +0 -9
  131. package/src/index.ts +16 -113
  132. package/src/lib/clipboard.ts +8 -28
  133. package/src/lib/css.ts +8 -0
  134. package/src/lib/format.ts +101 -86
  135. package/src/lib/plan-request.ts +16 -0
  136. package/src/lib/status.ts +45 -95
  137. package/src/lib/tool-icon.ts +39 -47
  138. package/src/lib/utils.ts +1 -3
  139. package/src/styles/terminal.css +81 -49
  140. package/src/styles/theme.css +136 -19
  141. package/src/workspace.ts +1 -24
  142. package/build/SessionPanel-13l25ubU.d.mts +0 -609
  143. package/build/SessionPanel-DgwjH4Ve.mjs.map +0 -1
  144. package/build/format-ljc3lKpA.d.mts +0 -59
  145. 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) return null
32
- if (!editor.contains(range.startContainer)) return null
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) return null
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) return
54
+ if (!sel) {
55
+ return
56
+ }
50
57
 
51
58
  const childNodes = editor.childNodes
52
- if (childNodes.length === 0) return
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) return null
81
- if (!editor.contains(range.startContainer)) return null
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) return null
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) return direct
136
+ if (direct.height > 0) {
137
+ return direct
138
+ }
122
139
 
123
140
  const node = range.startContainer
124
- if (node.nodeType !== Node.ELEMENT_NODE) return null
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) continue
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) return rect
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) return
178
+ if (!sel || sel.rangeCount === 0) {
179
+ return
180
+ }
156
181
  const range = sel.getRangeAt(0)
157
- if (!editor.contains(range.startContainer)) return
182
+ if (!editor.contains(range.startContainer)) {
183
+ return
184
+ }
158
185
 
159
186
  const rect = caretRect(range)
160
- if (!rect) return
187
+ if (!rect) {
188
+ return
189
+ }
161
190
 
162
191
  const box = editor.getBoundingClientRect()
163
- if (rect.bottom > box.bottom) editor.scrollTop += rect.bottom - box.bottom
164
- else if (rect.top < box.top) editor.scrollTop -= box.top - rect.top
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) return
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) return // skip sentinel <br>
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) return null
219
- if (!editor.contains(range.startContainer)) return null
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) return { start, end: start }
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) return
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) return
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) continue // skip sentinel <br>
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) return 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)) return undefined
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)) return undefined
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)) return undefined
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)) return undefined
132
+ if (!isChipElement(node)) {
133
+ return undefined
134
+ }
123
135
  const raw = node.dataset.chipData
124
- if (!raw) return undefined
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)) return null
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) return null
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) return i
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) return (child.textContent ?? '') !== ''
202
- if (isBRElement(child)) return !child.dataset.sentinel
203
- if (isChipElement(child)) return chipNodeToSegment(child) !== null
204
- if (isHTMLElement(child)) return (child.textContent ?? '') !== ''
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])) segIdx++
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) return node
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
- // Skip non-element nodes, chip elements, and BR elements
278
- if (!(child instanceof HTMLElement)) continue
279
- if (child.dataset.chipTrigger !== undefined) continue
280
- if (child instanceof HTMLBRElement) continue
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) continue
387
+ if (matches.length === 0) {
388
+ continue
389
+ }
361
390
 
362
391
  const parent = textNode.parentNode
363
- if (!parent) continue
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) continue
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) continue
514
+ if (matches.length === 0) {
515
+ continue
516
+ }
490
517
 
491
518
  decorated = true
492
519
  const parent = textNode.parentNode
493
- if (!parent) continue
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) continue
600
+ if (bulletIndices.length === 0) {
601
+ continue
602
+ }
578
603
 
579
604
  decorated = true
580
605
  const parent = textNode.parentNode
581
- if (!parent) continue
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)) textNodes.push(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) continue
672
+ if (runs.length === 0) {
673
+ continue
674
+ }
651
675
 
652
676
  decorated = true
653
677
  const parent = textNode.parentNode
654
- if (!parent) continue
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) decorateMarkdownInEditor(editor)
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) return null
745
+ if (!sel || sel.rangeCount === 0) {
746
+ return null
747
+ }
720
748
  return sel.getRangeAt(0)
721
749
  }