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