@seed-ship/mcp-ui-solid 2.13.0 → 2.14.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.
@@ -20,6 +20,10 @@ export interface ScratchpadPanelProps {
20
20
  /** Called when user clicks retry on error state */
21
21
  onRetry?: () => void
22
22
  onClose?: () => void
23
+ /** When true, action buttons show loading spinner and stay open until next server update */
24
+ asyncAction?: boolean
25
+ /** When true (set by server), scratchpad stays visible during stream */
26
+ pinned?: boolean
23
27
  closable?: boolean
24
28
  autoCloseDelay?: number
25
29
  collapsible?: boolean
@@ -38,6 +42,7 @@ const STATUS_BADGES: Record<ScratchpadState['status'], { label: string; class: s
38
42
  export const ScratchpadPanel: Component<ScratchpadPanelProps> = (props) => {
39
43
  const [collapsed, setCollapsed] = createSignal(false)
40
44
  const [localPreview, setLocalPreview] = createSignal<ScratchpadState['preview']>(undefined)
45
+ const [loadingAction, setLoadingAction] = createSignal<string | null>(null)
41
46
  let previewTimer: ReturnType<typeof setTimeout> | null = null
42
47
  const badge = () => STATUS_BADGES[props.state.status] || STATUS_BADGES.loading
43
48
  const isClosable = () => props.closable !== false
@@ -49,18 +54,25 @@ export const ScratchpadPanel: Component<ScratchpadPanelProps> = (props) => {
49
54
  const CLOSE_ALIASES = new Set(['done', 'close', 'dismiss', 'validate', 'cancel', 'sufficient'])
50
55
 
51
56
  const handleAction = (action: string, data?: unknown) => {
57
+ if (props.asyncAction && !CLOSE_ALIASES.has(action)) {
58
+ setLoadingAction(action)
59
+ }
52
60
  props.onAction?.(action, data)
53
61
  if (CLOSE_ALIASES.has(action) && props.onClose) {
54
62
  props.onClose()
55
63
  }
56
64
  }
57
65
 
58
- // Auto-close on complete
66
+ // Auto-close on complete (unless pinned)
59
67
  createEffect(() => {
60
- if (props.state.status === 'complete' && props.autoCloseDelay) {
68
+ if (props.state.status === 'complete' && props.autoCloseDelay && !props.pinned) {
61
69
  const timer = setTimeout(() => props.onClose?.(), props.autoCloseDelay)
62
70
  onCleanup(() => clearTimeout(timer))
63
71
  }
72
+ // Clear loading action when server responds
73
+ if (props.state.status !== 'processing' && loadingAction()) {
74
+ setLoadingAction(null)
75
+ }
64
76
  })
65
77
 
66
78
  // Preview auto-refresh when filters change
@@ -285,6 +297,9 @@ const SectionRenderer: Component<{
285
297
  <Match when={props.section.type === 'feedback'}><FeedbackSection content={props.section.content} onAction={props.onAction} /></Match>
286
298
  <Match when={props.section.type === 'prompt'}><PromptSection content={props.section.content} onAction={props.onAction} /></Match>
287
299
  <Match when={props.section.type === 'stepper'}><StepperProgressSection content={props.section.content} /></Match>
300
+ <Match when={props.section.type === 'error'}><ErrorSectionRenderer content={props.section.content} onAction={props.onAction} /></Match>
301
+ <Match when={props.section.type === 'source_card'}><SourceCardSection content={props.section.content} /></Match>
302
+ <Match when={props.section.type === 'diff'}><DiffSection content={props.section.content} /></Match>
288
303
  <Match when={true}><pre class="text-xs text-gray-500 overflow-auto">{JSON.stringify(props.section.content, null, 2)}</pre></Match>
289
304
  </Switch>
290
305
  </div>
@@ -798,3 +813,116 @@ const StepperProgressSection: Component<{ content: unknown }> = (props) => {
798
813
  </Show>
799
814
  )
800
815
  }
816
+
817
+ // ─── Error Section (F6) ──────────────────────────────────────
818
+
819
+ const ErrorSectionRenderer: Component<{
820
+ content: unknown
821
+ onAction?: (action: string, data?: unknown) => void
822
+ }> = (props) => {
823
+ const [showDetails, setShowDetails] = createSignal(false)
824
+ const data = () => {
825
+ const c = props.content as any
826
+ return { message: c?.message || 'Error', severity: c?.severity || 'error', retryAction: c?.retryAction, retryLabel: c?.retryLabel || 'Retry', details: c?.details, timestamp: c?.timestamp }
827
+ }
828
+ const isWarning = () => data().severity === 'warning'
829
+
830
+ return (
831
+ <div class={`rounded-lg px-3 py-2 text-sm ${isWarning() ? 'bg-amber-50 dark:bg-amber-900/10 text-amber-700 dark:text-amber-400 border border-amber-200 dark:border-amber-800' : 'bg-red-50 dark:bg-red-900/10 text-red-700 dark:text-red-400 border border-red-200 dark:border-red-800'}`}>
832
+ <div class="flex items-start gap-2">
833
+ <span class="flex-shrink-0">{isWarning() ? '⚠️' : '❌'}</span>
834
+ <div class="flex-1">
835
+ <p>{data().message}</p>
836
+ <div class="flex gap-2 mt-2">
837
+ <Show when={data().retryAction}>
838
+ <button type="button" onClick={() => props.onAction?.(data().retryAction!)} class={`px-2 py-1 text-xs font-medium rounded ${isWarning() ? 'bg-amber-600 text-white hover:bg-amber-700' : 'bg-red-600 text-white hover:bg-red-700'} transition-colors`}>&#128260; {data().retryLabel}</button>
839
+ </Show>
840
+ <Show when={data().details}>
841
+ <button type="button" onClick={() => setShowDetails(!showDetails())} class="px-2 py-1 text-xs opacity-70 hover:opacity-100">&#9654; Details</button>
842
+ </Show>
843
+ </div>
844
+ <Show when={showDetails() && data().details}>
845
+ <pre class="mt-2 text-xs opacity-70 overflow-x-auto">{data().details}</pre>
846
+ </Show>
847
+ </div>
848
+ </div>
849
+ </div>
850
+ )
851
+ }
852
+
853
+ // ─── Source Card Section (F9) ────────────────────────────────
854
+
855
+ const SourceCardSection: Component<{ content: unknown }> = (props) => {
856
+ const data = () => {
857
+ const c = props.content as any
858
+ return { name: c?.name || 'Source', status: c?.status || 'available', capabilities: c?.capabilities || [], latency_ms: c?.latency_ms, freshness: c?.freshness, row_count: c?.row_count }
859
+ }
860
+ const statusIcon = () => ({ queried: '✅', available: '📦', error: '❌' } as Record<string, string>)[data().status] || '📦'
861
+
862
+ return (
863
+ <div class="rounded-lg border border-gray-200 dark:border-gray-700 px-3 py-2">
864
+ <div class="flex items-center justify-between mb-1.5">
865
+ <span class="text-sm font-medium text-gray-900 dark:text-white">{statusIcon()} {data().name}</span>
866
+ <Show when={data().row_count !== undefined}>
867
+ <span class="text-xs font-bold text-blue-600 dark:text-blue-400">{data().row_count?.toLocaleString()} results</span>
868
+ </Show>
869
+ </div>
870
+ <div class="flex flex-wrap gap-1.5 mb-1">
871
+ <For each={data().capabilities}>
872
+ {(cap: any) => (
873
+ <span class={`text-[10px] px-1.5 py-0.5 rounded ${cap.supported ? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400' : 'bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400'}`}>
874
+ {cap.supported ? '✅' : '❌'} {cap.label}
875
+ </span>
876
+ )}
877
+ </For>
878
+ </div>
879
+ <Show when={data().freshness || data().latency_ms}>
880
+ <p class="text-[10px] text-gray-400">{[data().freshness, data().latency_ms ? `${data().latency_ms}ms` : ''].filter(Boolean).join(' · ')}</p>
881
+ </Show>
882
+ </div>
883
+ )
884
+ }
885
+
886
+ // ─── Diff Section (F10) ──────────────────────────────────────
887
+
888
+ const DiffSection: Component<{ content: unknown }> = (props) => {
889
+ const data = () => {
890
+ const c = props.content as any
891
+ return { left: c?.left || { label: 'A', rows: [] }, right: c?.right || { label: 'B', rows: [] }, highlight: c?.highlight_columns || [] }
892
+ }
893
+ const allKeys = () => {
894
+ const l = data().left.rows[0] || {}
895
+ const r = data().right.rows[0] || {}
896
+ return [...new Set([...Object.keys(l), ...Object.keys(r)])]
897
+ }
898
+
899
+ return (
900
+ <div class="overflow-x-auto">
901
+ <table class="min-w-full text-xs">
902
+ <thead>
903
+ <tr>
904
+ <th class="px-2 py-1 text-left text-gray-500 dark:text-gray-400"></th>
905
+ <th class="px-2 py-1 text-left font-medium text-blue-600 dark:text-blue-400">{data().left.label}</th>
906
+ <th class="px-2 py-1 text-left font-medium text-purple-600 dark:text-purple-400">{data().right.label}</th>
907
+ </tr>
908
+ </thead>
909
+ <tbody>
910
+ <For each={allKeys()}>
911
+ {(key) => {
912
+ const lVal = () => data().left.rows[0]?.[key]
913
+ const rVal = () => data().right.rows[0]?.[key]
914
+ const isDiff = () => String(lVal()) !== String(rVal()) && data().highlight.includes(key)
915
+ return (
916
+ <tr class={`border-t border-gray-100 dark:border-gray-700 ${isDiff() ? 'bg-yellow-50 dark:bg-yellow-900/10' : ''}`}>
917
+ <td class="px-2 py-1 font-mono text-gray-500 dark:text-gray-400">{key}</td>
918
+ <td class="px-2 py-1 text-gray-900 dark:text-white">{lVal() !== undefined ? String(lVal()) : '—'}</td>
919
+ <td class="px-2 py-1 text-gray-900 dark:text-white">{rVal() !== undefined ? String(rVal()) : '—'}</td>
920
+ </tr>
921
+ )
922
+ }}
923
+ </For>
924
+ </tbody>
925
+ </table>
926
+ </div>
927
+ )
928
+ }
@@ -366,7 +366,7 @@ export interface ScratchpadState {
366
366
  export interface ScratchpadSection {
367
367
  id: string
368
368
  title: string
369
- type: 'data' | 'filter' | 'preview' | 'message' | 'action' | 'steps' | 'form' | 'understanding' | 'feedback' | 'prompt' | 'stepper'
369
+ type: 'data' | 'filter' | 'preview' | 'message' | 'action' | 'steps' | 'form' | 'understanding' | 'feedback' | 'prompt' | 'stepper' | 'error' | 'source_card' | 'diff'
370
370
  content: unknown
371
371
  /** Can the human edit this section? */
372
372
  editable: boolean
@@ -385,6 +385,8 @@ export interface ScratchpadEvent {
385
385
  sections?: ScratchpadSection[]
386
386
  /** How to merge sections on update (default: 'replace') */
387
387
  sectionMode?: 'replace' | 'append' | 'upsert'
388
+ /** If true, scratchpad stays visible during stream (no auto-close on complete) */
389
+ pinned?: boolean
388
390
  filters?: Record<string, string | string[]>
389
391
  preview?: { count: number; rows?: Record<string, unknown>[]; summary: string }
390
392
  agentMessages?: Array<{ text: string; type: 'info' | 'question' | 'warning' }>