@workerdeck/ui 2.7.1 → 2.8.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 (61) hide show
  1. package/README.md +30 -30
  2. package/build/{SessionPanel-B5FfiBxE.mjs → SessionPanel-Dg4xf-ql.mjs} +46 -46
  3. package/build/SessionPanel-Dg4xf-ql.mjs.map +1 -0
  4. package/build/format.mjs +1 -1
  5. package/build/index.d.mts +11 -11
  6. package/build/index.mjs +4 -4
  7. package/build/index.mjs.map +1 -1
  8. package/build/scoped.css +2 -2
  9. package/build/{status-BO9wJloK.mjs → status-kmsBhuFG.mjs} +3 -3
  10. package/build/status-kmsBhuFG.mjs.map +1 -0
  11. package/build/workspace.mjs +4 -4
  12. package/build/workspace.mjs.map +1 -1
  13. package/package.json +18 -18
  14. package/src/components/agent/Composer.tsx +2 -2
  15. package/src/components/agent/ContextDialog.tsx +1 -1
  16. package/src/components/agent/EntryIcon.tsx +1 -1
  17. package/src/components/agent/FileTree.tsx +1 -1
  18. package/src/components/agent/FileViewer.tsx +1 -1
  19. package/src/components/agent/HostFilesDialog.tsx +2 -2
  20. package/src/components/agent/PermissionModeSelect.tsx +2 -2
  21. package/src/components/agent/PermissionPrompt.tsx +2 -2
  22. package/src/components/agent/SessionBrowser.tsx +1 -1
  23. package/src/components/agent/SessionItem.tsx +1 -1
  24. package/src/components/agent/SessionPanel.tsx +2 -2
  25. package/src/components/agent/SkillsDialog.tsx +3 -3
  26. package/src/components/agent/StatusBar.tsx +1 -1
  27. package/src/components/agent/TaskList.tsx +2 -2
  28. package/src/components/agent/TranscriptRows.tsx +1 -1
  29. package/src/components/agent/UsageDialog.tsx +1 -1
  30. package/src/components/agent/use-subagent-frame.ts +2 -2
  31. package/src/components/agent/use-transcript-jumps.ts +2 -2
  32. package/src/components/prompt-area/clipboard-helpers.ts +2 -2
  33. package/src/components/prompt-area/cursor-helpers.ts +7 -7
  34. package/src/components/prompt-area/dom-helpers.ts +8 -8
  35. package/src/components/prompt-area/html-to-markdown.ts +2 -2
  36. package/src/components/prompt-area/prompt-area-engine.ts +4 -4
  37. package/src/components/prompt-area/prompt-area-list-ops.ts +10 -10
  38. package/src/components/prompt-area/prompt-area.tsx +2 -2
  39. package/src/components/prompt-area/trigger-presets.ts +6 -6
  40. package/src/components/prompt-area/types.ts +6 -6
  41. package/src/components/prompt-area/use-chip-editing.ts +8 -8
  42. package/src/components/prompt-area/use-markdown-mode.ts +3 -3
  43. package/src/components/prompt-area/use-prompt-area-events.ts +3 -3
  44. package/src/components/prompt-area/use-prompt-area-keydown.ts +3 -3
  45. package/src/components/prompt-area/use-prompt-area-state.ts +4 -4
  46. package/src/components/prompt-area/use-prompt-area.ts +5 -5
  47. package/src/components/prompt-area/use-trigger-search.ts +1 -1
  48. package/src/components/terminal/PermissionPrompt.tsx +4 -4
  49. package/src/components/terminal/affordances.tsx +1 -1
  50. package/src/components/terminal/height.ts +2 -2
  51. package/src/components/terminal/items.tsx +2 -2
  52. package/src/components/terminal/scrubber.tsx +1 -1
  53. package/src/components/terminal/todos.ts +1 -1
  54. package/src/components/ui/Splitter.tsx +1 -1
  55. package/src/lib/context-note.ts +3 -3
  56. package/src/lib/format.ts +2 -2
  57. package/src/styles/scoped.entry.css +2 -2
  58. package/src/styles/terminal.css +79 -79
  59. package/src/styles/theme.css +39 -39
  60. package/build/SessionPanel-B5FfiBxE.mjs.map +0 -1
  61. package/build/status-BO9wJloK.mjs.map +0 -1
@@ -7,7 +7,7 @@ import { cn } from '../../lib/utils.ts'
7
7
  export interface TaskListProps {
8
8
  tasks: readonly SessionTask[]
9
9
  showCompleted: boolean
10
- // Absent where the toggle lives outside this list — VS Code puts it in the view's title bar.
10
+ // Absent where the toggle lives outside this list - VS Code puts it in the view's title bar.
11
11
  onShowCompletedChange?: (showCompleted: boolean) => void
12
12
  onSelectTask?: (task: SessionTask) => void
13
13
  className?: string
@@ -36,7 +36,7 @@ export function TaskList({ tasks, showCompleted, onShowCompletedChange, onSelect
36
36
  </div>
37
37
  {shown.length === 0 ? (
38
38
  <p className="py-6 text-center text-body-sm text-fg-4">
39
- {tasks.length === 0 ? 'No tasks yet — a checklist appears once the agent plans one.' : `${hidden} completed, all hidden.`}
39
+ {tasks.length === 0 ? 'No tasks yet - a checklist appears once the agent plans one.' : `${hidden} completed, all hidden.`}
40
40
  </p>
41
41
  ) : (
42
42
  <div className="flex flex-col">
@@ -91,7 +91,7 @@ function StickyPromptLane({
91
91
  }
92
92
 
93
93
  // The virtualized row lane and its scroll-ownership regime. Two things want to write
94
- // `scrollTop` — the follow spring and the virtualizer's size-change corrections — and they are
94
+ // `scrollTop` - the follow spring and the virtualizer's size-change corrections - and they are
95
95
  // split by regime here: pinned, corrections are suppressed outright; escaped, the virtualizer
96
96
  // corrects so the scrollback holds still (GOTCHAS "The transcript is virtualized").
97
97
  export function TranscriptRows({
@@ -44,7 +44,7 @@ export function UsageDialog({
44
44
  {rateLimits.length === 0 ? (
45
45
  <p className="py-6 text-center text-body-sm text-fg-4">
46
46
  {engine === 'claude'
47
- ? 'This session reports no plan windows — API-key sessions have none, and a subscription session reports them once a turn has run.'
47
+ ? 'This session reports no plan windows - API-key sessions have none, and a subscription session reports them once a turn has run.'
48
48
  : `Plan windows are a claude.ai subscription thing; this session runs on the ${engine} engine.`}
49
49
  </p>
50
50
  ) : (
@@ -6,7 +6,7 @@ import { subagentItems, type ToolCallItem } from '../terminal/blocks.ts'
6
6
  // The sub-agent frame machine: which agent frame is on screen, how it is entered (a host
7
7
  // `openSubagent` request or a Task row in the transcript), how it is left (Escape, the strip's
8
8
  // Back, or the host withdrawing), and what the transcript reveals on the way out. The frame
9
- // round-trips through the host's URL — the anti-loop rules live in GOTCHAS ("The sub-agent
9
+ // round-trips through the host's URL - the anti-loop rules live in GOTCHAS ("The sub-agent
10
10
  // frame round-trips through the URL"); here they mean: entry keys on the nonce alone, and the
11
11
  // report is deduped through a ref, so an echo of our own report is inert on arrival.
12
12
  export function useSubagentFrame(options: {
@@ -51,7 +51,7 @@ export function useSubagentFrame(options: {
51
51
  // eslint-disable-next-line react-hooks/exhaustive-deps
52
52
  }, [openSubagentNonce])
53
53
 
54
- // A reveal targets the root transcript, so it closes whatever frame is open — without the
54
+ // A reveal targets the root transcript, so it closes whatever frame is open - without the
55
55
  // return reveal, which would fight the requested one.
56
56
  const revealNonce = reveal?.nonce
57
57
  useEffect(() => {
@@ -7,12 +7,12 @@ import { gapBefore, type TranscriptRow } from './transcript-rows.ts'
7
7
  const AIM_PASSES = 4
8
8
  const AIM_SETTLE_MS = 50
9
9
 
10
- // How long a send's re-pin outlasts the click — long enough to eat a trackpad's momentum tail.
10
+ // How long a send's re-pin outlasts the click - long enough to eat a trackpad's momentum tail.
11
11
  export const REPIN_HOLD_MS = 750
12
12
 
13
13
  type RepinTarget = Pick<ReturnType<typeof useStickToBottomContext>, 'scrollToBottom' | 'state'>
14
14
 
15
- // The send re-pin: a held, escape-proof pin, not one `scrollToBottom('instant')` — a trackpad's
15
+ // The send re-pin: a held, escape-proof pin, not one `scrollToBottom('instant')` - a trackpad's
16
16
  // trailing momentum tick reads as escape intent and aborts the one-shot before its first frame
17
17
  // (GOTCHAS "The send re-pin is a held pin"). Every line is load-bearing: clear the stale escape
18
18
  // flag, hold through the momentum tail, seed the `ignoreEscapes` record the library only
@@ -19,7 +19,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
19
19
  * corresponding node kind encountered during a depth-first walk.
20
20
  */
21
21
  type FragmentVisitor = {
22
- /** A text node — receives its text content (may be empty). */
22
+ /** A text node - receives its text content (may be empty). */
23
23
  onText: (text: string) => void
24
24
  /** A chip element (has `data-chip-trigger`). */
25
25
  onChip: (node: HTMLElement) => void
@@ -191,7 +191,7 @@ export function insertSegmentsAtCursor(currentSegments: Segment[], pastedSegment
191
191
  insertOnce()
192
192
  result.push(seg)
193
193
  } else {
194
- // Cursor falls inside this text segment — split it.
194
+ // Cursor falls inside this text segment - split it.
195
195
  const splitAt = cursorOffset - offset
196
196
  const before = seg.text.slice(0, splitAt)
197
197
  const after = seg.text.slice(splitAt)
@@ -3,12 +3,12 @@
3
3
  *
4
4
  * Invariants:
5
5
  * - All functions are synchronous. Never return a promise or cross a microtask
6
- * boundary after a DOM mutation — that can cause the browser to fire
6
+ * boundary after a DOM mutation - that can cause the browser to fire
7
7
  * `selectionchange` and reset the caret.
8
8
  * - Never cache a `Selection` or `Range` across calls. Ranges become detached
9
9
  * after DOM mutations. Each function calls `window.getSelection()` or
10
10
  * `getSelectionRange()` fresh.
11
- * - Chip nodes are treated atomically via `isChipElement` — we never descend
11
+ * - Chip nodes are treated atomically via `isChipElement` - we never descend
12
12
  * into a contentEditable=false subtree when mapping offsets.
13
13
  */
14
14
  import {
@@ -120,11 +120,11 @@ export function createRangeAtOffset(editor: HTMLElement, targetOffset: number):
120
120
  * Where the caret is on screen, in viewport coordinates.
121
121
  *
122
122
  * A collapsed range usually measures fine, but not when it sits *between* child
123
- * nodes of an element — which is exactly where a freshly inserted newline puts
123
+ * nodes of an element - which is exactly where a freshly inserted newline puts
124
124
  * it, because this editor's content is a flat run of text nodes and `<br>`s
125
125
  * directly under the editor element. There `getBoundingClientRect()` is all
126
126
  * zeros, and the obvious fallback (measure `startContainer`'s element) resolves
127
- * to the editor itself, whose rect is the viewport box — so it reports the
127
+ * to the editor itself, whose rect is the viewport box - so it reports the
128
128
  * caret as trivially visible and scrolls nothing.
129
129
  *
130
130
  * So measure the neighbour instead: the child the caret sits before, or failing
@@ -163,14 +163,14 @@ function caretRect(range: Range): DOMRect | null {
163
163
  * The editor is its own scroll container once it hits `maxHeight`
164
164
  * (`prompt-area.tsx` sets `overflowY: auto` there). Typing scrolls the caret
165
165
  * into view for free, because that is the browser's own behaviour for a native
166
- * edit — but every path that calls `preventDefault()` and rebuilds the DOM
166
+ * edit - but every path that calls `preventDefault()` and rebuilds the DOM
167
167
  * itself (Shift+Enter, list continuation, undo/redo, the bold/italic wrap,
168
168
  * paste) places the caret with a Range instead, and setting a selection does
169
169
  * not scroll anything. The symptom is a prompt that has grown past its cap and
170
170
  * stops following what you are writing: type and it scrolls, press Shift+Enter
171
171
  * and the new line appears below the fold.
172
172
  *
173
- * A no-op when the caret is already visible, so it is safe on every call — it
173
+ * A no-op when the caret is already visible, so it is safe on every call - it
174
174
  * corrects an off-screen caret rather than scrolling to one.
175
175
  */
176
176
  export function scrollCaretIntoView(editor: HTMLElement): void {
@@ -334,7 +334,7 @@ export function findDOMPosition(container: HTMLElement, targetOffset: number): {
334
334
  }
335
335
  remaining -= 1
336
336
  } else if (isHTMLElement(child)) {
337
- // Decoration element (markdown span, URL anchor) — recurse
337
+ // Decoration element (markdown span, URL anchor) - recurse
338
338
  const textLen = (child.textContent ?? '').length
339
339
  if (remaining <= textLen) {
340
340
  const result = findDOMPosition(child, remaining)
@@ -73,7 +73,7 @@ export function isLinkElement(node: Node): node is HTMLAnchorElement {
73
73
  export function safeJsonParse(json: string): unknown {
74
74
  try {
75
75
  // JSON.parse returns `any` by default. We narrow it to `unknown`
76
- // which is the safest pattern — callers must validate before use.
76
+ // which is the safest pattern - callers must validate before use.
77
77
  const parsed: unknown = JSON.parse(json)
78
78
  return parsed
79
79
  } catch {
@@ -208,11 +208,11 @@ export function indexOfChildNode(parent: HTMLElement, child: Node): number {
208
208
  * `readSegmentsFromDOM` in use-prompt-area.ts. This is the single predicate
209
209
  * shared with `domChildIndexToSegmentIndex` below, so a DOM child index can
210
210
  * never map to a different segment index than the one the reader actually
211
- * produces — decoration elements (the URL `<a>` from `decorateURLsInEditor`,
211
+ * produces - decoration elements (the URL `<a>` from `decorateURLsInEditor`,
212
212
  * the markdown `<span data-md>` from `decorateMarkdownInEditor`) fall through
213
213
  * to the reader's "unknown element" branch and DO produce a text segment, so
214
214
  * they must count here too, not just chips/text/`<br>`. A chip element only
215
- * counts if `chipNodeToSegment` would actually accept it — the reader skips a
215
+ * counts if `chipNodeToSegment` would actually accept it - the reader skips a
216
216
  * chip missing a required attribute (trigger/value/display), so this must too.
217
217
  */
218
218
  export function childProducesSegment(child: Node): boolean {
@@ -348,7 +348,7 @@ export function normalizeEditorDOM(editor: HTMLElement): boolean {
348
348
  * Walks direct-child text nodes in the editor and wraps URL text in
349
349
  * `<a>` elements for visual styling and clickability.
350
350
  *
351
- * This is a DOM-only decoration — it does NOT modify the segment model.
351
+ * This is a DOM-only decoration - it does NOT modify the segment model.
352
352
  * The `<a>` elements are stripped by `normalizeEditorDOM` on every input cycle,
353
353
  * so they are re-applied fresh each time.
354
354
  *
@@ -393,7 +393,7 @@ export function decorateURLsInEditor(editor: HTMLElement): boolean {
393
393
  continue
394
394
  }
395
395
 
396
- // Validate URLs upfront – only keep those with safe protocols (CWE-79)
396
+ // Validate URLs upfront - only keep those with safe protocols (CWE-79)
397
397
  const safeMatches: Array<{ url: string; href: string; index: number }> = []
398
398
  for (const { url, index } of matches) {
399
399
  try {
@@ -447,7 +447,7 @@ export function decorateURLsInEditor(editor: HTMLElement): boolean {
447
447
  * Walks direct-child text nodes in the editor and wraps markdown-formatted
448
448
  * text (`**bold**`, `*italic*`, `***bold-italic***`) in styled `<span>` elements.
449
449
  *
450
- * This is a DOM-only decoration — it does NOT modify the segment model.
450
+ * This is a DOM-only decoration - it does NOT modify the segment model.
451
451
  * The `<span>` elements are stripped by `normalizeEditorDOM` on every input cycle,
452
452
  * so they are re-applied fresh each time.
453
453
  *
@@ -529,7 +529,7 @@ export function decorateMarkdownInEditor(editor: HTMLElement): boolean {
529
529
  fragment.appendChild(document.createTextNode(text.slice(lastIndex, index)))
530
530
  }
531
531
 
532
- // Parent span — textContent still returns full match (e.g. "**world**")
532
+ // Parent span - textContent still returns full match (e.g. "**world**")
533
533
  const span = document.createElement('span')
534
534
  span.dataset.md = 'true'
535
535
 
@@ -642,7 +642,7 @@ export function decorateBulletsInEditor(editor: HTMLElement): boolean {
642
642
  * unchanged) and is stripped by {@link normalizeEditorDOM} each input cycle.
643
643
  * Must run BEFORE the node-splitting passes ({@link decorateURLsInEditor},
644
644
  * {@link decorateMarkdownInEditor}, {@link decorateBulletsInEditor}) so every
645
- * direct-child text node is still a whole line — otherwise a mid-line split
645
+ * direct-child text node is still a whole line - otherwise a mid-line split
646
646
  * fragment beginning with whitespace would let the `^` anchor false-match
647
647
  * non-line-leading whitespace.
648
648
  *
@@ -240,7 +240,7 @@ function serializeNode(node: Node, depth: number): string {
240
240
  return `\n\n${serializeList(node, depth)}\n\n`
241
241
  }
242
242
  case 'LI': {
243
- // A stray <li> outside a list wrapper — emit its content as a line.
243
+ // A stray <li> outside a list wrapper - emit its content as a line.
244
244
  return `${serializeChildren(node, depth).trim()}\n`
245
245
  }
246
246
  case 'PRE': {
@@ -284,7 +284,7 @@ function normalizeOutput(markdown: string): string {
284
284
  /**
285
285
  * Converts an HTML string to markdown source text. Returns '' for empty or
286
286
  * body-less input. Block markdown (headings, lists, quotes, fences, tables,
287
- * links) is emitted as literal markdown text — that is the editor's intended
287
+ * links) is emitted as literal markdown text - that is the editor's intended
288
288
  * display; only `*`/`**`/`***` and bare URLs get visually decorated inline.
289
289
  */
290
290
  export function htmlToMarkdown(html: string): string {
@@ -77,7 +77,7 @@ export function truncateSegmentsToLength(segments: Segment[], maxLength: number)
77
77
  * in the editor model: a space, newline, or tab.
78
78
  *
79
79
  * Trigger detection, paste auto-resolution, and position validation all rely
80
- * on the *same* notion of a boundary — keeping it here prevents the three
80
+ * on the *same* notion of a boundary - keeping it here prevents the three
81
81
  * call sites from silently drifting apart (e.g. one handling tabs and the
82
82
  * others not).
83
83
  */
@@ -284,7 +284,7 @@ export function resolveChip(
284
284
  * The sibling of {@link resolveChip}, for suggestions that are a typing aid
285
285
  * rather than a token: what lands in the document is ordinary editable text the
286
286
  * user is expected to finish and change, and it must not look or behave like a
287
- * resolved chip — no immutability, no trigger character, nothing for a consumer
287
+ * resolved chip - no immutability, no trigger character, nothing for a consumer
288
288
  * to parse back out. The caret is left at the end of the inserted text so typing
289
289
  * continues from there.
290
290
  */
@@ -303,7 +303,7 @@ export function resolveText(
303
303
  for (const seg of segments) {
304
304
  if (seg.type === 'chip') {
305
305
  const chipEnd = offset + `${seg.trigger}${seg.displayText}`.length
306
- // A trigger range can never overlap a chip — chips are atomic — so a chip
306
+ // A trigger range can never overlap a chip - chips are atomic - so a chip
307
307
  // is either wholly before or wholly after, and is kept either way.
308
308
  if (chipEnd <= triggerStart || offset >= triggerEnd) {
309
309
  newSegments.push(seg)
@@ -444,7 +444,7 @@ function splitTextByTriggerPatterns(text: string, triggerByChar: Map<string, Tri
444
444
  const query = text.slice(i + 1, end)
445
445
  if (query.length > 0) {
446
446
  // Treat both undefined and '' from onSelect as "no custom label"
447
- // and fall back to the query — an empty displayText would render
447
+ // and fall back to the query - an empty displayText would render
448
448
  // a blank chip.
449
449
  const displayText = trigger.onSelect?.({ value: query, label: query }) || query
450
450
  segments.push({
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * List auto-formatting logic for the PromptArea component.
3
- * Pure — no DOM dependencies, fully testable in Node.
3
+ * Pure - no DOM dependencies, fully testable in Node.
4
4
  */
5
5
  import type { Segment } from './types.ts'
6
6
  import { replaceTextRange, segmentsToPlainText } from './prompt-area-engine.ts'
@@ -26,7 +26,7 @@ export type ListContext = {
26
26
  }
27
27
 
28
28
  /**
29
- * Parsed shape of a single list line — the SINGLE source of truth for what
29
+ * Parsed shape of a single list line - the SINGLE source of truth for what
30
30
  * counts as a list line (both `getListContext` and the renumber engine derive
31
31
  * from this, so the bullet/number regexes live in exactly one place).
32
32
  *
@@ -127,7 +127,7 @@ export function autoFormatListPrefix(segments: Segment[], cursorPos: number): {
127
127
  }
128
128
 
129
129
  /**
130
- * Handles Enter key in a list line — continues the list or exits.
130
+ * Handles Enter key in a list line - continues the list or exits.
131
131
  */
132
132
  export function insertListContinuation(segments: Segment[], cursorPos: number): { segments: Segment[]; cursorOffset: number } | null {
133
133
  const plainText = segmentsToPlainText(segments)
@@ -186,8 +186,8 @@ function getPrevListLineLevel(text: string, lineStart: number): number | null {
186
186
  /**
187
187
  * Indents a list item by one level (adds 2 spaces before the prefix), capped at
188
188
  * one level deeper than the line above. An item can only nest under a preceding
189
- * sibling, so the first item of a list — or an item already one level below its
190
- * parent — cannot indent further (returns null). This keeps sub-items visually
189
+ * sibling, so the first item of a list - or an item already one level below its
190
+ * parent - cannot indent further (returns null). This keeps sub-items visually
191
191
  * connected to a parent instead of drifting arbitrarily deep.
192
192
  */
193
193
  export function indentListItem(segments: Segment[], cursorPos: number): { segments: Segment[]; cursorOffset: number } | null {
@@ -259,8 +259,8 @@ function swapListPrefixLine(line: string, markdownEnabled: boolean): string {
259
259
  /**
260
260
  * Returns the set of line indices that sit inside a *balanced* fenced code
261
261
  * block (a ```…``` pair), so their leading "- "/"• " markers are preserved
262
- * verbatim. An unterminated (unpaired) fence marker is NOT protective — its
263
- * following lines still normalize — so a stray "```" in prose does not silently
262
+ * verbatim. An unterminated (unpaired) fence marker is NOT protective - its
263
+ * following lines still normalize - so a stray "```" in prose does not silently
264
264
  * suppress bullet normalization for the rest of the text.
265
265
  */
266
266
  function fenceProtectedLineIndices(lines: string[]): Set<number> {
@@ -351,12 +351,12 @@ export function normalizeListPrefixes(segments: Segment[], markdownEnabled: bool
351
351
  export type NumberEdit = { oldStart: number; oldEnd: number; newText: string }
352
352
 
353
353
  /**
354
- * Whether the text holds a genuine ordered-list run worth renumbering — a run
354
+ * Whether the text holds a genuine ordered-list run worth renumbering - a run
355
355
  * of 2+ consecutive same-level numbered lines that either starts at 1 or is
356
356
  * already a contiguous `n, n+1, …` sequence. Used to gate the paste path so a
357
357
  * copied list fragment (`3. 4. 5.` → renumber, or a broken `1. 1. 1.`) is
358
358
  * rebuilt, while incidental numeric-leading prose that `parseListLine` would
359
- * otherwise treat as a list — `1985. Born / 2020. Died`, `5. / 10. / 15.` — is
359
+ * otherwise treat as a list - `1985. Born / 2020. Died`, `5. / 10. / 15.` - is
360
360
  * left untouched.
361
361
  */
362
362
  export function hasOrderedListRun(text: string): boolean {
@@ -395,7 +395,7 @@ export function hasOrderedListRun(text: string): boolean {
395
395
  * Recomputes ordered-list numbering across the whole text. Returns the new text
396
396
  * plus the list of changed digit runs (ascending by `oldStart`) for cursor
397
397
  * remapping. When nothing changes, returns the SAME text reference and an empty
398
- * `edits` array — the no-op guard that keeps this off the typing hot path.
398
+ * `edits` array - the no-op guard that keeps this off the typing hot path.
399
399
  */
400
400
  export function renumberOrderedListLines(text: string): { text: string; edits: NumberEdit[] } {
401
401
  // Cheap pre-gate: with no ordered-list line there is nothing to renumber, so
@@ -277,7 +277,7 @@ export function PromptArea({
277
277
  ) : null
278
278
 
279
279
  // Typography (font-size/line-height) lives on the container, not the editor, so
280
- // it cascades to the editor AND the placeholder overlays — and a consumer can
280
+ // it cascades to the editor AND the placeholder overlays - and a consumer can
281
281
  // override all three at once via `className` (e.g. `text-base leading-6`).
282
282
  return (
283
283
  <div className={cn('prompt-area-container relative text-sm leading-relaxed', className)}>
@@ -316,7 +316,7 @@ export function PromptArea({
316
316
  onBlur={handleBlurCombined}
317
317
  />
318
318
 
319
- {/* Overflow gradient indicator – visible when auto-grow is collapsed and content is clipped */}
319
+ {/* Overflow gradient indicator - visible when auto-grow is collapsed and content is clipped */}
320
320
  {autoGrow && hasOverflow && !isFocused && (
321
321
  <div
322
322
  aria-hidden="true"
@@ -18,12 +18,12 @@
18
18
 
19
19
  import type { TriggerConfig, TriggerPosition } from './types.ts'
20
20
 
21
- // Shared option type — everything in TriggerConfig except the keys each
21
+ // Shared option type - everything in TriggerConfig except the keys each
22
22
  // factory sets by default.
23
23
 
24
24
  type TriggerPresetOptions = Omit<Partial<TriggerConfig>, 'char' | 'position' | 'mode'>
25
25
 
26
- // @mention — dropdown at any position
26
+ // @mention - dropdown at any position
27
27
 
28
28
  export type MentionTriggerOptions = TriggerPresetOptions & {
29
29
  /** Override the trigger character. Defaults to `'@'`. */
@@ -48,14 +48,14 @@ export function mentionTrigger(opts: MentionTriggerOptions = {}): TriggerConfig
48
48
  }
49
49
  }
50
50
 
51
- // /command — dropdown anywhere (opt into line-start-only with `position`)
51
+ // /command - dropdown anywhere (opt into line-start-only with `position`)
52
52
 
53
53
  export type CommandTriggerOptions = TriggerPresetOptions & {
54
54
  /** Override the trigger character. Defaults to `'/'`. */
55
55
  char?: string
56
56
  /**
57
57
  * Where the command trigger is valid. Defaults to `'any'`, so commands fire
58
- * anywhere a `/` follows whitespace — not just at the start of a line.
58
+ * anywhere a `/` follows whitespace - not just at the start of a line.
59
59
  * Set to `'start'` to restrict the dropdown to the very start of the input
60
60
  * or immediately after a newline (the classic slash-command behavior).
61
61
  */
@@ -83,7 +83,7 @@ export function commandTrigger(opts: CommandTriggerOptions = {}): TriggerConfig
83
83
  }
84
84
  }
85
85
 
86
- // #hashtag — dropdown at any position, auto-resolve on space
86
+ // #hashtag - dropdown at any position, auto-resolve on space
87
87
 
88
88
  export type HashtagTriggerOptions = TriggerPresetOptions & {
89
89
  /** Override the trigger character. Defaults to `'#'`. */
@@ -132,7 +132,7 @@ export function callbackTrigger(opts: CallbackTriggerOptions): TriggerConfig {
132
132
  }
133
133
  }
134
134
 
135
- // Launch trigger — fires onActivate and swallows the character
135
+ // Launch trigger - fires onActivate and swallows the character
136
136
 
137
137
  export type LaunchTriggerOptions = Omit<Partial<TriggerConfig>, 'mode'> & {
138
138
  /** The trigger character. Required. */
@@ -46,7 +46,7 @@ export type TriggerPosition = 'start' | 'any'
46
46
  * - 'dropdown': Shows a popover with suggestions from `onSearch`
47
47
  * - 'callback': Inserts the char, then fires `onActivate` with the typed query
48
48
  * - 'launch': Fires `onActivate` on keydown and SUPPRESSES the char (it never
49
- * enters the editor) — for opening an external surface (dialog, palette) where
49
+ * enters the editor) - for opening an external surface (dialog, palette) where
50
50
  * no in-editor text should appear. Honors `position` like the other modes.
51
51
  */
52
52
  export type TriggerMode = 'dropdown' | 'callback' | 'launch'
@@ -102,7 +102,7 @@ export type TriggerConfig = {
102
102
  * For 'dropdown' mode: opt a suggestion out of becoming a chip.
103
103
  *
104
104
  * Return a string and the trigger's range is replaced with that **plain,
105
- * editable text** — the trigger character included — with the caret left at
105
+ * editable text** - the trigger character included - with the caret left at
106
106
  * its end. Return undefined and the suggestion resolves to a chip as usual,
107
107
  * so one dropdown can mix both kinds.
108
108
  *
@@ -239,7 +239,7 @@ export type PromptAreaProps = {
239
239
  /**
240
240
  * When markdown is on, the editor rewrites typed list markers (`- ` / `* `)
241
241
  * to a `•` bullet glyph in the model. Set to `false` to keep the original
242
- * marker in the value/`onChange` text — needed when a host renders the output
242
+ * marker in the value/`onChange` text - needed when a host renders the output
243
243
  * as real markdown, where `•` is not a valid list marker. Default `true`.
244
244
  */
245
245
  normalizeBullets?: boolean
@@ -271,8 +271,8 @@ export type PromptAreaProps = {
271
271
  * the caret kept where the edit happened. Chips count as their
272
272
  * `trigger + displayText` length.
273
273
  *
274
- * The cap applies to typing only. Paste is not capped — divert it via
275
- * `onRawPaste` if needed — and the imperative `setText` / `appendText` also
274
+ * The cap applies to typing only. Paste is not capped - divert it via
275
+ * `onRawPaste` if needed - and the imperative `setText` / `appendText` also
276
276
  * bypass it, so a programmatic write can exceed the cap until the next
277
277
  * keystroke truncates.
278
278
  */
@@ -316,7 +316,7 @@ export type PromptAreaProps = {
316
316
  onBlur?: (e: React.FocusEvent<HTMLDivElement>) => void
317
317
  /**
318
318
  * Called at the start of a paste, before PromptArea reads the clipboard. Call
319
- * `preventDefault()` to take over the paste completely — e.g. to divert large
319
+ * `preventDefault()` to take over the paste completely - e.g. to divert large
320
320
  * text or non-image files to an upload pipeline. The built-in segment/image
321
321
  * paste handling is skipped when the event's default is prevented.
322
322
  */
@@ -80,7 +80,7 @@ export function useChipEditing({
80
80
 
81
81
  // The chip node currently edited via `reopenOnChipClick`, kept in lockstep with
82
82
  // `editingChip`/`activeTrigger`. Answers "is THIS exact element the open one" by
83
- // reference identity — trigger+value cannot distinguish two chips sharing a value.
83
+ // reference identity - trigger+value cannot distinguish two chips sharing a value.
84
84
  const openChipNode = useRef<HTMLElement | null>(null)
85
85
 
86
86
  // Set by `handleMouseDown` when the mousedown landed on `openChipNode.current`;
@@ -88,7 +88,7 @@ export function useChipEditing({
88
88
  // "toggle closed". A real mousedown on the editor root rather than a
89
89
  // `dismissTrigger` flag: bubbling reaches the root before `document`, where
90
90
  // TriggerPopover's outside-click dismiss listens and would clear `openChipNode`
91
- // first — and it stays scoped to this node, so an unrelated dismiss cannot poison
91
+ // first - and it stays scoped to this node, so an unrelated dismiss cannot poison
92
92
  // a later click on the same chip.
93
93
  const suppressReopenChip = useRef<HTMLElement | null>(null)
94
94
 
@@ -106,7 +106,7 @@ export function useChipEditing({
106
106
 
107
107
  let node: Node | null = target
108
108
  while (node && node !== editor) {
109
- // Check for URL link click — only navigate on Cmd/Ctrl+Click;
109
+ // Check for URL link click - only navigate on Cmd/Ctrl+Click;
110
110
  // plain click just positions the cursor for editing.
111
111
  if (isLinkElement(node)) {
112
112
  if (e.metaKey || e.ctrlKey) {
@@ -137,12 +137,12 @@ export function useChipEditing({
137
137
  if (chip) {
138
138
  // Native chip-click dropdown: reopen this trigger's suggestions
139
139
  // anchored to the chip so the selection can replace it in place.
140
- // Gated on `!disabled` — a disabled composer must not accept edits
140
+ // Gated on `!disabled` - a disabled composer must not accept edits
141
141
  // through any path, including this one.
142
142
  const config = triggers.find((t) => t.char === chip.trigger)
143
143
  // A click on THIS exact chip element while its own dropdown was
144
144
  // open just closed it (see `suppressReopenChip` and
145
- // `handleMouseDown`) — treat that as a toggle-close, not a reopen.
145
+ // `handleMouseDown`) - treat that as a toggle-close, not a reopen.
146
146
  const wasOpenForThisChip = suppressReopenChip.current === node
147
147
  suppressReopenChip.current = null
148
148
  if (!disabled && !wasOpenForThisChip && config?.reopenOnChipClick && config.mode === 'dropdown' && config.onSearch) {
@@ -212,7 +212,7 @@ export function useChipEditing({
212
212
  }
213
213
  const editor = editorRef.current
214
214
  if (editor && !disabled) {
215
- // Re-verify the click-time index still holds the same chip — the
215
+ // Re-verify the click-time index still holds the same chip - the
216
216
  // model may have shifted (external value update, undo/redo) while
217
217
  // the dropdown was open. If it moved, recover ONLY when exactly one
218
218
  // chip in the document now matches trigger+value: with duplicates,
@@ -258,7 +258,7 @@ export function useChipEditing({
258
258
  renderSegmentsToDOM(newSegments)
259
259
 
260
260
  // Same value + display text + data: treat as a no-op confirmation
261
- // rather than a destructive delete+add — onChipDelete is
261
+ // rather than a destructive delete+add - onChipDelete is
262
262
  // documented as firing on backspace/forward-delete, not on
263
263
  // re-confirming the already-selected suggestion.
264
264
  const unchanged =
@@ -271,7 +271,7 @@ export function useChipEditing({
271
271
  }
272
272
 
273
273
  // +1 when a space was inserted, matching resolveChip's own
274
- // "+1 accounts for the trailing space after the chip" placement —
274
+ // "+1 accounts for the trailing space after the chip" placement -
275
275
  // landing exactly at the chip's end would put the caret at the
276
276
  // same bare element boundary the inserted space exists to avoid.
277
277
  const caretOffset = segmentsToPlainText(newSegments.slice(0, segIdx + 1)).length + (insertedSpace ? 1 : 0)
@@ -37,7 +37,7 @@ import { useCallback, useMemo, useState } from 'react'
37
37
  */
38
38
  export type PromptAreaMode = 'markdown' | 'plain'
39
39
 
40
- /** Returns the other mode. Pure — handy for building custom toggles. */
40
+ /** Returns the other mode. Pure - handy for building custom toggles. */
41
41
  export function oppositeMode(mode: PromptAreaMode): PromptAreaMode {
42
42
  return mode === 'markdown' ? 'plain' : 'markdown'
43
43
  }
@@ -47,7 +47,7 @@ export type UseMarkdownModeOptions = {
47
47
  initialMode?: PromptAreaMode
48
48
  /**
49
49
  * Controlled mode. When provided, the hook mirrors this value and never owns
50
- * its own state — drive changes through `onModeChange`.
50
+ * its own state - drive changes through `onModeChange`.
51
51
  */
52
52
  mode?: PromptAreaMode
53
53
  /** Called with the next mode whenever `toggle`/`setMode` change it. */
@@ -57,7 +57,7 @@ export type UseMarkdownModeOptions = {
57
57
  export type MarkdownModeState = {
58
58
  /** The active mode. */
59
59
  mode: PromptAreaMode
60
- /** `true` in markdown mode — spread onto `<PromptArea markdown={markdown} />`. */
60
+ /** `true` in markdown mode - spread onto `<PromptArea markdown={markdown} />`. */
61
61
  markdown: boolean
62
62
  /** `true` in plain-text mode (the inverse of `markdown`). */
63
63
  isPlainText: boolean
@@ -165,9 +165,9 @@ export function usePromptAreaEvents(deps: EventHandlerDeps): PromptAreaEventHand
165
165
  }
166
166
 
167
167
  // When markdown mode is on, prefer the richest clipboard flavor:
168
- // 1. text/markdown — some apps (e.g. Slack) hand out markdown directly,
168
+ // 1. text/markdown - some apps (e.g. Slack) hand out markdown directly,
169
169
  // preserving nested lists that their text/plain flattens.
170
- // 2. text/html — convert web/Notion/Docs/GitHub HTML to markdown.
170
+ // 2. text/html - convert web/Notion/Docs/GitHub HTML to markdown.
171
171
  // Otherwise (markdown off, or neither present) fall back to plain text.
172
172
  let text = ''
173
173
  if (markdownEnabled) {
@@ -180,7 +180,7 @@ export function usePromptAreaEvents(deps: EventHandlerDeps): PromptAreaEventHand
180
180
  } else {
181
181
  const html = e.clipboardData.getData('text/html')
182
182
  // A converter failure (e.g. stack overflow on pathologically deep
183
- // nesting) must not drop the paste — leave text empty so the
183
+ // nesting) must not drop the paste - leave text empty so the
184
184
  // text/plain fallback below still runs.
185
185
  if (html) {
186
186
  try {
@@ -373,7 +373,7 @@ export function usePromptAreaKeydown({
373
373
  }
374
374
 
375
375
  // 1.75 Launch triggers: a trigger with mode 'launch' fires onActivate on
376
- // keydown and suppresses the char so it never enters the editor — for
376
+ // keydown and suppresses the char so it never enters the editor - for
377
377
  // opening an external surface (dialog, palette). The DOM read is gated on
378
378
  // the typed key actually matching a launch char, so it stays off the hot
379
379
  // path. insertChip still inserts a chip at the cursor if the consumer
@@ -406,7 +406,7 @@ export function usePromptAreaKeydown({
406
406
  // 2. Trigger dropdown navigation. Gated on the dropdown actually being
407
407
  // ON SCREEN, which matches TriggerPopover's own render condition
408
408
  // (non-empty suggestions, OR loading/error/emptyMessage) rather than
409
- // just `suggestions.length > 0` — otherwise a popover left open in a
409
+ // just `suggestions.length > 0` - otherwise a popover left open in a
410
410
  // loading/empty state (e.g. right after a chip-click reopen, before its
411
411
  // empty-query search resolves) lets Enter fall through to onSubmit and
412
412
  // Escape fall through to onEscape while still visibly on screen.
@@ -499,7 +499,7 @@ export function usePromptAreaKeydown({
499
499
  }
500
500
 
501
501
  // 3. Enter without Shift (skipping IME): under `submitOnEnter` it submits
502
- // *unconditionally* — the send key must not turn into "another bullet" because
502
+ // *unconditionally* - the send key must not turn into "another bullet" because
503
503
  // of what the line above starts with; list continuation lives on Shift+Enter
504
504
  // (branch 2.8). Without `submitOnEnter`, Enter is the newline key and continues.
505
505
  if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
@@ -35,11 +35,11 @@ export type UsePromptAreaStateOptions = {
35
35
  }
36
36
 
37
37
  export type PromptAreaBind = {
38
- /** Ref to attach to PromptArea — gives access to imperative methods. */
38
+ /** Ref to attach to PromptArea - gives access to imperative methods. */
39
39
  ref: React.RefObject<PromptAreaHandle | null>
40
- /** Current segment array — pass as `value` prop. */
40
+ /** Current segment array - pass as `value` prop. */
41
41
  value: Segment[]
42
- /** Setter — pass as `onChange` prop. */
42
+ /** Setter - pass as `onChange` prop. */
43
43
  onChange: (segments: Segment[]) => void
44
44
  }
45
45
 
@@ -83,7 +83,7 @@ export function usePromptAreaState(options: UsePromptAreaStateOptions = {}): Pro
83
83
 
84
84
  const chips = useMemo(() => value.filter((seg): seg is ChipSegment => seg.type === 'chip'), [value])
85
85
 
86
- // Bind object — safe to spread onto <PromptArea>
86
+ // Bind object - safe to spread onto <PromptArea>
87
87
  const bind = useMemo<PromptAreaBind>(() => ({ ref, value, onChange: setValue }), [value])
88
88
 
89
89
  const clear = useCallback(() => {