@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
|
@@ -21,7 +21,8 @@ function Svg({ className, children }: IconProps & { children: React.ReactNode })
|
|
|
21
21
|
strokeLinecap="round"
|
|
22
22
|
strokeLinejoin="round"
|
|
23
23
|
aria-hidden="true"
|
|
24
|
-
className={className}
|
|
24
|
+
className={className}
|
|
25
|
+
>
|
|
25
26
|
{children}
|
|
26
27
|
</svg>
|
|
27
28
|
)
|
|
@@ -34,38 +35,48 @@ const FileBody = (
|
|
|
34
35
|
</>
|
|
35
36
|
)
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
)
|
|
38
|
+
function File({ className }: IconProps) {
|
|
39
|
+
return <Svg className={className}>{FileBody}</Svg>
|
|
40
|
+
}
|
|
41
|
+
function FileText({ className }: IconProps) {
|
|
42
|
+
return (
|
|
43
|
+
<Svg className={className}>
|
|
44
|
+
{FileBody}
|
|
45
|
+
<path d="M10 9H8" />
|
|
46
|
+
<path d="M16 13H8" />
|
|
47
|
+
<path d="M16 17H8" />
|
|
48
|
+
</Svg>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
function FileSpreadsheet({ className }: IconProps) {
|
|
52
|
+
return (
|
|
53
|
+
<Svg className={className}>
|
|
54
|
+
{FileBody}
|
|
55
|
+
<path d="M8 13h2" />
|
|
56
|
+
<path d="M14 13h2" />
|
|
57
|
+
<path d="M8 17h2" />
|
|
58
|
+
<path d="M14 17h2" />
|
|
59
|
+
</Svg>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
function FileCode({ className }: IconProps) {
|
|
63
|
+
return (
|
|
64
|
+
<Svg className={className}>
|
|
65
|
+
{FileBody}
|
|
66
|
+
<path d="M10 12.5 8 15l2 2.5" />
|
|
67
|
+
<path d="m14 12.5 2 2.5-2 2.5" />
|
|
68
|
+
</Svg>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
function ImageIcon({ className }: IconProps) {
|
|
72
|
+
return (
|
|
73
|
+
<Svg className={className}>
|
|
74
|
+
<rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
|
|
75
|
+
<circle cx="9" cy="9" r="2" />
|
|
76
|
+
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
|
|
77
|
+
</Svg>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
69
80
|
|
|
70
81
|
type FileStripProps = {
|
|
71
82
|
files: PromptAreaFile[]
|
|
@@ -79,17 +90,21 @@ const COLLAPSE_THRESHOLD = 3
|
|
|
79
90
|
|
|
80
91
|
/** Pick a lucide icon key based on MIME type. */
|
|
81
92
|
function getFileIconKey(type?: string): 'pdf' | 'spreadsheet' | 'code' | 'image' | 'default' {
|
|
82
|
-
if (!type)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
if (!type) {
|
|
94
|
+
return 'default'
|
|
95
|
+
}
|
|
96
|
+
if (type === 'application/pdf') {
|
|
97
|
+
return 'pdf'
|
|
98
|
+
}
|
|
99
|
+
if (type.includes('spreadsheet') || type === 'text/csv') {
|
|
100
|
+
return 'spreadsheet'
|
|
101
|
+
}
|
|
102
|
+
if (type.startsWith('text/') || type.includes('javascript') || type.includes('json') || type.includes('xml')) {
|
|
91
103
|
return 'code'
|
|
92
|
-
|
|
104
|
+
}
|
|
105
|
+
if (type.startsWith('image/')) {
|
|
106
|
+
return 'image'
|
|
107
|
+
}
|
|
93
108
|
return 'default'
|
|
94
109
|
}
|
|
95
110
|
|
|
@@ -103,16 +118,24 @@ const FILE_ICONS = {
|
|
|
103
118
|
|
|
104
119
|
/** Format bytes into a human-readable string. */
|
|
105
120
|
function formatFileSize(bytes: number): string {
|
|
106
|
-
if (bytes < 1024)
|
|
107
|
-
|
|
108
|
-
|
|
121
|
+
if (bytes < 1024) {
|
|
122
|
+
return `${bytes} B`
|
|
123
|
+
}
|
|
124
|
+
if (bytes < 1024 * 1024) {
|
|
125
|
+
return `${(bytes / 1024).toFixed(1)} KB`
|
|
126
|
+
}
|
|
127
|
+
if (bytes < 1024 * 1024 * 1024) {
|
|
128
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
129
|
+
}
|
|
109
130
|
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`
|
|
110
131
|
}
|
|
111
132
|
|
|
112
133
|
/** Extract a short extension label from a filename (e.g., "PDF", "CSV"). */
|
|
113
134
|
function getExtensionLabel(name: string): string | null {
|
|
114
135
|
const dot = name.lastIndexOf('.')
|
|
115
|
-
if (dot === -1 || dot === name.length - 1)
|
|
136
|
+
if (dot === -1 || dot === name.length - 1) {
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
116
139
|
return name.slice(dot + 1).toUpperCase()
|
|
117
140
|
}
|
|
118
141
|
|
|
@@ -140,19 +163,14 @@ function FileCard({
|
|
|
140
163
|
compact ? 'h-10 w-36 px-2' : 'h-14 w-48 px-3',
|
|
141
164
|
onClick && 'cursor-pointer',
|
|
142
165
|
)}
|
|
143
|
-
onClick={() => onClick?.(file)}
|
|
166
|
+
onClick={() => onClick?.(file)}
|
|
167
|
+
>
|
|
144
168
|
{(() => {
|
|
145
169
|
const Icon = FILE_ICONS[getFileIconKey(file.type)]
|
|
146
|
-
return (
|
|
147
|
-
<Icon
|
|
148
|
-
className={cn('text-muted-foreground flex-shrink-0', compact ? 'h-4 w-4' : 'h-5 w-5')}
|
|
149
|
-
/>
|
|
150
|
-
)
|
|
170
|
+
return <Icon className={cn('text-muted-foreground flex-shrink-0', compact ? 'h-4 w-4' : 'h-5 w-5')} />
|
|
151
171
|
})()}
|
|
152
172
|
<div className="min-w-0 flex-1">
|
|
153
|
-
<div
|
|
154
|
-
className={cn('truncate font-medium', compact ? 'text-xs' : 'text-sm')}
|
|
155
|
-
title={file.name}>
|
|
173
|
+
<div className={cn('truncate font-medium', compact ? 'text-xs' : 'text-sm')} title={file.name}>
|
|
156
174
|
{file.name}
|
|
157
175
|
</div>
|
|
158
176
|
{!compact && meta && <div className="text-muted-foreground truncate text-xs">{meta}</div>}
|
|
@@ -175,15 +193,15 @@ export function FileStrip({ files, onRemove, onClick, className }: FileStripProp
|
|
|
175
193
|
const toggleRef = useRef<HTMLButtonElement>(null)
|
|
176
194
|
|
|
177
195
|
useEffect(() => {
|
|
178
|
-
if (!expanded)
|
|
196
|
+
if (!expanded) {
|
|
197
|
+
return
|
|
198
|
+
}
|
|
179
199
|
const handleClick = (e: MouseEvent) => {
|
|
180
200
|
const target = e.target
|
|
181
|
-
if (!(target instanceof Node))
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
!toggleRef.current?.contains(target)
|
|
186
|
-
) {
|
|
201
|
+
if (!(target instanceof Node)) {
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
if (popoverRef.current && !popoverRef.current.contains(target) && !toggleRef.current?.contains(target)) {
|
|
187
205
|
setExpanded(false)
|
|
188
206
|
}
|
|
189
207
|
}
|
|
@@ -191,7 +209,9 @@ export function FileStrip({ files, onRemove, onClick, className }: FileStripProp
|
|
|
191
209
|
return () => document.removeEventListener('mousedown', handleClick)
|
|
192
210
|
}, [expanded])
|
|
193
211
|
|
|
194
|
-
if (files.length === 0)
|
|
212
|
+
if (files.length === 0) {
|
|
213
|
+
return null
|
|
214
|
+
}
|
|
195
215
|
|
|
196
216
|
const collapsible = files.length > COLLAPSE_THRESHOLD
|
|
197
217
|
const compact = collapsible
|
|
@@ -202,13 +222,7 @@ export function FileStrip({ files, onRemove, onClick, className }: FileStripProp
|
|
|
202
222
|
<div className={cn('relative', className)}>
|
|
203
223
|
<div className="flex flex-wrap gap-2" role="list" aria-label="Attached files">
|
|
204
224
|
{(collapsible ? visibleFiles : files).map((file) => (
|
|
205
|
-
<FileCard
|
|
206
|
-
key={file.id}
|
|
207
|
-
file={file}
|
|
208
|
-
compact={compact}
|
|
209
|
-
onRemove={onRemove}
|
|
210
|
-
onClick={onClick}
|
|
211
|
-
/>
|
|
225
|
+
<FileCard key={file.id} file={file} compact={compact} onRemove={onRemove} onClick={onClick} />
|
|
212
226
|
))}
|
|
213
227
|
{collapsible && (
|
|
214
228
|
<div role="listitem">
|
|
@@ -219,7 +233,8 @@ export function FileStrip({ files, onRemove, onClick, className }: FileStripProp
|
|
|
219
233
|
className={cn(
|
|
220
234
|
'border-border text-muted-foreground hover:bg-surface-hover flex flex-shrink-0 cursor-pointer items-center justify-center rounded-lg border transition-colors',
|
|
221
235
|
compact ? 'h-10 px-3 text-xs' : 'h-14 px-4 text-sm',
|
|
222
|
-
)}
|
|
236
|
+
)}
|
|
237
|
+
>
|
|
223
238
|
{expanded ? 'Show less' : `+${hiddenCount} more`}
|
|
224
239
|
</button>
|
|
225
240
|
</div>
|
|
@@ -231,16 +246,11 @@ export function FileStrip({ files, onRemove, onClick, className }: FileStripProp
|
|
|
231
246
|
ref={popoverRef}
|
|
232
247
|
className={cn(
|
|
233
248
|
'bg-surface border-border absolute bottom-full left-0 z-10 mb-2 max-h-48 overflow-y-auto rounded-lg border p-2 shadow-lg',
|
|
234
|
-
)}
|
|
249
|
+
)}
|
|
250
|
+
>
|
|
235
251
|
<div className="flex flex-wrap gap-2" role="list" aria-label="More attached files">
|
|
236
252
|
{files.slice(COLLAPSE_THRESHOLD).map((file) => (
|
|
237
|
-
<FileCard
|
|
238
|
-
key={file.id}
|
|
239
|
-
file={file}
|
|
240
|
-
compact={compact}
|
|
241
|
-
onRemove={onRemove}
|
|
242
|
-
onClick={onClick}
|
|
243
|
-
/>
|
|
253
|
+
<FileCard key={file.id} file={file} compact={compact} onRemove={onRemove} onClick={onClick} />
|
|
244
254
|
))}
|
|
245
255
|
</div>
|
|
246
256
|
</div>
|
|
@@ -14,9 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { isHTMLElement, isTextNode } from './dom-helpers.ts'
|
|
16
16
|
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
17
|
// Inline style / emphasis detection
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
18
|
|
|
21
19
|
/** Reads a single declaration value from an inline `style` attribute string. */
|
|
22
20
|
function getStyleValue(style: string, prop: string): string {
|
|
@@ -26,7 +24,9 @@ function getStyleValue(style: string, prop: string): string {
|
|
|
26
24
|
|
|
27
25
|
/** Whether a CSS font-weight value is bold (`bold`, `bolder`, or >= 600). */
|
|
28
26
|
function isBoldWeight(value: string): boolean {
|
|
29
|
-
if (value === 'bold' || value === 'bolder')
|
|
27
|
+
if (value === 'bold' || value === 'bolder') {
|
|
28
|
+
return true
|
|
29
|
+
}
|
|
30
30
|
const numeric = Number.parseInt(value, 10)
|
|
31
31
|
return !Number.isNaN(numeric) && numeric >= 600
|
|
32
32
|
}
|
|
@@ -45,22 +45,17 @@ function inlineEmphasis(node: HTMLElement): { prefix: string; suffix: string } {
|
|
|
45
45
|
const bold = weight ? isBoldWeight(weight) : tag === 'B' || tag === 'STRONG'
|
|
46
46
|
|
|
47
47
|
const fontStyle = getStyleValue(style, 'font-style')
|
|
48
|
-
const italic = fontStyle
|
|
49
|
-
? fontStyle === 'italic' || fontStyle === 'oblique'
|
|
50
|
-
: tag === 'I' || tag === 'EM'
|
|
48
|
+
const italic = fontStyle ? fontStyle === 'italic' || fontStyle === 'oblique' : tag === 'I' || tag === 'EM'
|
|
51
49
|
|
|
52
50
|
const decoration = getStyleValue(style, 'text-decoration')
|
|
53
|
-
const strike =
|
|
54
|
-
tag === 'S' || tag === 'DEL' || tag === 'STRIKE' || decoration.includes('line-through')
|
|
51
|
+
const strike = tag === 'S' || tag === 'DEL' || tag === 'STRIKE' || decoration.includes('line-through')
|
|
55
52
|
|
|
56
53
|
const prefix = (strike ? '~~' : '') + (bold ? '**' : '') + (italic ? '*' : '')
|
|
57
54
|
const suffix = (italic ? '*' : '') + (bold ? '**' : '') + (strike ? '~~' : '')
|
|
58
55
|
return { prefix, suffix }
|
|
59
56
|
}
|
|
60
57
|
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
58
|
// Text handling
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
59
|
|
|
65
60
|
/** Collapses HTML whitespace runs (incl. ` ` -> U+00A0) to single spaces. */
|
|
66
61
|
function collapseWhitespace(text: string): string {
|
|
@@ -72,16 +67,16 @@ function escapeText(text: string): string {
|
|
|
72
67
|
return text.replace(/\*/g, '\\*')
|
|
73
68
|
}
|
|
74
69
|
|
|
75
|
-
// ---------------------------------------------------------------------------
|
|
76
70
|
// Block serializers
|
|
77
|
-
// ---------------------------------------------------------------------------
|
|
78
71
|
|
|
79
72
|
/** Derives a fenced-block language from `class="language-ts"` or `lang="ts"`. */
|
|
80
73
|
function detectCodeLang(pre: HTMLElement): string {
|
|
81
74
|
const code = pre.querySelector('code')
|
|
82
75
|
const classNames = `${pre.className} ${code?.className ?? ''}`
|
|
83
76
|
const fromClass = /(?:language|lang)-([\w-]+)/.exec(classNames)
|
|
84
|
-
if (fromClass)
|
|
77
|
+
if (fromClass) {
|
|
78
|
+
return fromClass[1]
|
|
79
|
+
}
|
|
85
80
|
return pre.getAttribute('lang') ?? code?.getAttribute('lang') ?? ''
|
|
86
81
|
}
|
|
87
82
|
|
|
@@ -93,7 +88,9 @@ function serializePre(pre: HTMLElement): string {
|
|
|
93
88
|
|
|
94
89
|
function serializeInlineCode(node: HTMLElement): string {
|
|
95
90
|
const content = node.textContent ?? ''
|
|
96
|
-
if (content.includes('`'))
|
|
91
|
+
if (content.includes('`')) {
|
|
92
|
+
return `\`\` ${content} \`\``
|
|
93
|
+
}
|
|
97
94
|
return `\`${content}\``
|
|
98
95
|
}
|
|
99
96
|
|
|
@@ -105,8 +102,12 @@ function isSafeHref(href: string): boolean {
|
|
|
105
102
|
function serializeAnchor(node: HTMLElement, depth: number): string {
|
|
106
103
|
const href = node.getAttribute('href') ?? ''
|
|
107
104
|
const label = serializeChildren(node, depth).trim()
|
|
108
|
-
if (!isSafeHref(href))
|
|
109
|
-
|
|
105
|
+
if (!isSafeHref(href)) {
|
|
106
|
+
return label
|
|
107
|
+
}
|
|
108
|
+
if (!label || label === href) {
|
|
109
|
+
return href
|
|
110
|
+
}
|
|
110
111
|
return `[${label}](${href})`
|
|
111
112
|
}
|
|
112
113
|
|
|
@@ -115,7 +116,9 @@ function serializeImage(node: HTMLElement): string {
|
|
|
115
116
|
// Gate the src through the same allow-list as anchors: only http(s)/mailto
|
|
116
117
|
// survive, so a `javascript:`/`vbscript:`/`data:` src never reaches the
|
|
117
118
|
// emitted markdown (defense-in-depth for consumers that render it as HTML).
|
|
118
|
-
if (!src || !isSafeHref(src))
|
|
119
|
+
if (!src || !isSafeHref(src)) {
|
|
120
|
+
return ''
|
|
121
|
+
}
|
|
119
122
|
return ``
|
|
120
123
|
}
|
|
121
124
|
|
|
@@ -141,7 +144,9 @@ function serializeList(list: HTMLElement, depth: number): string {
|
|
|
141
144
|
const lines: string[] = []
|
|
142
145
|
|
|
143
146
|
for (const child of Array.from(list.childNodes)) {
|
|
144
|
-
if (!isHTMLElement(child) || child.tagName !== 'LI')
|
|
147
|
+
if (!isHTMLElement(child) || child.tagName !== 'LI') {
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
145
150
|
|
|
146
151
|
const marker = ordered ? `${index}. ` : '- '
|
|
147
152
|
index++
|
|
@@ -163,14 +168,14 @@ function serializeList(list: HTMLElement, depth: number): string {
|
|
|
163
168
|
|
|
164
169
|
function serializeTable(table: HTMLElement, depth: number): string {
|
|
165
170
|
const rows = Array.from(table.querySelectorAll('tr'))
|
|
166
|
-
if (rows.length === 0)
|
|
171
|
+
if (rows.length === 0) {
|
|
172
|
+
return ''
|
|
173
|
+
}
|
|
167
174
|
|
|
168
175
|
const cells = rows.map((row) =>
|
|
169
176
|
Array.from(row.children)
|
|
170
177
|
.filter((cell) => cell.tagName === 'TD' || cell.tagName === 'TH')
|
|
171
|
-
.map((cell) =>
|
|
172
|
-
serializeChildren(cell, depth).replace(/\n+/g, ' ').replace(/\|/g, '\\|').trim(),
|
|
173
|
-
),
|
|
178
|
+
.map((cell) => serializeChildren(cell, depth).replace(/\n+/g, ' ').replace(/\|/g, '\\|').trim()),
|
|
174
179
|
)
|
|
175
180
|
|
|
176
181
|
const header = cells[0]
|
|
@@ -180,9 +185,7 @@ function serializeTable(table: HTMLElement, depth: number): string {
|
|
|
180
185
|
return [toRow(header), toRow(separator), ...cells.slice(1).map(toRow)].join('\n')
|
|
181
186
|
}
|
|
182
187
|
|
|
183
|
-
// ---------------------------------------------------------------------------
|
|
184
188
|
// Recursive walker
|
|
185
|
-
// ---------------------------------------------------------------------------
|
|
186
189
|
|
|
187
190
|
function serializeChildren(node: Node, depth: number): string {
|
|
188
191
|
let out = ''
|
|
@@ -193,8 +196,12 @@ function serializeChildren(node: Node, depth: number): string {
|
|
|
193
196
|
}
|
|
194
197
|
|
|
195
198
|
function serializeNode(node: Node, depth: number): string {
|
|
196
|
-
if (isTextNode(node))
|
|
197
|
-
|
|
199
|
+
if (isTextNode(node)) {
|
|
200
|
+
return escapeText(collapseWhitespace(node.textContent ?? ''))
|
|
201
|
+
}
|
|
202
|
+
if (!isHTMLElement(node)) {
|
|
203
|
+
return ''
|
|
204
|
+
}
|
|
198
205
|
|
|
199
206
|
const tag = node.tagName
|
|
200
207
|
switch (tag) {
|
|
@@ -202,42 +209,56 @@ function serializeNode(node: Node, depth: number): string {
|
|
|
202
209
|
case 'STYLE':
|
|
203
210
|
case 'NOSCRIPT':
|
|
204
211
|
case 'HEAD':
|
|
205
|
-
case 'TITLE':
|
|
212
|
+
case 'TITLE': {
|
|
206
213
|
return ''
|
|
207
|
-
|
|
214
|
+
}
|
|
215
|
+
case 'BR': {
|
|
208
216
|
return '\n'
|
|
209
|
-
|
|
217
|
+
}
|
|
218
|
+
case 'HR': {
|
|
210
219
|
return '\n\n---\n\n'
|
|
220
|
+
}
|
|
211
221
|
case 'H1':
|
|
212
222
|
case 'H2':
|
|
213
223
|
case 'H3':
|
|
214
224
|
case 'H4':
|
|
215
225
|
case 'H5':
|
|
216
|
-
case 'H6':
|
|
226
|
+
case 'H6': {
|
|
217
227
|
return `\n\n${'#'.repeat(Number(tag[1]))} ${serializeChildren(node, depth).trim()}\n\n`
|
|
218
|
-
|
|
228
|
+
}
|
|
229
|
+
case 'P': {
|
|
219
230
|
return `\n\n${serializeChildren(node, depth).trim()}\n\n`
|
|
220
|
-
|
|
231
|
+
}
|
|
232
|
+
case 'DIV': {
|
|
221
233
|
return `\n${serializeChildren(node, depth).trim()}\n`
|
|
222
|
-
|
|
234
|
+
}
|
|
235
|
+
case 'BLOCKQUOTE': {
|
|
223
236
|
return serializeBlockquote(node, depth)
|
|
237
|
+
}
|
|
224
238
|
case 'UL':
|
|
225
|
-
case 'OL':
|
|
239
|
+
case 'OL': {
|
|
226
240
|
return `\n\n${serializeList(node, depth)}\n\n`
|
|
227
|
-
|
|
241
|
+
}
|
|
242
|
+
case 'LI': {
|
|
228
243
|
// A stray <li> outside a list wrapper — emit its content as a line.
|
|
229
244
|
return `${serializeChildren(node, depth).trim()}\n`
|
|
230
|
-
|
|
245
|
+
}
|
|
246
|
+
case 'PRE': {
|
|
231
247
|
return serializePre(node)
|
|
232
|
-
|
|
248
|
+
}
|
|
249
|
+
case 'CODE': {
|
|
233
250
|
// Inline code only: <pre> handles its own <code> via textContent.
|
|
234
251
|
return serializeInlineCode(node)
|
|
235
|
-
|
|
252
|
+
}
|
|
253
|
+
case 'A': {
|
|
236
254
|
return serializeAnchor(node, depth)
|
|
237
|
-
|
|
255
|
+
}
|
|
256
|
+
case 'IMG': {
|
|
238
257
|
return serializeImage(node)
|
|
239
|
-
|
|
258
|
+
}
|
|
259
|
+
case 'TABLE': {
|
|
240
260
|
return `\n\n${serializeTable(node, depth)}\n\n`
|
|
261
|
+
}
|
|
241
262
|
default: {
|
|
242
263
|
// Inline emphasis (B/STRONG/I/EM/S/DEL + styled SPAN/FONT) and the
|
|
243
264
|
// "span soup" unwrap case both resolve here: emphasis markers when the
|
|
@@ -248,9 +269,7 @@ function serializeNode(node: Node, depth: number): string {
|
|
|
248
269
|
}
|
|
249
270
|
}
|
|
250
271
|
|
|
251
|
-
// ---------------------------------------------------------------------------
|
|
252
272
|
// Output normalization
|
|
253
|
-
// ---------------------------------------------------------------------------
|
|
254
273
|
|
|
255
274
|
/** Trims trailing spaces and caps consecutive blank lines at one. */
|
|
256
275
|
function normalizeOutput(markdown: string): string {
|
|
@@ -260,9 +279,7 @@ function normalizeOutput(markdown: string): string {
|
|
|
260
279
|
.trim()
|
|
261
280
|
}
|
|
262
281
|
|
|
263
|
-
// ---------------------------------------------------------------------------
|
|
264
282
|
// Public API
|
|
265
|
-
// ---------------------------------------------------------------------------
|
|
266
283
|
|
|
267
284
|
/**
|
|
268
285
|
* Converts an HTML string to markdown source text. Returns '' for empty or
|
|
@@ -271,8 +288,12 @@ function normalizeOutput(markdown: string): string {
|
|
|
271
288
|
* display; only `*`/`**`/`***` and bare URLs get visually decorated inline.
|
|
272
289
|
*/
|
|
273
290
|
export function htmlToMarkdown(html: string): string {
|
|
274
|
-
if (!html)
|
|
291
|
+
if (!html) {
|
|
292
|
+
return ''
|
|
293
|
+
}
|
|
275
294
|
const doc = new DOMParser().parseFromString(html, 'text/html')
|
|
276
|
-
if (!doc.body)
|
|
295
|
+
if (!doc.body) {
|
|
296
|
+
return ''
|
|
297
|
+
}
|
|
277
298
|
return normalizeOutput(serializeChildren(doc.body, 0))
|
|
278
299
|
}
|
|
@@ -12,7 +12,9 @@ type ImageStripProps = {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function ImageStrip({ images, onRemove, onClick, className }: ImageStripProps) {
|
|
15
|
-
if (images.length === 0)
|
|
15
|
+
if (images.length === 0) {
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
16
18
|
|
|
17
19
|
return (
|
|
18
20
|
<div className={cn('flex flex-wrap gap-2', className)} role="list" aria-label="Attached images">
|
|
@@ -20,28 +22,16 @@ export function ImageStrip({ images, onRemove, onClick, className }: ImageStripP
|
|
|
20
22
|
<div
|
|
21
23
|
key={image.id}
|
|
22
24
|
role="listitem"
|
|
23
|
-
className={cn(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
onClick={() => onClick?.(image)}>
|
|
28
|
-
{/* eslint-disable-next-line @next/next/no-img-element -- registry component used outside Next.js */}
|
|
29
|
-
<img
|
|
30
|
-
src={image.url}
|
|
31
|
-
alt={image.alt ?? 'Attached image'}
|
|
32
|
-
className="h-full w-full object-cover"
|
|
33
|
-
/>
|
|
25
|
+
className={cn('border-border relative h-16 w-16 flex-shrink-0 overflow-hidden rounded-md border', onClick && 'cursor-pointer')}
|
|
26
|
+
onClick={() => onClick?.(image)}
|
|
27
|
+
>
|
|
28
|
+
<img src={image.url} alt={image.alt ?? 'Attached image'} className="h-full w-full object-cover" />
|
|
34
29
|
{image.loading && (
|
|
35
30
|
<div className="absolute inset-0 flex items-center justify-center bg-black/40">
|
|
36
31
|
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
|
37
32
|
</div>
|
|
38
33
|
)}
|
|
39
|
-
{onRemove && (
|
|
40
|
-
<RemoveButton
|
|
41
|
-
onClick={() => onRemove(image)}
|
|
42
|
-
label={`Remove ${image.alt ?? 'image'}`}
|
|
43
|
-
/>
|
|
44
|
-
)}
|
|
34
|
+
{onRemove && <RemoveButton onClick={() => onRemove(image)} label={`Remove ${image.alt ?? 'image'}`} />}
|
|
45
35
|
</div>
|
|
46
36
|
))}
|
|
47
37
|
</div>
|
|
@@ -6,18 +6,5 @@
|
|
|
6
6
|
export { PromptArea } from './prompt-area.tsx'
|
|
7
7
|
export { usePromptAreaState } from './use-prompt-area-state.ts'
|
|
8
8
|
export { commandTrigger, mentionTrigger, hashtagTrigger } from './trigger-presets.ts'
|
|
9
|
-
export {
|
|
10
|
-
|
|
11
|
-
plainTextToSegments,
|
|
12
|
-
isSegmentsEmpty,
|
|
13
|
-
getChipsByTrigger,
|
|
14
|
-
} from './segment-helpers.ts'
|
|
15
|
-
export type {
|
|
16
|
-
PromptAreaHandle,
|
|
17
|
-
PromptAreaProps,
|
|
18
|
-
Segment,
|
|
19
|
-
ChipSegment,
|
|
20
|
-
TextSegment,
|
|
21
|
-
TriggerConfig,
|
|
22
|
-
TriggerSuggestion,
|
|
23
|
-
} from './types.ts'
|
|
9
|
+
export { segmentsToPlainText, plainTextToSegments, isSegmentsEmpty, getChipsByTrigger } from './segment-helpers.ts'
|
|
10
|
+
export type { PromptAreaHandle, PromptAreaProps, Segment, ChipSegment, TextSegment, TriggerConfig, TriggerSuggestion } from './types.ts'
|