@workerdeck/ui 0.23.0 → 1.1.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-DUt2VzXG.mjs → SessionPanel-I5QSLLb0.mjs} +2266 -3459
  2. package/build/SessionPanel-I5QSLLb0.mjs.map +1 -0
  3. package/build/SessionPanel-yNO60Tzw.d.mts +151 -0
  4. package/build/format-C_lpFq0l.d.mts +16 -0
  5. package/build/format.d.mts +8 -76
  6. package/build/format.mjs +2 -2
  7. package/build/index.d.mts +291 -1695
  8. package/build/index.mjs +6 -301
  9. package/build/index.mjs.map +1 -1
  10. package/build/scoped.css +32 -23
  11. package/build/{status-BE-zg88x.mjs → status-Dm9INVTt.mjs} +17 -88
  12. package/build/status-Dm9INVTt.mjs.map +1 -0
  13. package/build/workspace.d.mts +8 -187
  14. package/build/workspace.mjs +13 -144
  15. package/build/workspace.mjs.map +1 -1
  16. package/package.json +24 -24
  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 +3 -9
  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 +315 -996
  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 +104 -219
  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 +96 -32
  141. package/src/workspace.ts +1 -24
  142. package/build/SessionPanel-BobynUo4.d.mts +0 -669
  143. package/build/SessionPanel-DUt2VzXG.mjs.map +0 -1
  144. package/build/format-ljc3lKpA.d.mts +0 -59
  145. package/build/status-BE-zg88x.mjs.map +0 -1
@@ -86,7 +86,9 @@ export function getListContext(text: string, cursorPos: number): ListContext | n
86
86
  const line = text.slice(lineStart, lineEnd === -1 ? text.length : lineEnd)
87
87
 
88
88
  const parsed = parseListLine(line)
89
- if (!parsed) return null
89
+ if (!parsed) {
90
+ return null
91
+ }
90
92
 
91
93
  return {
92
94
  lineStart,
@@ -102,16 +104,15 @@ export function getListContext(text: string, cursorPos: number): ListContext | n
102
104
  * Detects if the user just typed a list trigger pattern (e.g., "- " or "* ")
103
105
  * and returns the segments with the replacement applied.
104
106
  */
105
- export function autoFormatListPrefix(
106
- segments: Segment[],
107
- cursorPos: number,
108
- ): { segments: Segment[]; cursorOffset: number } | null {
107
+ export function autoFormatListPrefix(segments: Segment[], cursorPos: number): { segments: Segment[]; cursorOffset: number } | null {
109
108
  const plainText = segmentsToPlainText(segments)
110
109
  const lineStart = plainText.lastIndexOf('\n', cursorPos - 1) + 1
111
110
  const lineText = plainText.slice(lineStart, cursorPos)
112
111
 
113
112
  const match = lineText.match(/^(\s*)[-*] $/)
114
- if (!match) return null
113
+ if (!match) {
114
+ return null
115
+ }
115
116
 
116
117
  const indent = match[1]
117
118
  const replacement = `${indent}• `
@@ -128,13 +129,12 @@ export function autoFormatListPrefix(
128
129
  /**
129
130
  * Handles Enter key in a list line — continues the list or exits.
130
131
  */
131
- export function insertListContinuation(
132
- segments: Segment[],
133
- cursorPos: number,
134
- ): { segments: Segment[]; cursorOffset: number } | null {
132
+ export function insertListContinuation(segments: Segment[], cursorPos: number): { segments: Segment[]; cursorOffset: number } | null {
135
133
  const plainText = segmentsToPlainText(segments)
136
134
  const ctx = getListContext(plainText, cursorPos)
137
- if (!ctx) return null
135
+ if (!ctx) {
136
+ return null
137
+ }
138
138
 
139
139
  const lineEnd = plainText.indexOf('\n', cursorPos)
140
140
  const lineContent = plainText.slice(ctx.contentStart, lineEnd === -1 ? plainText.length : lineEnd)
@@ -149,12 +149,7 @@ export function insertListContinuation(
149
149
  cursorOffset: Math.max(ctx.lineStart, cursorPos - 2),
150
150
  }
151
151
  }
152
- const newSegments = replaceTextRange(
153
- segments,
154
- ctx.lineStart,
155
- ctx.lineStart + ctx.prefix.length,
156
- '',
157
- )
152
+ const newSegments = replaceTextRange(segments, ctx.lineStart, ctx.lineStart + ctx.prefix.length, '')
158
153
  return {
159
154
  segments: newSegments,
160
155
  cursorOffset: ctx.lineStart,
@@ -180,7 +175,9 @@ export function insertListContinuation(
180
175
 
181
176
  /** Returns the indent level of the list line directly above `lineStart`, or null. */
182
177
  function getPrevListLineLevel(text: string, lineStart: number): number | null {
183
- if (lineStart === 0) return null
178
+ if (lineStart === 0) {
179
+ return null
180
+ }
184
181
  const prevLineStart = text.lastIndexOf('\n', lineStart - 2) + 1
185
182
  const parsed = parseListLine(text.slice(prevLineStart, lineStart - 1))
186
183
  return parsed ? parsed.indent : null
@@ -193,17 +190,18 @@ function getPrevListLineLevel(text: string, lineStart: number): number | null {
193
190
  * parent — cannot indent further (returns null). This keeps sub-items visually
194
191
  * connected to a parent instead of drifting arbitrarily deep.
195
192
  */
196
- export function indentListItem(
197
- segments: Segment[],
198
- cursorPos: number,
199
- ): { segments: Segment[]; cursorOffset: number } | null {
193
+ export function indentListItem(segments: Segment[], cursorPos: number): { segments: Segment[]; cursorOffset: number } | null {
200
194
  const plainText = segmentsToPlainText(segments)
201
195
  const ctx = getListContext(plainText, cursorPos)
202
- if (!ctx) return null
196
+ if (!ctx) {
197
+ return null
198
+ }
203
199
 
204
200
  const prevLevel = getPrevListLineLevel(plainText, ctx.lineStart)
205
201
  const maxLevel = prevLevel === null ? 0 : prevLevel + 1
206
- if (ctx.indent >= maxLevel) return null
202
+ if (ctx.indent >= maxLevel) {
203
+ return null
204
+ }
207
205
 
208
206
  const newSegments = replaceTextRange(segments, ctx.lineStart, ctx.lineStart, ' ')
209
207
  return {
@@ -215,13 +213,12 @@ export function indentListItem(
215
213
  /**
216
214
  * Outdents a list item by one level (removes 2 spaces from before the prefix).
217
215
  */
218
- export function outdentListItem(
219
- segments: Segment[],
220
- cursorPos: number,
221
- ): { segments: Segment[]; cursorOffset: number } | null {
216
+ export function outdentListItem(segments: Segment[], cursorPos: number): { segments: Segment[]; cursorOffset: number } | null {
222
217
  const plainText = segmentsToPlainText(segments)
223
218
  const ctx = getListContext(plainText, cursorPos)
224
- if (!ctx || ctx.indent === 0) return null
219
+ if (!ctx || ctx.indent === 0) {
220
+ return null
221
+ }
225
222
 
226
223
  const newSegments = replaceTextRange(segments, ctx.lineStart, ctx.lineStart + 2, '')
227
224
  return {
@@ -233,22 +230,18 @@ export function outdentListItem(
233
230
  /**
234
231
  * Removes the list prefix from the current line (e.g., on Backspace).
235
232
  */
236
- export function removeListPrefix(
237
- segments: Segment[],
238
- cursorPos: number,
239
- ): { segments: Segment[]; cursorOffset: number } | null {
233
+ export function removeListPrefix(segments: Segment[], cursorPos: number): { segments: Segment[]; cursorOffset: number } | null {
240
234
  const plainText = segmentsToPlainText(segments)
241
235
  const ctx = getListContext(plainText, cursorPos)
242
- if (!ctx) return null
236
+ if (!ctx) {
237
+ return null
238
+ }
243
239
 
244
- if (cursorPos > ctx.contentStart) return null
240
+ if (cursorPos > ctx.contentStart) {
241
+ return null
242
+ }
245
243
 
246
- const newSegments = replaceTextRange(
247
- segments,
248
- ctx.lineStart,
249
- ctx.contentStart,
250
- ' '.repeat(ctx.indent),
251
- )
244
+ const newSegments = replaceTextRange(segments, ctx.lineStart, ctx.contentStart, ' '.repeat(ctx.indent))
252
245
  return {
253
246
  segments: newSegments,
254
247
  cursorOffset: ctx.lineStart + ctx.indent * 2,
@@ -274,11 +267,15 @@ function fenceProtectedLineIndices(lines: string[]): Set<number> {
274
267
  const protectedLines = new Set<number>()
275
268
  let openIndex = -1
276
269
  lines.forEach((line, i) => {
277
- if (!FENCE_LINE.test(line)) return
270
+ if (!FENCE_LINE.test(line)) {
271
+ return
272
+ }
278
273
  if (openIndex === -1) {
279
274
  openIndex = i
280
275
  } else {
281
- for (let k = openIndex; k <= i; k++) protectedLines.add(k)
276
+ for (let k = openIndex; k <= i; k++) {
277
+ protectedLines.add(k)
278
+ }
282
279
  openIndex = -1
283
280
  }
284
281
  })
@@ -296,9 +293,7 @@ function fenceProtectedLineIndices(lines: string[]): Set<number> {
296
293
  export function normalizeListPrefixText(text: string, markdownEnabled: boolean): string {
297
294
  const lines = text.split('\n')
298
295
  const protectedLines = fenceProtectedLineIndices(lines)
299
- return lines
300
- .map((line, i) => (protectedLines.has(i) ? line : swapListPrefixLine(line, markdownEnabled)))
301
- .join('\n')
296
+ return lines.map((line, i) => (protectedLines.has(i) ? line : swapListPrefixLine(line, markdownEnabled))).join('\n')
302
297
  }
303
298
 
304
299
  /**
@@ -311,32 +306,35 @@ export function normalizeListPrefixText(text: string, markdownEnabled: boolean):
311
306
  export function normalizeListPrefixes(segments: Segment[], markdownEnabled: boolean): Segment[] {
312
307
  const globalLines: string[] = []
313
308
  segments.forEach((seg) => {
314
- if (seg.type === 'text') globalLines.push(...seg.text.split('\n'))
309
+ if (seg.type === 'text') {
310
+ globalLines.push(...seg.text.split('\n'))
311
+ }
315
312
  })
316
313
  const protectedLines = fenceProtectedLineIndices(globalLines)
317
314
 
318
315
  let globalIndex = 0
319
316
  let changed = false
320
317
  const result = segments.map((seg) => {
321
- if (seg.type !== 'text') return seg
318
+ if (seg.type !== 'text') {
319
+ return seg
320
+ }
322
321
  const newText = seg.text
323
322
  .split('\n')
324
323
  .map((line) => {
325
- const out = protectedLines.has(globalIndex)
326
- ? line
327
- : swapListPrefixLine(line, markdownEnabled)
324
+ const out = protectedLines.has(globalIndex) ? line : swapListPrefixLine(line, markdownEnabled)
328
325
  globalIndex++
329
326
  return out
330
327
  })
331
328
  .join('\n')
332
- if (newText === seg.text) return seg
329
+ if (newText === seg.text) {
330
+ return seg
331
+ }
333
332
  changed = true
334
333
  return { ...seg, text: newText }
335
334
  })
336
335
  return changed ? result : segments
337
336
  }
338
337
 
339
- // ---------------------------------------------------------------------------
340
338
  // Ordered-list renumbering
341
339
  //
342
340
  // The visible number is a projection of position (like BlockNote/ProseMirror/
@@ -345,7 +343,6 @@ export function normalizeListPrefixes(segments: Segment[], markdownEnabled: bool
345
343
  // a fresh counter at 1, ascending continues the shallower level and clears
346
344
  // deeper ones. Every run restarts at 1, so `1. 1. 1.` rebuilds to `1. 2. 3.`
347
345
  // and Tab-indenting an item restarts its sublist at 1.
348
- // ---------------------------------------------------------------------------
349
346
 
350
347
  /**
351
348
  * A single rewritten number's digit run, in the INPUT text's coordinates.
@@ -375,7 +372,9 @@ export function hasOrderedListRun(text: string): boolean {
375
372
  sequential = sequential && parsed.number === prevNumber + 1
376
373
  prevNumber = parsed.number
377
374
  runLength++
378
- if (runLength >= 2 && (runStart === 1 || sequential)) return true
375
+ if (runLength >= 2 && (runStart === 1 || sequential)) {
376
+ return true
377
+ }
379
378
  } else if (parsed?.kind === 'numbered') {
380
379
  // Start a fresh run at this line's level.
381
380
  runLevel = parsed.indent
@@ -403,7 +402,9 @@ export function renumberOrderedListLines(text: string): { text: string; edits: N
403
402
  // skip the per-line scan and throwaway rebuild. This runs on every structural
404
403
  // edit (Enter, Tab, bold/italic wrap) and each paste, most of which never
405
404
  // touch a numbered list.
406
- if (!/^[ \t]*\d+\. /m.test(text)) return { text, edits: [] }
405
+ if (!/^[ \t]*\d+\. /m.test(text)) {
406
+ return { text, edits: [] }
407
+ }
407
408
 
408
409
  const counters = new Map<number, number>()
409
410
  const edits: NumberEdit[] = []
@@ -413,7 +414,9 @@ export function renumberOrderedListLines(text: string): { text: string; edits: N
413
414
 
414
415
  const clearDeeperThan = (level: number, inclusive: boolean) => {
415
416
  for (const key of counters.keys()) {
416
- if (inclusive ? key >= level : key > level) counters.delete(key)
417
+ if (inclusive ? key >= level : key > level) {
418
+ counters.delete(key)
419
+ }
417
420
  }
418
421
  }
419
422
 
@@ -450,7 +453,9 @@ export function renumberOrderedListLines(text: string): { text: string; edits: N
450
453
  }
451
454
  }
452
455
 
453
- if (i < lines.length - 1) out += '\n'
456
+ if (i < lines.length - 1) {
457
+ out += '\n'
458
+ }
454
459
  lineStart += line.length + 1 // + 1 for the consumed '\n'
455
460
  }
456
461
 
@@ -488,7 +493,9 @@ export function renumberOrderedListSegments(segments: Segment[]): {
488
493
  edits: NumberEdit[]
489
494
  } {
490
495
  const { edits } = renumberOrderedListLines(segmentsToPlainText(segments))
491
- if (edits.length === 0) return { segments, edits }
496
+ if (edits.length === 0) {
497
+ return { segments, edits }
498
+ }
492
499
 
493
500
  let result = segments
494
501
  for (let i = edits.length - 1; i >= 0; i--) {
@@ -115,26 +115,24 @@ export function PromptArea({
115
115
  maxLength,
116
116
  })
117
117
 
118
- // Expose imperative handle via ref
119
118
  useImperativeHandle(ref, () => handle, [handle])
120
119
 
121
- // Auto-focus on mount
122
120
  useEffect(() => {
123
121
  if (autoFocus) {
124
122
  editorRef.current?.focus()
125
123
  }
126
124
  }, [autoFocus, editorRef])
127
125
 
128
- // -----------------------------------------------------------------------
129
126
  // Auto-grow: expand on focus/input, shrink on blur
130
- // -----------------------------------------------------------------------
131
127
 
132
128
  const [isFocused, setIsFocused] = useState(false)
133
129
  const [editorHeight, setEditorHeight] = useState<number | undefined>(undefined)
134
130
 
135
131
  const syncHeight = useCallback(() => {
136
132
  const el = editorRef.current
137
- if (!el) return
133
+ if (!el) {
134
+ return
135
+ }
138
136
  // Temporarily set height to auto so scrollHeight reflects true content height
139
137
  el.style.height = 'auto'
140
138
  const contentHeight = el.scrollHeight
@@ -143,20 +141,28 @@ export function PromptArea({
143
141
  }, [editorRef])
144
142
 
145
143
  const handleFocus = useCallback(() => {
146
- if (!autoGrow) return
144
+ if (!autoGrow) {
145
+ return
146
+ }
147
147
  setIsFocused(true)
148
148
  syncHeight()
149
149
  }, [autoGrow, syncHeight])
150
150
 
151
151
  const handleBlurWithShrink = useCallback(() => {
152
152
  eventHandlers.onBlur()
153
- if (!autoGrow) return
153
+ if (!autoGrow) {
154
+ return
155
+ }
154
156
  setTimeout(() => {
155
157
  const editor = editorRef.current
156
- if (!editor) return
158
+ if (!editor) {
159
+ return
160
+ }
157
161
  // Only shrink if focus truly left the component
158
162
  const activeEl = document.activeElement
159
- if (activeEl && editor.parentElement?.contains(activeEl)) return
163
+ if (activeEl && editor.parentElement?.contains(activeEl)) {
164
+ return
165
+ }
160
166
  setIsFocused(false)
161
167
  setEditorHeight(undefined)
162
168
  }, BLUR_DELAY_MS)
@@ -176,15 +182,15 @@ export function PromptArea({
176
182
  }
177
183
  }, [value, autoGrow, isFocused, syncHeight])
178
184
 
179
- // -----------------------------------------------------------------------
180
185
  // Overflow indicator: detect when collapsed content is clipped
181
- // -----------------------------------------------------------------------
182
186
 
183
187
  const [hasOverflow, setHasOverflow] = useState(false)
184
188
  const overflowTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
185
189
 
186
190
  useEffect(() => {
187
- if (!autoGrow) return
191
+ if (!autoGrow) {
192
+ return
193
+ }
188
194
 
189
195
  const checkOverflow = () => {
190
196
  if (isFocused) {
@@ -192,7 +198,9 @@ export function PromptArea({
192
198
  return
193
199
  }
194
200
  const el = editorRef.current
195
- if (!el) return
201
+ if (!el) {
202
+ return
203
+ }
196
204
  setHasOverflow(el.scrollHeight > el.clientHeight)
197
205
  }
198
206
 
@@ -207,9 +215,7 @@ export function PromptArea({
207
215
  }
208
216
  }, [autoGrow, isFocused, value, editorRef])
209
217
 
210
- // -----------------------------------------------------------------------
211
218
  // Compute editor style
212
- // -----------------------------------------------------------------------
213
219
 
214
220
  const editorStyle = useMemo((): React.CSSProperties => {
215
221
  if (!autoGrow) {
@@ -238,7 +244,9 @@ export function PromptArea({
238
244
  const handleKeyDownCombined = useCallback(
239
245
  (e: React.KeyboardEvent<HTMLDivElement>) => {
240
246
  onKeyDown?.(e)
241
- if (e.defaultPrevented) return
247
+ if (e.defaultPrevented) {
248
+ return
249
+ }
242
250
  handleKeyDown(e)
243
251
  },
244
252
  [onKeyDown, handleKeyDown],
@@ -256,27 +264,16 @@ export function PromptArea({
256
264
  [onBlur, handleBlurWithShrink],
257
265
  )
258
266
 
259
- const isEmpty =
260
- value.length === 0 || (value.length === 1 && value[0].type === 'text' && value[0].text === '')
267
+ const isEmpty = value.length === 0 || (value.length === 1 && value[0].type === 'text' && value[0].text === '')
261
268
 
262
269
  const imageStrip =
263
270
  images.length > 0 ? (
264
- <ImageStrip
265
- images={images}
266
- onRemove={onImageRemove}
267
- onClick={onImageClick}
268
- className={imagePosition === 'above' ? 'pb-2' : 'pt-2'}
269
- />
271
+ <ImageStrip images={images} onRemove={onImageRemove} onClick={onImageClick} className={imagePosition === 'above' ? 'pb-2' : 'pt-2'} />
270
272
  ) : null
271
273
 
272
274
  const fileStrip =
273
275
  files.length > 0 ? (
274
- <FileStrip
275
- files={files}
276
- onRemove={onFileRemove}
277
- onClick={onFileClick}
278
- className={filePosition === 'above' ? 'pb-2' : 'pt-2'}
279
- />
276
+ <FileStrip files={files} onRemove={onFileRemove} onClick={onFileClick} className={filePosition === 'above' ? 'pb-2' : 'pt-2'} />
280
277
  ) : null
281
278
 
282
279
  // Typography (font-size/line-height) lives on the container, not the editor, so
@@ -286,8 +283,6 @@ export function PromptArea({
286
283
  <div className={cn('prompt-area-container relative text-sm leading-relaxed', className)}>
287
284
  {imagePosition === 'above' && imageStrip}
288
285
  {filePosition === 'above' && fileStrip}
289
-
290
- {/* Editor + placeholder wrapper */}
291
286
  <div className="relative">
292
287
  <div
293
288
  ref={editorRef}
@@ -327,7 +322,8 @@ export function PromptArea({
327
322
  aria-hidden="true"
328
323
  className="pointer-events-auto absolute right-0 bottom-0 left-0 cursor-pointer"
329
324
  style={{ height: '32px' }}
330
- onClick={() => editorRef.current?.focus()}>
325
+ onClick={() => editorRef.current?.focus()}
326
+ >
331
327
  <div
332
328
  className="h-full w-full"
333
329
  style={{
@@ -337,8 +333,6 @@ export function PromptArea({
337
333
  />
338
334
  </div>
339
335
  )}
340
-
341
- {/* Placeholder overlay */}
342
336
  {isEmpty &&
343
337
  placeholder &&
344
338
  (Array.isArray(placeholder) ? (
@@ -347,7 +341,8 @@ export function PromptArea({
347
341
  <div
348
342
  className="pointer-events-none absolute top-0 left-0 select-none"
349
343
  style={{ color: 'var(--prompt-area-placeholder, var(--muted-foreground))' }}
350
- aria-hidden="true">
344
+ aria-hidden="true"
345
+ >
351
346
  {placeholder}
352
347
  </div>
353
348
  ))}
@@ -355,8 +350,6 @@ export function PromptArea({
355
350
 
356
351
  {filePosition === 'below' && fileStrip}
357
352
  {imagePosition === 'below' && imageStrip}
358
-
359
- {/* Trigger suggestion popover */}
360
353
  {activeTrigger && activeTrigger.config.mode === 'dropdown' && (
361
354
  <TriggerPopover
362
355
  suggestions={suggestions}
@@ -20,15 +20,9 @@ export function RemoveButton({ onClick, label, className }: RemoveButtonProps) {
20
20
  'transition-colors',
21
21
  className,
22
22
  )}
23
- aria-label={label}>
24
- <svg
25
- width="8"
26
- height="8"
27
- viewBox="0 0 10 10"
28
- fill="none"
29
- stroke="currentColor"
30
- strokeWidth="1.5"
31
- strokeLinecap="round">
23
+ aria-label={label}
24
+ >
25
+ <svg width="8" height="8" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
32
26
  <line x1="2.75" y1="2.75" x2="7.25" y2="7.25" />
33
27
  <line x1="7.25" y1="2.75" x2="2.75" y2="7.25" />
34
28
  </svg>
@@ -20,10 +20,6 @@ import { segmentsToPlainText, plainTextToSegments } from './prompt-area-engine.t
20
20
  // Re-export serialization utilities so consumers have a single import.
21
21
  export { segmentsToPlainText, plainTextToSegments }
22
22
 
23
- // ---------------------------------------------------------------------------
24
- // Factories
25
- // ---------------------------------------------------------------------------
26
-
27
23
  /** Create a text segment. */
28
24
  export function text(value: string): TextSegment {
29
25
  return { type: 'text', text: value }
@@ -34,13 +30,11 @@ export function chip(opts: Omit<ChipSegment, 'type'>): ChipSegment {
34
30
  return { type: 'chip', ...opts }
35
31
  }
36
32
 
37
- // ---------------------------------------------------------------------------
38
- // Predicates
39
- // ---------------------------------------------------------------------------
40
-
41
33
  /** Returns `true` when the segment array is empty or contains only whitespace text. */
42
34
  export function isSegmentsEmpty(segments: Segment[]): boolean {
43
- if (segments.length === 0) return true
35
+ if (segments.length === 0) {
36
+ return true
37
+ }
44
38
  return segments.every((seg) => seg.type === 'text' && seg.text.trim() === '')
45
39
  }
46
40
 
@@ -56,7 +50,5 @@ export function getChips(segments: Segment[]): ChipSegment[] {
56
50
 
57
51
  /** Extracts chips matching a specific trigger character. */
58
52
  export function getChipsByTrigger(segments: Segment[], trigger: string): ChipSegment[] {
59
- return segments.filter(
60
- (seg): seg is ChipSegment => seg.type === 'chip' && seg.trigger === trigger,
61
- )
53
+ return segments.filter((seg): seg is ChipSegment => seg.type === 'chip' && seg.trigger === trigger)
62
54
  }
@@ -38,12 +38,10 @@ export function TriggerPopover({
38
38
  const popoverRef = useRef<HTMLDivElement>(null)
39
39
  const selectedRef = useRef<HTMLButtonElement>(null)
40
40
 
41
- // Scroll selected item into view
42
41
  useEffect(() => {
43
42
  selectedRef.current?.scrollIntoView({ block: 'nearest' })
44
43
  }, [selectedIndex])
45
44
 
46
- // Click outside to dismiss
47
45
  useEffect(() => {
48
46
  const handleClickOutside = (e: MouseEvent) => {
49
47
  const target = e.target
@@ -55,8 +53,12 @@ export function TriggerPopover({
55
53
  return () => document.removeEventListener('mousedown', handleClickOutside)
56
54
  }, [onDismiss])
57
55
 
58
- if (!triggerRect) return null
59
- if (suggestions.length === 0 && !loading && !error && !emptyMessage) return null
56
+ if (!triggerRect) {
57
+ return null
58
+ }
59
+ if (suggestions.length === 0 && !loading && !error && !emptyMessage) {
60
+ return null
61
+ }
60
62
 
61
63
  // Position the popover relative to the trigger character, clamped to the
62
64
  // viewport. Flip above the trigger when there isn't enough room below, so the
@@ -71,27 +73,19 @@ export function TriggerPopover({
71
73
  zIndex: 50,
72
74
  maxWidth: `${popoverMaxWidth}px`,
73
75
  maxHeight: `${POPOVER_MAX_HEIGHT}px`,
74
- ...(positionAbove
75
- ? { bottom: `${window.innerHeight - triggerRect.top + 4}px` }
76
- : { top: `${triggerRect.bottom + 4}px` }),
76
+ ...(positionAbove ? { bottom: `${window.innerHeight - triggerRect.top + 4}px` } : { top: `${triggerRect.bottom + 4}px` }),
77
77
  }
78
78
 
79
79
  return (
80
80
  <div
81
81
  ref={popoverRef}
82
- className={cn(
83
- 'min-w-[200px] overflow-y-auto',
84
- 'bg-surface rounded-xl border p-2 shadow-md',
85
- 'animate-in fade-in-0 zoom-in-95',
86
- )}
82
+ className={cn('min-w-[200px] overflow-y-auto', 'bg-surface rounded-xl border p-2 shadow-md', 'animate-in fade-in-0 zoom-in-95')}
87
83
  style={style}
88
84
  role="listbox"
89
- aria-label={`${triggerChar} suggestions`}>
85
+ aria-label={`${triggerChar} suggestions`}
86
+ >
90
87
  {loading ? (
91
- <div
92
- role="option"
93
- aria-selected={false}
94
- className="text-muted-foreground px-3 py-2 text-sm">
88
+ <div role="option" aria-selected={false} className="text-muted-foreground px-3 py-2 text-sm">
95
89
  Loading suggestions...
96
90
  </div>
97
91
  ) : error ? (
@@ -99,10 +93,7 @@ export function TriggerPopover({
99
93
  {error}
100
94
  </div>
101
95
  ) : suggestions.length === 0 && emptyMessage ? (
102
- <div
103
- role="option"
104
- aria-selected={false}
105
- className="text-muted-foreground px-3 py-2 text-sm">
96
+ <div role="option" aria-selected={false} className="text-muted-foreground px-3 py-2 text-sm">
106
97
  {emptyMessage}
107
98
  </div>
108
99
  ) : (
@@ -121,15 +112,12 @@ export function TriggerPopover({
121
112
  onMouseDown={(e) => {
122
113
  e.preventDefault() // Prevent blur on the editor
123
114
  onSelect(suggestion)
124
- }}>
115
+ }}
116
+ >
125
117
  {suggestion.icon && <span className="mt-0.5 shrink-0">{suggestion.icon}</span>}
126
118
  <div className="min-w-0 flex-1">
127
119
  <div className="truncate font-medium">{suggestion.label}</div>
128
- {suggestion.description && (
129
- <div className="text-muted-foreground truncate text-xs">
130
- {suggestion.description}
131
- </div>
132
- )}
120
+ {suggestion.description && <div className="text-muted-foreground truncate text-xs">{suggestion.description}</div>}
133
121
  </div>
134
122
  </button>
135
123
  ))
@@ -18,16 +18,12 @@
18
18
 
19
19
  import type { TriggerConfig, TriggerPosition } from './types.ts'
20
20
 
21
- // ---------------------------------------------------------------------------
22
21
  // Shared option type — everything in TriggerConfig except the keys each
23
22
  // factory sets by default.
24
- // ---------------------------------------------------------------------------
25
23
 
26
24
  type TriggerPresetOptions = Omit<Partial<TriggerConfig>, 'char' | 'position' | 'mode'>
27
25
 
28
- // ---------------------------------------------------------------------------
29
26
  // @mention — dropdown at any position
30
- // ---------------------------------------------------------------------------
31
27
 
32
28
  export type MentionTriggerOptions = TriggerPresetOptions & {
33
29
  /** Override the trigger character. Defaults to `'@'`. */
@@ -52,9 +48,7 @@ export function mentionTrigger(opts: MentionTriggerOptions = {}): TriggerConfig
52
48
  }
53
49
  }
54
50
 
55
- // ---------------------------------------------------------------------------
56
51
  // /command — dropdown anywhere (opt into line-start-only with `position`)
57
- // ---------------------------------------------------------------------------
58
52
 
59
53
  export type CommandTriggerOptions = TriggerPresetOptions & {
60
54
  /** Override the trigger character. Defaults to `'/'`. */
@@ -89,9 +83,7 @@ export function commandTrigger(opts: CommandTriggerOptions = {}): TriggerConfig
89
83
  }
90
84
  }
91
85
 
92
- // ---------------------------------------------------------------------------
93
86
  // #hashtag — dropdown at any position, auto-resolve on space
94
- // ---------------------------------------------------------------------------
95
87
 
96
88
  export type HashtagTriggerOptions = TriggerPresetOptions & {
97
89
  /** Override the trigger character. Defaults to `'#'`. */
@@ -117,9 +109,7 @@ export function hashtagTrigger(opts: HashtagTriggerOptions = {}): TriggerConfig
117
109
  }
118
110
  }
119
111
 
120
- // ---------------------------------------------------------------------------
121
112
  // Generic callback trigger (e.g., for file pickers, model selectors)
122
- // ---------------------------------------------------------------------------
123
113
 
124
114
  export type CallbackTriggerOptions = Omit<Partial<TriggerConfig>, 'mode'> & {
125
115
  /** The trigger character. Required. */