@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
@@ -6,12 +6,10 @@
6
6
  * - Immutable chips for resolved mentions/commands
7
7
  * - Configurable trigger behavior (dropdown vs callback)
8
8
  * - Simple inline markdown rendering
9
+ *
10
+ * The document model is an ordered array of {@link TextSegment}/{@link ChipSegment}.
9
11
  */
10
12
 
11
- /**
12
- * A segment of content within the editable text.
13
- * The document model is an ordered array of these segments.
14
- */
15
13
  export type TextSegment = {
16
14
  type: 'text'
17
15
  text: string
@@ -94,10 +92,7 @@ export type TriggerConfig = {
94
92
  * newer search supersedes this one. Pass it to `fetch()` or other async
95
93
  * APIs to cancel in-flight work automatically.
96
94
  */
97
- onSearch?: (
98
- query: string,
99
- options: { signal: AbortSignal },
100
- ) => TriggerSuggestion[] | Promise<TriggerSuggestion[]>
95
+ onSearch?: (query: string, options: { signal: AbortSignal }) => TriggerSuggestion[] | Promise<TriggerSuggestion[]>
101
96
  /**
102
97
  * For 'dropdown' mode: called when a suggestion is selected.
103
98
  * Return the display text for the chip, or void to use `suggestion.label`.
@@ -0,0 +1,305 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useMemo, useRef } from 'react'
4
+ import type { Segment, TriggerConfig, ActiveTrigger, TriggerSuggestion, ChipSegment } from './types.ts'
5
+ import { segmentsToPlainText } from './prompt-area-engine.ts'
6
+ import {
7
+ isChipElement,
8
+ isLinkElement,
9
+ chipNodeToSegment,
10
+ indexOfChildNode,
11
+ domChildIndexToSegmentIndex,
12
+ safeJsonStringify,
13
+ } from './dom-helpers.ts'
14
+ import { setCursorAtOffset } from './cursor-helpers.ts'
15
+
16
+ type UseChipEditingOptions = {
17
+ editorRef: React.RefObject<HTMLDivElement | null>
18
+ triggers: TriggerConfig[]
19
+ disabled: boolean
20
+ onChange: (segments: Segment[]) => void
21
+ renderSegmentsToDOM: (segments: Segment[]) => void
22
+ onChipClick?: (chip: ChipSegment) => void
23
+ onChipAdd?: (chip: ChipSegment) => void
24
+ onChipDelete?: (chip: ChipSegment) => void
25
+ onLinkClick?: (url: string) => void
26
+ setActiveTrigger: React.Dispatch<React.SetStateAction<ActiveTrigger | null>>
27
+ setSelectedSuggestionIndex: React.Dispatch<React.SetStateAction<number>>
28
+ setTriggerRect: React.Dispatch<React.SetStateAction<DOMRect | null>>
29
+ runSearch: (query: string, config: TriggerConfig) => void
30
+ activeTrigger: ActiveTrigger | null
31
+ suggestions: TriggerSuggestion[]
32
+ }
33
+
34
+ type UseChipEditingReturn = {
35
+ handleClick: (e: React.MouseEvent<HTMLDivElement>) => void
36
+ handleMouseDown: (e: React.MouseEvent<HTMLDivElement>) => void
37
+ /** Forget the clicked chip: typing or a dismiss supersedes the chip-click dropdown. */
38
+ clear: () => void
39
+ /**
40
+ * The `reopenOnChipClick` selection path: when a chip-click dropdown is open, replace the
41
+ * clicked chip in place and return true (the caller dismisses and refocuses); return false
42
+ * when no chip edit is in flight so the caller resolves typed trigger text as usual.
43
+ * `pushUndo` arrives per call because the undo stack lives in usePromptAreaEvents, which is
44
+ * wired up after this hook.
45
+ */
46
+ trySelectEditingChip: (
47
+ segments: Segment[],
48
+ chipData: Omit<ChipSegment, 'type' | 'trigger'>,
49
+ triggerChar: string,
50
+ pushUndo: (segments: Segment[]) => void,
51
+ ) => boolean
52
+ }
53
+
54
+ // The chip-click editing path (`reopenOnChipClick`), moved whole from use-prompt-area.ts:
55
+ // the three refs and their anti-loop choreography, the mousedown/click delegation, and the
56
+ // re-verify-on-select recovery.
57
+ export function useChipEditing({
58
+ editorRef,
59
+ triggers,
60
+ disabled,
61
+ onChange,
62
+ renderSegmentsToDOM,
63
+ onChipClick,
64
+ onChipAdd,
65
+ onChipDelete,
66
+ onLinkClick,
67
+ setActiveTrigger,
68
+ setSelectedSuggestionIndex,
69
+ setTriggerRect,
70
+ runSearch,
71
+ activeTrigger,
72
+ suggestions,
73
+ }: UseChipEditingOptions): UseChipEditingReturn {
74
+ // Chip whose dropdown was reopened via `reopenOnChipClick`: while set, the active
75
+ // dropdown edits this chip in place instead of resolving typed text. `segIndex` is
76
+ // the segment index at CLICK time, not the DOM node, which can detach if
77
+ // `renderSegmentsToDOM` re-renders while the dropdown is open; selection re-verifies
78
+ // it and falls back to a trigger+value search if the model shifted underneath.
79
+ const editingChip = useRef<{ chip: ChipSegment; segIndex: number } | null>(null)
80
+
81
+ // The chip node currently edited via `reopenOnChipClick`, kept in lockstep with
82
+ // `editingChip`/`activeTrigger`. Answers "is THIS exact element the open one" by
83
+ // reference identity — trigger+value cannot distinguish two chips sharing a value.
84
+ const openChipNode = useRef<HTMLElement | null>(null)
85
+
86
+ // Set by `handleMouseDown` when the mousedown landed on `openChipNode.current`;
87
+ // read and cleared by the following `handleClick` to tell "reopen" from
88
+ // "toggle closed". A real mousedown on the editor root rather than a
89
+ // `dismissTrigger` flag: bubbling reaches the root before `document`, where
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
92
+ // a later click on the same chip.
93
+ const suppressReopenChip = useRef<HTMLElement | null>(null)
94
+
95
+ const handleClick = useCallback(
96
+ (e: React.MouseEvent<HTMLDivElement>) => {
97
+ const target = e.target
98
+ if (!(target instanceof Node)) {
99
+ return
100
+ }
101
+
102
+ const editor = editorRef.current
103
+ if (!editor) {
104
+ return
105
+ }
106
+
107
+ let node: Node | null = target
108
+ while (node && node !== editor) {
109
+ // Check for URL link click — only navigate on Cmd/Ctrl+Click;
110
+ // plain click just positions the cursor for editing.
111
+ if (isLinkElement(node)) {
112
+ if (e.metaKey || e.ctrlKey) {
113
+ e.preventDefault()
114
+ onLinkClick?.(node.href)
115
+ window.open(node.href, '_blank', 'noopener,noreferrer')
116
+ return
117
+ }
118
+ // Plain click: let the browser place the cursor inside the link text
119
+ break
120
+ }
121
+
122
+ if (isChipElement(node)) {
123
+ // Spawn ripple effect. `isChipElement` has already narrowed `node`
124
+ // to HTMLElement, so no cast is needed.
125
+ const rect = node.getBoundingClientRect()
126
+ const ripple = document.createElement('span')
127
+ ripple.className = 'prompt-area-chip-ripple'
128
+ const size = Math.max(rect.width, rect.height)
129
+ ripple.style.width = `${size}px`
130
+ ripple.style.height = `${size}px`
131
+ ripple.style.left = `${e.clientX - rect.left - size / 2}px`
132
+ ripple.style.top = `${e.clientY - rect.top - size / 2}px`
133
+ node.appendChild(ripple)
134
+ ripple.addEventListener('animationend', () => ripple.remove())
135
+
136
+ const chip = chipNodeToSegment(node)
137
+ if (chip) {
138
+ // Native chip-click dropdown: reopen this trigger's suggestions
139
+ // anchored to the chip so the selection can replace it in place.
140
+ // Gated on `!disabled` — a disabled composer must not accept edits
141
+ // through any path, including this one.
142
+ const config = triggers.find((t) => t.char === chip.trigger)
143
+ // A click on THIS exact chip element while its own dropdown was
144
+ // open just closed it (see `suppressReopenChip` and
145
+ // `handleMouseDown`) — treat that as a toggle-close, not a reopen.
146
+ const wasOpenForThisChip = suppressReopenChip.current === node
147
+ suppressReopenChip.current = null
148
+ if (!disabled && !wasOpenForThisChip && config?.reopenOnChipClick && config.mode === 'dropdown' && config.onSearch) {
149
+ const childIdx = indexOfChildNode(editor, node)
150
+ editingChip.current = {
151
+ chip,
152
+ segIndex: domChildIndexToSegmentIndex(editor, childIdx),
153
+ }
154
+ openChipNode.current = node
155
+ setActiveTrigger({ config, startOffset: 0, query: '' })
156
+ setSelectedSuggestionIndex(0)
157
+ setTriggerRect(rect)
158
+ runSearch('', config)
159
+ }
160
+ onChipClick?.(chip)
161
+ }
162
+ return
163
+ }
164
+ node = node.parentNode
165
+ }
166
+ },
167
+ [onChipClick, onLinkClick, triggers, runSearch, disabled],
168
+ )
169
+
170
+ // Feeds `suppressReopenChip` for handleClick's toggle-close detection.
171
+
172
+ const handleMouseDown = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
173
+ const target = e.target
174
+ const editor = editorRef.current
175
+ if (!editor || !(target instanceof Node)) {
176
+ suppressReopenChip.current = null
177
+ return
178
+ }
179
+
180
+ let node: Node | null = target
181
+ while (node && node !== editor) {
182
+ if (isChipElement(node)) {
183
+ suppressReopenChip.current = openChipNode.current === node ? node : null
184
+ return
185
+ }
186
+ node = node.parentNode
187
+ }
188
+ suppressReopenChip.current = null
189
+ }, [])
190
+
191
+ // Select a suggestion from the dropdown
192
+
193
+ const clear = useCallback(() => {
194
+ editingChip.current = null
195
+ openChipNode.current = null
196
+ }, [])
197
+
198
+ // Chip-click dropdown (`reopenOnChipClick`): replace the clicked chip in
199
+ // place instead of resolving typed trigger text at the caret. Disabled
200
+ // is re-checked here (not just at open time) in case the composer
201
+ // became disabled while the popover was still open.
202
+ const trySelectEditingChip = useCallback(
203
+ (
204
+ segments: Segment[],
205
+ chipData: Omit<ChipSegment, 'type' | 'trigger'>,
206
+ triggerChar: string,
207
+ pushUndo: (segments: Segment[]) => void,
208
+ ): boolean => {
209
+ const editing = editingChip.current
210
+ if (!editing) {
211
+ return false
212
+ }
213
+ const editor = editorRef.current
214
+ if (editor && !disabled) {
215
+ // Re-verify the click-time index still holds the same chip — the
216
+ // model may have shifted (external value update, undo/redo) while
217
+ // the dropdown was open. If it moved, recover ONLY when exactly one
218
+ // chip in the document now matches trigger+value: with duplicates,
219
+ // guessing risks silently editing the wrong instance, which is
220
+ // worse than the no-op this falls back to.
221
+ const atIndex = segments[editing.segIndex]
222
+ const stillThere = atIndex?.type === 'chip' && atIndex.trigger === editing.chip.trigger && atIndex.value === editing.chip.value
223
+ const segIdx = stillThere
224
+ ? editing.segIndex
225
+ : (() => {
226
+ const matches: number[] = []
227
+ segments.forEach((seg, i) => {
228
+ if (seg.type === 'chip' && seg.trigger === editing.chip.trigger && seg.value === editing.chip.value) {
229
+ matches.push(i)
230
+ }
231
+ })
232
+ return matches.length === 1 ? matches[0] : -1
233
+ })()
234
+ const oldChip = segIdx !== -1 ? segments[segIdx] : undefined
235
+
236
+ if (oldChip?.type === 'chip') {
237
+ const newChip: ChipSegment = {
238
+ type: 'chip',
239
+ trigger: triggerChar,
240
+ ...chipData,
241
+ }
242
+ let newSegments = segments.map((seg, i) => (i === segIdx ? newChip : seg))
243
+
244
+ // Guarantee a real landing spot after the replaced chip, mirroring
245
+ // resolveChip's trailing-space convention (prompt-area-engine.ts):
246
+ // if the new chip is now the last segment (or directly followed by
247
+ // another chip), the caret would land at a bare element boundary
248
+ // with no text node, which some engines fail to render/snap a
249
+ // visible caret at.
250
+ const nextSeg = newSegments[segIdx + 1]
251
+ const insertedSpace = !nextSeg || nextSeg.type !== 'text' || nextSeg.text.length === 0
252
+ if (insertedSpace) {
253
+ newSegments = [...newSegments.slice(0, segIdx + 1), { type: 'text', text: ' ' }, ...newSegments.slice(segIdx + 1)]
254
+ }
255
+
256
+ pushUndo(segments)
257
+ onChange(newSegments)
258
+ renderSegmentsToDOM(newSegments)
259
+
260
+ // Same value + display text + data: treat as a no-op confirmation
261
+ // rather than a destructive delete+add — onChipDelete is
262
+ // documented as firing on backspace/forward-delete, not on
263
+ // re-confirming the already-selected suggestion.
264
+ const unchanged =
265
+ oldChip.value === newChip.value &&
266
+ oldChip.displayText === newChip.displayText &&
267
+ safeJsonStringify(oldChip.data) === safeJsonStringify(newChip.data)
268
+ if (!unchanged) {
269
+ onChipDelete?.(oldChip)
270
+ onChipAdd?.(newChip)
271
+ }
272
+
273
+ // +1 when a space was inserted, matching resolveChip's own
274
+ // "+1 accounts for the trailing space after the chip" placement —
275
+ // landing exactly at the chip's end would put the caret at the
276
+ // same bare element boundary the inserted space exists to avoid.
277
+ const caretOffset = segmentsToPlainText(newSegments.slice(0, segIdx + 1)).length + (insertedSpace ? 1 : 0)
278
+ setCursorAtOffset(editor, caretOffset)
279
+ }
280
+ }
281
+ return true
282
+ },
283
+ [disabled, onChange, renderSegmentsToDOM, onChipAdd, onChipDelete],
284
+ )
285
+
286
+ // Chip-click dropdown: once the empty-query suggestions arrive, preselect
287
+ // the chip's current value so the list opens "on" the existing choice.
288
+ useEffect(() => {
289
+ const editing = editingChip.current
290
+ if (!editing || !activeTrigger?.config.reopenOnChipClick) {
291
+ return
292
+ }
293
+ const idx = suggestions.findIndex((s) => s.value === editing.chip.value)
294
+ if (idx > 0) {
295
+ setSelectedSuggestionIndex(idx)
296
+ }
297
+ }, [suggestions, activeTrigger])
298
+
299
+ // Memoized so consumers can put the hook's result in dependency arrays without
300
+ // re-creating their callbacks every render.
301
+ return useMemo(
302
+ () => ({ handleClick, handleMouseDown, clear, trySelectEditingChip }),
303
+ [handleClick, handleMouseDown, clear, trySelectEditingChip],
304
+ )
305
+ }
@@ -30,10 +30,6 @@
30
30
 
31
31
  import { useCallback, useMemo, useState } from 'react'
32
32
 
33
- // ---------------------------------------------------------------------------
34
- // Mode
35
- // ---------------------------------------------------------------------------
36
-
37
33
  /**
38
34
  * The two rendering variants of a PromptArea.
39
35
  * - `'markdown'`: inline markdown is decorated and list markers normalize to `•`.
@@ -46,10 +42,6 @@ export function oppositeMode(mode: PromptAreaMode): PromptAreaMode {
46
42
  return mode === 'markdown' ? 'plain' : 'markdown'
47
43
  }
48
44
 
49
- // ---------------------------------------------------------------------------
50
- // Options / return type
51
- // ---------------------------------------------------------------------------
52
-
53
45
  export type UseMarkdownModeOptions = {
54
46
  /** Starting mode for the uncontrolled hook. Defaults to `'markdown'`. */
55
47
  initialMode?: PromptAreaMode
@@ -75,24 +67,22 @@ export type MarkdownModeState = {
75
67
  toggle: () => void
76
68
  }
77
69
 
78
- // ---------------------------------------------------------------------------
79
- // Hook
80
- // ---------------------------------------------------------------------------
81
-
82
70
  export function useMarkdownMode(options: UseMarkdownModeOptions = {}): MarkdownModeState {
83
71
  const { initialMode = 'markdown', mode: controlledMode, onModeChange } = options
84
72
  const isControlled = controlledMode !== undefined
85
73
 
86
- const [internalMode, setInternalMode] = useState<PromptAreaMode>(
87
- () => controlledMode ?? initialMode,
88
- )
74
+ const [internalMode, setInternalMode] = useState<PromptAreaMode>(() => controlledMode ?? initialMode)
89
75
 
90
76
  const mode = isControlled ? controlledMode : internalMode
91
77
 
92
78
  const setMode = useCallback(
93
79
  (next: PromptAreaMode) => {
94
- if (next === mode) return
95
- if (!isControlled) setInternalMode(next)
80
+ if (next === mode) {
81
+ return
82
+ }
83
+ if (!isControlled) {
84
+ setInternalMode(next)
85
+ }
96
86
  onModeChange?.(next)
97
87
  },
98
88
  [mode, isControlled, onModeChange],