@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.
- package/build/{SessionPanel-DUt2VzXG.mjs → SessionPanel-I5QSLLb0.mjs} +2266 -3459
- package/build/SessionPanel-I5QSLLb0.mjs.map +1 -0
- package/build/SessionPanel-yNO60Tzw.d.mts +151 -0
- package/build/format-C_lpFq0l.d.mts +16 -0
- package/build/format.d.mts +8 -76
- package/build/format.mjs +2 -2
- package/build/index.d.mts +291 -1695
- package/build/index.mjs +6 -301
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +32 -23
- package/build/{status-BE-zg88x.mjs → status-Dm9INVTt.mjs} +17 -88
- package/build/status-Dm9INVTt.mjs.map +1 -0
- package/build/workspace.d.mts +8 -187
- package/build/workspace.mjs +13 -144
- package/build/workspace.mjs.map +1 -1
- package/package.json +24 -24
- package/src/components/agent/CodeEditor.tsx +31 -103
- package/src/components/agent/Composer.tsx +114 -333
- package/src/components/agent/ContextDialog.tsx +20 -41
- package/src/components/agent/ContextRing.tsx +6 -28
- package/src/components/agent/Conversation.tsx +21 -33
- package/src/components/agent/EditorTabs.tsx +39 -67
- package/src/components/agent/EngineIcon.tsx +29 -72
- package/src/components/agent/EntryIcon.tsx +16 -0
- package/src/components/agent/FileCard.tsx +10 -16
- package/src/components/agent/FileTree.tsx +52 -125
- package/src/components/agent/FileViewer.tsx +30 -60
- package/src/components/agent/HostFilesDialog.tsx +40 -69
- package/src/components/agent/Loader.tsx +3 -24
- package/src/components/agent/McpDialog.tsx +64 -137
- package/src/components/agent/Message.tsx +2 -21
- package/src/components/agent/ModelSelect.tsx +33 -62
- package/src/components/agent/PermissionModeSelect.tsx +21 -79
- package/src/components/agent/PermissionPrompt.tsx +59 -63
- package/src/components/agent/ProjectIcon.tsx +228 -93
- package/src/components/agent/PromptTokenText.tsx +10 -16
- package/src/components/agent/QuestionPrompt.tsx +43 -64
- package/src/components/agent/Reasoning.tsx +13 -16
- package/src/components/agent/Response.tsx +3 -9
- package/src/components/agent/Scrubber.tsx +31 -80
- package/src/components/agent/SessionBrowser.tsx +64 -243
- package/src/components/agent/SessionEmptyState.tsx +10 -40
- package/src/components/agent/SessionInfoDialog.tsx +46 -78
- package/src/components/agent/SessionItem.tsx +53 -320
- package/src/components/agent/SessionList.tsx +23 -39
- package/src/components/agent/SessionPanel.tsx +315 -996
- package/src/components/agent/SessionStatusIcon.tsx +14 -28
- package/src/components/agent/SessionSteps.tsx +34 -187
- package/src/components/agent/SessionWorkspace.tsx +104 -219
- package/src/components/agent/SkillsDialog.tsx +35 -80
- package/src/components/agent/StatusBar.tsx +51 -136
- package/src/components/agent/SubagentStrip.tsx +12 -77
- package/src/components/agent/ToolCallCard.tsx +94 -135
- package/src/components/agent/Transcript.tsx +103 -1288
- package/src/components/agent/TranscriptItemView.tsx +155 -0
- package/src/components/agent/TranscriptRows.tsx +418 -0
- package/src/components/agent/UsageDialog.tsx +7 -29
- package/src/components/agent/UsageMeters.tsx +18 -73
- package/src/components/agent/pulse.tsx +9 -32
- package/src/components/agent/scrubber-marks.ts +33 -103
- package/src/components/agent/status.ts +1 -5
- package/src/components/agent/tool-result-fetch.tsx +3 -25
- package/src/components/agent/tool-result-image.tsx +32 -111
- package/src/components/agent/transcript-rows.ts +41 -118
- package/src/components/agent/transcript-variant.tsx +3 -80
- package/src/components/agent/use-height-epoch.ts +10 -24
- package/src/components/agent/use-path-links.ts +45 -63
- package/src/components/agent/use-subagent-frame.ts +106 -0
- package/src/components/agent/use-transcript-jumps.ts +49 -107
- package/src/components/prompt-area/animated-placeholder.tsx +6 -5
- package/src/components/prompt-area/clipboard-helpers.ts +26 -21
- package/src/components/prompt-area/cursor-helpers.ts +79 -37
- package/src/components/prompt-area/dom-helpers.ts +97 -69
- package/src/components/prompt-area/file-strip.tsx +90 -80
- package/src/components/prompt-area/html-to-markdown.ts +67 -46
- package/src/components/prompt-area/image-strip.tsx +8 -18
- package/src/components/prompt-area/index.ts +2 -15
- package/src/components/prompt-area/prompt-area-engine.ts +100 -115
- package/src/components/prompt-area/prompt-area-list-ops.ts +66 -59
- package/src/components/prompt-area/prompt-area.tsx +31 -38
- package/src/components/prompt-area/remove-button.tsx +3 -9
- package/src/components/prompt-area/segment-helpers.ts +4 -12
- package/src/components/prompt-area/trigger-popover.tsx +15 -27
- package/src/components/prompt-area/trigger-presets.ts +0 -10
- package/src/components/prompt-area/types.ts +3 -8
- package/src/components/prompt-area/use-chip-editing.ts +305 -0
- package/src/components/prompt-area/use-markdown-mode.ts +7 -17
- package/src/components/prompt-area/use-prompt-area-events.ts +80 -86
- package/src/components/prompt-area/use-prompt-area-keydown.ts +598 -0
- package/src/components/prompt-area/use-prompt-area-state.ts +5 -23
- package/src/components/prompt-area/use-prompt-area.ts +170 -896
- package/src/components/prompt-area/use-trigger-search.ts +21 -19
- package/src/components/terminal/PermissionPrompt.tsx +56 -59
- package/src/components/terminal/QuestionPrompt.tsx +28 -79
- package/src/components/terminal/StatusLine.tsx +13 -63
- package/src/components/terminal/TerminalTranscript.tsx +44 -160
- package/src/components/terminal/affordances.tsx +68 -98
- package/src/components/terminal/blocks.ts +20 -156
- package/src/components/terminal/diff.tsx +19 -67
- package/src/components/terminal/height.ts +163 -313
- package/src/components/terminal/image-box.ts +0 -44
- package/src/components/terminal/items.tsx +150 -305
- package/src/components/terminal/markdown.tsx +36 -95
- package/src/components/terminal/press.tsx +26 -53
- package/src/components/terminal/prompt.tsx +40 -170
- package/src/components/terminal/result-preview.ts +8 -56
- package/src/components/terminal/row.tsx +11 -98
- package/src/components/terminal/scrubber.tsx +108 -401
- package/src/components/terminal/surface.tsx +6 -54
- package/src/components/terminal/todos.ts +66 -0
- package/src/components/terminal/tool-run.ts +21 -192
- package/src/components/ui/AlertDialog.tsx +8 -23
- package/src/components/ui/Badge.tsx +18 -29
- package/src/components/ui/Button.tsx +7 -15
- package/src/components/ui/Card.tsx +2 -13
- package/src/components/ui/CodeBlock.tsx +10 -37
- package/src/components/ui/CopyButton.tsx +10 -23
- package/src/components/ui/Dialog.tsx +23 -36
- package/src/components/ui/Empty.tsx +6 -29
- package/src/components/ui/Input.tsx +14 -16
- package/src/components/ui/Menu.tsx +6 -18
- package/src/components/ui/PortalScope.tsx +1 -21
- package/src/components/ui/ProgressRing.tsx +8 -19
- package/src/components/ui/Select.tsx +18 -31
- package/src/components/ui/Sonner.tsx +1 -2
- package/src/components/ui/Spinner.tsx +1 -1
- package/src/components/ui/Splitter.tsx +30 -64
- package/src/components/ui/Textarea.tsx +2 -5
- package/src/components/ui/Tooltip.tsx +3 -13
- package/src/format.ts +0 -9
- package/src/index.ts +16 -113
- package/src/lib/clipboard.ts +8 -28
- package/src/lib/css.ts +8 -0
- package/src/lib/format.ts +101 -86
- package/src/lib/plan-request.ts +16 -0
- package/src/lib/status.ts +45 -95
- package/src/lib/tool-icon.ts +39 -47
- package/src/lib/utils.ts +1 -3
- package/src/styles/terminal.css +81 -49
- package/src/styles/theme.css +96 -32
- package/src/workspace.ts +1 -24
- package/build/SessionPanel-BobynUo4.d.mts +0 -669
- package/build/SessionPanel-DUt2VzXG.mjs.map +0 -1
- package/build/format-ljc3lKpA.d.mts +0 -59
- package/build/status-BE-zg88x.mjs.map +0 -1
|
@@ -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)
|
|
95
|
-
|
|
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],
|