@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
|
@@ -4,9 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { Segment, ChipSegment, TriggerConfig, TriggerPosition, ActiveTrigger } from './types.ts'
|
|
6
6
|
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
7
|
// Serialization
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
8
|
|
|
11
9
|
/**
|
|
12
10
|
* Converts an array of segments to a plain text string.
|
|
@@ -15,7 +13,9 @@ import type { Segment, ChipSegment, TriggerConfig, TriggerPosition, ActiveTrigge
|
|
|
15
13
|
export function segmentsToPlainText(segments: Segment[]): string {
|
|
16
14
|
return segments
|
|
17
15
|
.map((seg) => {
|
|
18
|
-
if (seg.type === 'text')
|
|
16
|
+
if (seg.type === 'text') {
|
|
17
|
+
return seg.text
|
|
18
|
+
}
|
|
19
19
|
return `${seg.trigger}${seg.displayText}`
|
|
20
20
|
})
|
|
21
21
|
.join('')
|
|
@@ -26,7 +26,9 @@ export function segmentsToPlainText(segments: Segment[]): string {
|
|
|
26
26
|
* Used for initial value conversion from plain strings.
|
|
27
27
|
*/
|
|
28
28
|
export function plainTextToSegments(text: string): Segment[] {
|
|
29
|
-
if (!text)
|
|
29
|
+
if (!text) {
|
|
30
|
+
return []
|
|
31
|
+
}
|
|
30
32
|
return [{ type: 'text', text }]
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -37,12 +39,13 @@ export function plainTextToSegments(text: string): Segment[] {
|
|
|
37
39
|
* can't be partially represented).
|
|
38
40
|
*/
|
|
39
41
|
export function truncateSegmentsToLength(segments: Segment[], maxLength: number): Segment[] {
|
|
40
|
-
if (maxLength <= 0)
|
|
42
|
+
if (maxLength <= 0) {
|
|
43
|
+
return []
|
|
44
|
+
}
|
|
41
45
|
const result: Segment[] = []
|
|
42
46
|
let length = 0
|
|
43
47
|
for (const seg of segments) {
|
|
44
|
-
const segLength =
|
|
45
|
-
seg.type === 'text' ? seg.text.length : seg.trigger.length + seg.displayText.length
|
|
48
|
+
const segLength = seg.type === 'text' ? seg.text.length : seg.trigger.length + seg.displayText.length
|
|
46
49
|
if (length + segLength <= maxLength) {
|
|
47
50
|
result.push(seg)
|
|
48
51
|
length += segLength
|
|
@@ -54,8 +57,12 @@ export function truncateSegmentsToLength(segments: Segment[], maxLength: number)
|
|
|
54
57
|
// Don't split a surrogate pair: if the cut lands right after a high
|
|
55
58
|
// surrogate, drop the incomplete code point instead of a lone surrogate.
|
|
56
59
|
const code = seg.text.charCodeAt(remaining - 1)
|
|
57
|
-
if (code >= 0xd800 && code <= 0xdbff)
|
|
58
|
-
|
|
60
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
61
|
+
remaining -= 1
|
|
62
|
+
}
|
|
63
|
+
if (remaining > 0) {
|
|
64
|
+
result.push({ type: 'text', text: seg.text.slice(0, remaining) })
|
|
65
|
+
}
|
|
59
66
|
}
|
|
60
67
|
}
|
|
61
68
|
break
|
|
@@ -63,9 +70,7 @@ export function truncateSegmentsToLength(segments: Segment[], maxLength: number)
|
|
|
63
70
|
return result
|
|
64
71
|
}
|
|
65
72
|
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
73
|
// Whitespace / word boundaries
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
74
|
|
|
70
75
|
/**
|
|
71
76
|
* Single source of truth for what counts as an inline-whitespace word boundary
|
|
@@ -88,14 +93,14 @@ export function isInlineWhitespace(char: string | undefined): boolean {
|
|
|
88
93
|
function buildTriggerCharMap(triggers: TriggerConfig[]): Map<string, TriggerConfig> {
|
|
89
94
|
const map = new Map<string, TriggerConfig>()
|
|
90
95
|
for (const trigger of triggers) {
|
|
91
|
-
if (!map.has(trigger.char))
|
|
96
|
+
if (!map.has(trigger.char)) {
|
|
97
|
+
map.set(trigger.char, trigger)
|
|
98
|
+
}
|
|
92
99
|
}
|
|
93
100
|
return map
|
|
94
101
|
}
|
|
95
102
|
|
|
96
|
-
// ---------------------------------------------------------------------------
|
|
97
103
|
// Trigger position validation
|
|
98
|
-
// ---------------------------------------------------------------------------
|
|
99
104
|
|
|
100
105
|
/**
|
|
101
106
|
* Checks whether a trigger character at the given position in text
|
|
@@ -105,12 +110,10 @@ function buildTriggerCharMap(triggers: TriggerConfig[]): Map<string, TriggerConf
|
|
|
105
110
|
* @param charIndex - The index of the trigger character in the text
|
|
106
111
|
* @param position - The position rule to validate against
|
|
107
112
|
*/
|
|
108
|
-
export function isValidTriggerPosition(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
): boolean {
|
|
113
|
-
if (charIndex === 0) return true
|
|
113
|
+
export function isValidTriggerPosition(text: string, charIndex: number, position: TriggerPosition): boolean {
|
|
114
|
+
if (charIndex === 0) {
|
|
115
|
+
return true
|
|
116
|
+
}
|
|
114
117
|
|
|
115
118
|
const prevChar = text[charIndex - 1]
|
|
116
119
|
|
|
@@ -122,9 +125,7 @@ export function isValidTriggerPosition(
|
|
|
122
125
|
return isInlineWhitespace(prevChar)
|
|
123
126
|
}
|
|
124
127
|
|
|
125
|
-
// ---------------------------------------------------------------------------
|
|
126
128
|
// Trigger detection
|
|
127
|
-
// ---------------------------------------------------------------------------
|
|
128
129
|
|
|
129
130
|
/**
|
|
130
131
|
* Scans backwards from the cursor position to detect if the user is
|
|
@@ -136,12 +137,10 @@ export function isValidTriggerPosition(
|
|
|
136
137
|
* @param cursorPos - The cursor position (character offset from start)
|
|
137
138
|
* @param triggers - Available trigger configurations
|
|
138
139
|
*/
|
|
139
|
-
export function detectActiveTrigger(
|
|
140
|
-
text
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
): ActiveTrigger | null {
|
|
144
|
-
if (!text || cursorPos === 0 || triggers.length === 0) return null
|
|
140
|
+
export function detectActiveTrigger(text: string, cursorPos: number, triggers: TriggerConfig[]): ActiveTrigger | null {
|
|
141
|
+
if (!text || cursorPos === 0 || triggers.length === 0) {
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
145
144
|
|
|
146
145
|
const triggerByChar = buildTriggerCharMap(triggers)
|
|
147
146
|
|
|
@@ -169,7 +168,6 @@ export function detectActiveTrigger(
|
|
|
169
168
|
return null
|
|
170
169
|
}
|
|
171
170
|
|
|
172
|
-
// Check if this character is a trigger character
|
|
173
171
|
const matchingTrigger = triggerByChar.get(char)
|
|
174
172
|
if (matchingTrigger && isValidTriggerPosition(text, i, matchingTrigger.position)) {
|
|
175
173
|
return {
|
|
@@ -183,9 +181,7 @@ export function detectActiveTrigger(
|
|
|
183
181
|
return null
|
|
184
182
|
}
|
|
185
183
|
|
|
186
|
-
// ---------------------------------------------------------------------------
|
|
187
184
|
// Chip resolution
|
|
188
|
-
// ---------------------------------------------------------------------------
|
|
189
185
|
|
|
190
186
|
/**
|
|
191
187
|
* Resolves an active trigger into a chip within the segments array.
|
|
@@ -204,7 +200,6 @@ export function resolveChip(
|
|
|
204
200
|
const triggerStart = activeTrigger.startOffset
|
|
205
201
|
const triggerEnd = triggerStart + 1 + activeTrigger.query.length // +1 for trigger char
|
|
206
202
|
|
|
207
|
-
// Build the new segments by mapping plain text positions back to segment boundaries
|
|
208
203
|
const newSegments: Segment[] = []
|
|
209
204
|
let offset = 0
|
|
210
205
|
|
|
@@ -261,7 +256,6 @@ export function resolveChip(
|
|
|
261
256
|
}
|
|
262
257
|
}
|
|
263
258
|
|
|
264
|
-
// Merge adjacent text segments
|
|
265
259
|
const merged = mergeAdjacentTextSegments(newSegments)
|
|
266
260
|
|
|
267
261
|
// Cursor should be placed after the chip + trailing space.
|
|
@@ -273,11 +267,7 @@ export function resolveChip(
|
|
|
273
267
|
runningOffset += seg.text.length
|
|
274
268
|
} else {
|
|
275
269
|
runningOffset += seg.trigger.length + seg.displayText.length
|
|
276
|
-
if (
|
|
277
|
-
seg.value === chip.value &&
|
|
278
|
-
seg.displayText === chip.displayText &&
|
|
279
|
-
seg.trigger === activeTrigger.config.char
|
|
280
|
-
) {
|
|
270
|
+
if (seg.value === chip.value && seg.displayText === chip.displayText && seg.trigger === activeTrigger.config.char) {
|
|
281
271
|
lastChipEndOffset = runningOffset
|
|
282
272
|
}
|
|
283
273
|
}
|
|
@@ -315,7 +305,9 @@ export function resolveText(
|
|
|
315
305
|
const chipEnd = offset + `${seg.trigger}${seg.displayText}`.length
|
|
316
306
|
// A trigger range can never overlap a chip — chips are atomic — so a chip
|
|
317
307
|
// is either wholly before or wholly after, and is kept either way.
|
|
318
|
-
if (chipEnd <= triggerStart || offset >= triggerEnd)
|
|
308
|
+
if (chipEnd <= triggerStart || offset >= triggerEnd) {
|
|
309
|
+
newSegments.push(seg)
|
|
310
|
+
}
|
|
319
311
|
offset = chipEnd
|
|
320
312
|
continue
|
|
321
313
|
}
|
|
@@ -326,9 +318,13 @@ export function resolveText(
|
|
|
326
318
|
} else {
|
|
327
319
|
const before = seg.text.slice(0, Math.max(0, triggerStart - textStart))
|
|
328
320
|
const after = seg.text.slice(Math.min(seg.text.length, triggerEnd - textStart))
|
|
329
|
-
if (before)
|
|
321
|
+
if (before) {
|
|
322
|
+
newSegments.push({ type: 'text', text: before })
|
|
323
|
+
}
|
|
330
324
|
newSegments.push({ type: 'text', text })
|
|
331
|
-
if (after)
|
|
325
|
+
if (after) {
|
|
326
|
+
newSegments.push({ type: 'text', text: after })
|
|
327
|
+
}
|
|
332
328
|
}
|
|
333
329
|
offset = textEnd
|
|
334
330
|
}
|
|
@@ -337,13 +333,13 @@ export function resolveText(
|
|
|
337
333
|
// Clamp: a trigger detected against a stale document could otherwise leave the
|
|
338
334
|
// caret past the end, which renders as no caret at all.
|
|
339
335
|
const total = segmentsToPlainText(merged).length
|
|
340
|
-
if (cursorOffset > total)
|
|
336
|
+
if (cursorOffset > total) {
|
|
337
|
+
cursorOffset = total
|
|
338
|
+
}
|
|
341
339
|
return { segments: merged, cursorOffset }
|
|
342
340
|
}
|
|
343
341
|
|
|
344
|
-
// ---------------------------------------------------------------------------
|
|
345
342
|
// Chip removal
|
|
346
|
-
// ---------------------------------------------------------------------------
|
|
347
343
|
|
|
348
344
|
/**
|
|
349
345
|
* Removes a chip at the given segment index and merges adjacent text segments.
|
|
@@ -353,8 +349,12 @@ export function resolveText(
|
|
|
353
349
|
* @returns New segments array with the chip removed
|
|
354
350
|
*/
|
|
355
351
|
export function removeChipAtIndex(segments: Segment[], index: number): Segment[] {
|
|
356
|
-
if (index < 0 || index >= segments.length)
|
|
357
|
-
|
|
352
|
+
if (index < 0 || index >= segments.length) {
|
|
353
|
+
return segments
|
|
354
|
+
}
|
|
355
|
+
if (segments[index].type !== 'chip') {
|
|
356
|
+
return segments
|
|
357
|
+
}
|
|
358
358
|
|
|
359
359
|
const result = [...segments.slice(0, index), ...segments.slice(index + 1)]
|
|
360
360
|
return mergeAdjacentTextSegments(result)
|
|
@@ -368,26 +368,21 @@ export function removeChipAtIndex(segments: Segment[], index: number): Segment[]
|
|
|
368
368
|
* @param index - The segment index to revert
|
|
369
369
|
* @returns New segments with the chip replaced by text, or null if not applicable
|
|
370
370
|
*/
|
|
371
|
-
export function revertChipAtIndex(
|
|
372
|
-
segments
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
if (index < 0 || index >= segments.length) return null
|
|
371
|
+
export function revertChipAtIndex(segments: Segment[], index: number): { segments: Segment[]; revertedText: string } | null {
|
|
372
|
+
if (index < 0 || index >= segments.length) {
|
|
373
|
+
return null
|
|
374
|
+
}
|
|
376
375
|
const seg = segments[index]
|
|
377
|
-
if (seg.type !== 'chip' || !seg.autoResolved)
|
|
376
|
+
if (seg.type !== 'chip' || !seg.autoResolved) {
|
|
377
|
+
return null
|
|
378
|
+
}
|
|
378
379
|
|
|
379
380
|
const revertedText = `${seg.trigger}${seg.displayText}`
|
|
380
|
-
const result = [
|
|
381
|
-
...segments.slice(0, index),
|
|
382
|
-
{ type: 'text' as const, text: revertedText },
|
|
383
|
-
...segments.slice(index + 1),
|
|
384
|
-
]
|
|
381
|
+
const result = [...segments.slice(0, index), { type: 'text' as const, text: revertedText }, ...segments.slice(index + 1)]
|
|
385
382
|
return { segments: mergeAdjacentTextSegments(result), revertedText }
|
|
386
383
|
}
|
|
387
384
|
|
|
388
|
-
// ---------------------------------------------------------------------------
|
|
389
385
|
// Paste: resolve trigger patterns in segments
|
|
390
|
-
// ---------------------------------------------------------------------------
|
|
391
386
|
|
|
392
387
|
/**
|
|
393
388
|
* Scans text segments for trigger patterns and auto-resolves them into chips.
|
|
@@ -397,12 +392,11 @@ export function revertChipAtIndex(
|
|
|
397
392
|
* whitespace, or after a newline. This avoids false positives like email
|
|
398
393
|
* addresses (user@example.com).
|
|
399
394
|
*/
|
|
400
|
-
export function resolveTriggersInSegments(
|
|
401
|
-
segments: Segment[],
|
|
402
|
-
triggers: TriggerConfig[],
|
|
403
|
-
): Segment[] {
|
|
395
|
+
export function resolveTriggersInSegments(segments: Segment[], triggers: TriggerConfig[]): Segment[] {
|
|
404
396
|
const autoResolveTriggers = triggers.filter((t) => t.resolveOnSpace)
|
|
405
|
-
if (autoResolveTriggers.length === 0)
|
|
397
|
+
if (autoResolveTriggers.length === 0) {
|
|
398
|
+
return segments
|
|
399
|
+
}
|
|
406
400
|
|
|
407
401
|
const triggerByChar = buildTriggerCharMap(autoResolveTriggers)
|
|
408
402
|
const result: Segment[] = []
|
|
@@ -425,11 +419,10 @@ export function resolveTriggersInSegments(
|
|
|
425
419
|
* A trigger pattern is: (start-of-string | whitespace) + trigger_char + word_chars
|
|
426
420
|
* followed by whitespace or end-of-string.
|
|
427
421
|
*/
|
|
428
|
-
function splitTextByTriggerPatterns(
|
|
429
|
-
text
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
if (!text) return []
|
|
422
|
+
function splitTextByTriggerPatterns(text: string, triggerByChar: Map<string, TriggerConfig>): Segment[] {
|
|
423
|
+
if (!text) {
|
|
424
|
+
return []
|
|
425
|
+
}
|
|
433
426
|
|
|
434
427
|
const segments: Segment[] = []
|
|
435
428
|
let i = 0
|
|
@@ -479,9 +472,7 @@ function splitTextByTriggerPatterns(
|
|
|
479
472
|
return segments
|
|
480
473
|
}
|
|
481
474
|
|
|
482
|
-
// ---------------------------------------------------------------------------
|
|
483
475
|
// Text range replacement
|
|
484
|
-
// ---------------------------------------------------------------------------
|
|
485
476
|
|
|
486
477
|
/**
|
|
487
478
|
* Replaces a range of plain text within the segments array.
|
|
@@ -493,12 +484,7 @@ function splitTextByTriggerPatterns(
|
|
|
493
484
|
* @param replacement - The replacement text
|
|
494
485
|
* @returns New segments array with the replacement applied
|
|
495
486
|
*/
|
|
496
|
-
export function replaceTextRange(
|
|
497
|
-
segments: Segment[],
|
|
498
|
-
start: number,
|
|
499
|
-
end: number,
|
|
500
|
-
replacement: string,
|
|
501
|
-
): Segment[] {
|
|
487
|
+
export function replaceTextRange(segments: Segment[], start: number, end: number, replacement: string): Segment[] {
|
|
502
488
|
const newSegments: Segment[] = []
|
|
503
489
|
let offset = 0
|
|
504
490
|
let inserted = false
|
|
@@ -518,13 +504,11 @@ export function replaceTextRange(
|
|
|
518
504
|
if (chipEnd <= start || chipStart >= end) {
|
|
519
505
|
newSegments.push(seg)
|
|
520
506
|
}
|
|
521
|
-
// Chips within the range are removed
|
|
522
507
|
offset = chipEnd
|
|
523
508
|
} else {
|
|
524
509
|
const textStart = offset
|
|
525
510
|
const textEnd = offset + seg.text.length
|
|
526
511
|
|
|
527
|
-
// Check if this segment contains the insertion/replacement point
|
|
528
512
|
const isBefore = start === end ? textEnd < start : textEnd <= start
|
|
529
513
|
const isAfter = start === end ? textStart > end : textStart >= end
|
|
530
514
|
|
|
@@ -564,9 +548,7 @@ export function replaceTextRange(
|
|
|
564
548
|
return mergeAdjacentTextSegments(newSegments)
|
|
565
549
|
}
|
|
566
550
|
|
|
567
|
-
// ---------------------------------------------------------------------------
|
|
568
551
|
// Markdown formatting shortcuts
|
|
569
|
-
// ---------------------------------------------------------------------------
|
|
570
552
|
|
|
571
553
|
/**
|
|
572
554
|
* Toggles markdown wrap markers around a selected text range.
|
|
@@ -585,28 +567,24 @@ export function toggleMarkdownWrap(
|
|
|
585
567
|
selectionEnd: number,
|
|
586
568
|
marker: string,
|
|
587
569
|
): { segments: Segment[]; selectionStart: number; selectionEnd: number } | null {
|
|
588
|
-
if (selectionStart === selectionEnd)
|
|
570
|
+
if (selectionStart === selectionEnd) {
|
|
571
|
+
return null
|
|
572
|
+
}
|
|
589
573
|
|
|
590
574
|
const plainText = segmentsToPlainText(segments)
|
|
591
575
|
const markerLen = marker.length
|
|
592
576
|
|
|
593
|
-
|
|
594
|
-
const hasOpeningMarker =
|
|
595
|
-
selectionStart >= markerLen &&
|
|
596
|
-
plainText.slice(selectionStart - markerLen, selectionStart) === marker
|
|
577
|
+
const hasOpeningMarker = selectionStart >= markerLen && plainText.slice(selectionStart - markerLen, selectionStart) === marker
|
|
597
578
|
const hasClosingMarker =
|
|
598
|
-
selectionEnd + markerLen <= plainText.length &&
|
|
599
|
-
plainText.slice(selectionEnd, selectionEnd + markerLen) === marker
|
|
579
|
+
selectionEnd + markerLen <= plainText.length && plainText.slice(selectionEnd, selectionEnd + markerLen) === marker
|
|
600
580
|
|
|
601
581
|
let isWrapped = hasOpeningMarker && hasClosingMarker
|
|
602
582
|
|
|
603
583
|
// For single-char markers (e.g., '*'), ensure we're not matching
|
|
604
584
|
// inside a multi-char marker (e.g., '**')
|
|
605
585
|
if (isWrapped && markerLen === 1) {
|
|
606
|
-
const charBeforeOpening =
|
|
607
|
-
|
|
608
|
-
const charAfterClosing =
|
|
609
|
-
selectionEnd + markerLen < plainText.length ? plainText[selectionEnd + markerLen] : ''
|
|
586
|
+
const charBeforeOpening = selectionStart > markerLen ? plainText[selectionStart - markerLen - 1] : ''
|
|
587
|
+
const charAfterClosing = selectionEnd + markerLen < plainText.length ? plainText[selectionEnd + markerLen] : ''
|
|
610
588
|
if (charBeforeOpening === marker || charAfterClosing === marker) {
|
|
611
589
|
isWrapped = false
|
|
612
590
|
}
|
|
@@ -615,12 +593,7 @@ export function toggleMarkdownWrap(
|
|
|
615
593
|
if (isWrapped) {
|
|
616
594
|
// Unwrap: remove closing marker first (preserves start offsets), then opening
|
|
617
595
|
const afterClosing = replaceTextRange(segments, selectionEnd, selectionEnd + markerLen, '')
|
|
618
|
-
const afterOpening = replaceTextRange(
|
|
619
|
-
afterClosing,
|
|
620
|
-
selectionStart - markerLen,
|
|
621
|
-
selectionStart,
|
|
622
|
-
'',
|
|
623
|
-
)
|
|
596
|
+
const afterOpening = replaceTextRange(afterClosing, selectionStart - markerLen, selectionStart, '')
|
|
624
597
|
return {
|
|
625
598
|
segments: afterOpening,
|
|
626
599
|
selectionStart: selectionStart - markerLen,
|
|
@@ -638,9 +611,7 @@ export function toggleMarkdownWrap(
|
|
|
638
611
|
}
|
|
639
612
|
}
|
|
640
613
|
|
|
641
|
-
// ---------------------------------------------------------------------------
|
|
642
614
|
// Inline markdown parsing
|
|
643
|
-
// ---------------------------------------------------------------------------
|
|
644
615
|
|
|
645
616
|
export type MarkdownToken =
|
|
646
617
|
| { type: 'plain'; text: string }
|
|
@@ -654,7 +625,9 @@ export type MarkdownToken =
|
|
|
654
625
|
* Does NOT handle block-level markdown (lists, headings, etc.).
|
|
655
626
|
*/
|
|
656
627
|
export function parseInlineMarkdown(text: string): MarkdownToken[] {
|
|
657
|
-
if (!text)
|
|
628
|
+
if (!text) {
|
|
629
|
+
return []
|
|
630
|
+
}
|
|
658
631
|
|
|
659
632
|
const tokens: MarkdownToken[] = []
|
|
660
633
|
// Regex patterns for inline markdown elements:
|
|
@@ -668,7 +641,6 @@ export function parseInlineMarkdown(text: string): MarkdownToken[] {
|
|
|
668
641
|
let match: RegExpExecArray | null
|
|
669
642
|
|
|
670
643
|
while ((match = pattern.exec(text)) !== null) {
|
|
671
|
-
// Add any plain text before this match
|
|
672
644
|
if (match.index > lastIndex) {
|
|
673
645
|
tokens.push({ type: 'plain', text: text.slice(lastIndex, match.index) })
|
|
674
646
|
}
|
|
@@ -690,7 +662,6 @@ export function parseInlineMarkdown(text: string): MarkdownToken[] {
|
|
|
690
662
|
lastIndex = match.index + match[0].length
|
|
691
663
|
}
|
|
692
664
|
|
|
693
|
-
// Add any remaining plain text
|
|
694
665
|
if (lastIndex < text.length) {
|
|
695
666
|
tokens.push({ type: 'plain', text: text.slice(lastIndex) })
|
|
696
667
|
}
|
|
@@ -698,9 +669,7 @@ export function parseInlineMarkdown(text: string): MarkdownToken[] {
|
|
|
698
669
|
return tokens
|
|
699
670
|
}
|
|
700
671
|
|
|
701
|
-
// ---------------------------------------------------------------------------
|
|
702
672
|
// Segment comparison
|
|
703
|
-
// ---------------------------------------------------------------------------
|
|
704
673
|
|
|
705
674
|
/**
|
|
706
675
|
* Shallow equality check for two segment arrays.
|
|
@@ -708,15 +677,23 @@ export function parseInlineMarkdown(text: string): MarkdownToken[] {
|
|
|
708
677
|
* Avoids JSON.stringify overhead for the common case.
|
|
709
678
|
*/
|
|
710
679
|
export function segmentsEqual(a: Segment[], b: Segment[]): boolean {
|
|
711
|
-
if (a === b)
|
|
712
|
-
|
|
680
|
+
if (a === b) {
|
|
681
|
+
return true
|
|
682
|
+
}
|
|
683
|
+
if (a.length !== b.length) {
|
|
684
|
+
return false
|
|
685
|
+
}
|
|
713
686
|
|
|
714
687
|
for (let i = 0; i < a.length; i++) {
|
|
715
688
|
const sa = a[i]
|
|
716
689
|
const sb = b[i]
|
|
717
|
-
if (sa.type !== sb.type)
|
|
690
|
+
if (sa.type !== sb.type) {
|
|
691
|
+
return false
|
|
692
|
+
}
|
|
718
693
|
if (sa.type === 'text') {
|
|
719
|
-
if (sb.type !== 'text' || sa.text !== sb.text)
|
|
694
|
+
if (sb.type !== 'text' || sa.text !== sb.text) {
|
|
695
|
+
return false
|
|
696
|
+
}
|
|
720
697
|
} else {
|
|
721
698
|
if (
|
|
722
699
|
sb.type !== 'chip' ||
|
|
@@ -724,16 +701,23 @@ export function segmentsEqual(a: Segment[], b: Segment[]): boolean {
|
|
|
724
701
|
sa.value !== sb.value ||
|
|
725
702
|
sa.displayText !== sb.displayText ||
|
|
726
703
|
sa.autoResolved !== sb.autoResolved
|
|
727
|
-
)
|
|
704
|
+
) {
|
|
728
705
|
return false
|
|
706
|
+
}
|
|
729
707
|
}
|
|
730
708
|
}
|
|
731
709
|
return true
|
|
732
710
|
}
|
|
733
711
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
712
|
+
/**
|
|
713
|
+
* Content equality regardless of how text is chunked into segments: adjacent
|
|
714
|
+
* text segments are merged (and empty ones dropped) on both sides before
|
|
715
|
+
* comparing, so a DOM read split by decorations or per-`<br>` newlines equals
|
|
716
|
+
* the merged model that rendered it.
|
|
717
|
+
*/
|
|
718
|
+
export function segmentsContentEqual(a: Segment[], b: Segment[]): boolean {
|
|
719
|
+
return segmentsEqual(mergeAdjacentTextSegments(a), mergeAdjacentTextSegments(b))
|
|
720
|
+
}
|
|
737
721
|
|
|
738
722
|
/**
|
|
739
723
|
* Merges adjacent text segments into single text segments.
|
|
@@ -743,11 +727,12 @@ export function mergeAdjacentTextSegments(segments: Segment[]): Segment[] {
|
|
|
743
727
|
const result: Segment[] = []
|
|
744
728
|
|
|
745
729
|
for (const seg of segments) {
|
|
746
|
-
if (seg.type === 'text' && seg.text === '')
|
|
730
|
+
if (seg.type === 'text' && seg.text === '') {
|
|
731
|
+
continue
|
|
732
|
+
}
|
|
747
733
|
|
|
748
734
|
const last = result[result.length - 1]
|
|
749
735
|
if (seg.type === 'text' && last?.type === 'text') {
|
|
750
|
-
// Merge with previous text segment
|
|
751
736
|
result[result.length - 1] = { type: 'text', text: last.text + seg.text }
|
|
752
737
|
} else {
|
|
753
738
|
result.push(seg)
|