@seed-ship/mcp-ui-solid 4.0.6 → 4.2.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.
Files changed (73) hide show
  1. package/dist/components/AgentCard.cjs +122 -0
  2. package/dist/components/AgentCard.cjs.map +1 -0
  3. package/dist/components/AgentCard.d.ts +20 -0
  4. package/dist/components/AgentCard.d.ts.map +1 -0
  5. package/dist/components/AgentCard.js +122 -0
  6. package/dist/components/AgentCard.js.map +1 -0
  7. package/dist/components/AgentHandoff.cjs +49 -0
  8. package/dist/components/AgentHandoff.cjs.map +1 -0
  9. package/dist/components/AgentHandoff.d.ts +12 -0
  10. package/dist/components/AgentHandoff.d.ts.map +1 -0
  11. package/dist/components/AgentHandoff.js +49 -0
  12. package/dist/components/AgentHandoff.js.map +1 -0
  13. package/dist/components/BriefingDiff.cjs +165 -0
  14. package/dist/components/BriefingDiff.cjs.map +1 -0
  15. package/dist/components/BriefingDiff.d.ts +12 -0
  16. package/dist/components/BriefingDiff.d.ts.map +1 -0
  17. package/dist/components/BriefingDiff.js +165 -0
  18. package/dist/components/BriefingDiff.js.map +1 -0
  19. package/dist/components/FormFieldRenderer.cjs +314 -264
  20. package/dist/components/FormFieldRenderer.cjs.map +1 -1
  21. package/dist/components/FormFieldRenderer.d.ts.map +1 -1
  22. package/dist/components/FormFieldRenderer.js +315 -265
  23. package/dist/components/FormFieldRenderer.js.map +1 -1
  24. package/dist/components/FormRenderer.cjs +74 -17
  25. package/dist/components/FormRenderer.cjs.map +1 -1
  26. package/dist/components/FormRenderer.d.ts.map +1 -1
  27. package/dist/components/FormRenderer.js +76 -19
  28. package/dist/components/FormRenderer.js.map +1 -1
  29. package/dist/components/ScratchpadPanel.cjs +618 -411
  30. package/dist/components/ScratchpadPanel.cjs.map +1 -1
  31. package/dist/components/ScratchpadPanel.d.ts.map +1 -1
  32. package/dist/components/ScratchpadPanel.js +619 -412
  33. package/dist/components/ScratchpadPanel.js.map +1 -1
  34. package/dist/components/SplitStepper.cjs +121 -0
  35. package/dist/components/SplitStepper.cjs.map +1 -0
  36. package/dist/components/SplitStepper.d.ts +12 -0
  37. package/dist/components/SplitStepper.d.ts.map +1 -0
  38. package/dist/components/SplitStepper.js +121 -0
  39. package/dist/components/SplitStepper.js.map +1 -0
  40. package/dist/components/index.d.ts +8 -0
  41. package/dist/components/index.d.ts.map +1 -1
  42. package/dist/components.cjs +9 -0
  43. package/dist/components.cjs.map +1 -1
  44. package/dist/components.d.cts +8 -0
  45. package/dist/components.d.ts +8 -0
  46. package/dist/components.js +9 -0
  47. package/dist/components.js.map +1 -1
  48. package/dist/index.cjs +9 -0
  49. package/dist/index.cjs.map +1 -1
  50. package/dist/index.d.cts +9 -1
  51. package/dist/index.d.ts +9 -1
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +9 -0
  54. package/dist/index.js.map +1 -1
  55. package/dist/types/chat-bus.d.ts +81 -1
  56. package/dist/types/chat-bus.d.ts.map +1 -1
  57. package/dist/types/index.d.ts +15 -0
  58. package/dist/types/index.d.ts.map +1 -1
  59. package/dist/types.d.cts +15 -0
  60. package/dist/types.d.ts +15 -0
  61. package/package.json +1 -1
  62. package/src/components/AgentCard.tsx +109 -0
  63. package/src/components/AgentHandoff.tsx +64 -0
  64. package/src/components/BriefingDiff.tsx +93 -0
  65. package/src/components/FormFieldRenderer.tsx +35 -4
  66. package/src/components/FormRenderer.tsx +74 -4
  67. package/src/components/ScratchpadPanel.tsx +131 -49
  68. package/src/components/SplitStepper.tsx +111 -0
  69. package/src/components/index.ts +13 -0
  70. package/src/index.ts +15 -0
  71. package/src/types/chat-bus.ts +70 -1
  72. package/src/types/index.ts +18 -0
  73. package/tsconfig.tsbuildinfo +1 -1
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { Component, Show, For, Switch, Match, Accessor, createSignal, createEffect, onCleanup } from 'solid-js'
8
- import type { FormFieldParams } from '../types'
8
+ import type { FormFieldParams, PrefillSource } from '../types'
9
9
  import { useConditionalField } from '../hooks/useConditionalField'
10
10
 
11
11
  export interface FormFieldRendererProps {
@@ -20,6 +20,14 @@ export interface FormFieldRendererProps {
20
20
  formData?: Accessor<Record<string, any>>
21
21
  }
22
22
 
23
+ /** Badge config by prefill source */
24
+ const SOURCE_BADGES: Record<PrefillSource, { icon: string; title: string } | null> = {
25
+ detected: { icon: '\u2705', title: 'Detected from message' },
26
+ inferred: { icon: '\uD83D\uDD17', title: 'Inferred from context' },
27
+ user: { icon: '\u270F\uFE0F', title: 'Previously provided' },
28
+ default: null,
29
+ }
30
+
23
31
  export const FormFieldRenderer: Component<FormFieldRendererProps> = (props) => {
24
32
  // Conditional visibility based on showWhen
25
33
  const { isVisible } = useConditionalField({
@@ -27,10 +35,23 @@ export const FormFieldRenderer: Component<FormFieldRendererProps> = (props) => {
27
35
  formData: props.formData || (() => ({})),
28
36
  })
29
37
 
38
+ // Muted state — starts muted if field says so, clears on focus/click
39
+ const [isMuted, setIsMuted] = createSignal(!!props.field.muted)
40
+
41
+ const handleFieldFocus = () => {
42
+ if (isMuted()) setIsMuted(false)
43
+ }
44
+
30
45
  const status = () => props.field.fieldStatus || 'optional'
31
46
  const isUnsupported = () => status() === 'unsupported'
32
47
  const isFieldDisabled = () => props.disabled || isUnsupported()
33
48
 
49
+ const sourceBadge = () => {
50
+ const src = props.field.source
51
+ if (!src) return null
52
+ return SOURCE_BADGES[src]
53
+ }
54
+
34
55
  const baseInputClass = () => `
35
56
  w-full px-3 py-2 border rounded-md
36
57
  focus:ring-2 focus:ring-blue-500 focus:border-blue-500
@@ -40,6 +61,7 @@ export const FormFieldRenderer: Component<FormFieldRendererProps> = (props) => {
40
61
  : 'border-gray-300 dark:border-gray-600'}
41
62
  dark:bg-gray-700 dark:text-white
42
63
  ${isUnsupported() ? 'opacity-50' : ''}
64
+ ${isMuted() ? 'bg-gray-50 dark:bg-gray-700/50 text-gray-500 dark:text-gray-400' : ''}
43
65
  `
44
66
 
45
67
  const fieldId = () => `field-${props.field.name}`
@@ -47,12 +69,15 @@ export const FormFieldRenderer: Component<FormFieldRendererProps> = (props) => {
47
69
 
48
70
  return (
49
71
  <Show when={isVisible()}>
50
- <div class="space-y-1">
72
+ <div class="space-y-1" onFocusIn={handleFieldFocus} onClick={handleFieldFocus}>
51
73
  <Show when={props.field.label && props.field.type !== 'checkbox'}>
52
74
  <label
53
75
  for={fieldId()}
54
- class={`block text-sm font-medium ${isUnsupported() ? 'text-gray-400 dark:text-gray-500' : 'text-gray-700 dark:text-gray-300'}`}
76
+ class={`block text-sm font-medium ${isUnsupported() ? 'text-gray-400 dark:text-gray-500' : isMuted() ? 'text-gray-400 dark:text-gray-500' : 'text-gray-700 dark:text-gray-300'}`}
55
77
  >
78
+ <Show when={sourceBadge()}>
79
+ <span class="mr-1" title={sourceBadge()!.title}>{sourceBadge()!.icon}</span>
80
+ </Show>
56
81
  {props.field.label}
57
82
  <Show when={props.field.required || status() === 'required'}>
58
83
  <span class="text-red-500 ml-1" aria-hidden="true">*</span>
@@ -322,7 +347,13 @@ export const FormFieldRenderer: Component<FormFieldRendererProps> = (props) => {
322
347
  </p>
323
348
  </Show>
324
349
 
325
- <Show when={props.field.helpText && !props.error && !props.field.statusReason}>
350
+ <Show when={props.field.displayHint && props.field.prefill != null}>
351
+ <p class={`text-xs italic ${isMuted() ? 'text-gray-400 dark:text-gray-500' : 'text-gray-500 dark:text-gray-400'}`}>
352
+ {props.field.displayHint}
353
+ </p>
354
+ </Show>
355
+
356
+ <Show when={props.field.helpText && !props.error && !props.field.statusReason && !props.field.displayHint}>
326
357
  <p class="text-xs text-gray-500 dark:text-gray-400">{props.field.helpText}</p>
327
358
  </Show>
328
359
 
@@ -5,7 +5,7 @@
5
5
  * Sprint 4: Form state persistence
6
6
  */
7
7
 
8
- import { Component, createSignal, For, Show, onMount, createEffect } from 'solid-js'
8
+ import { Component, createSignal, For, Show, onMount, createEffect, onCleanup } from 'solid-js'
9
9
  import { FormFieldRenderer } from './FormFieldRenderer'
10
10
  import type { UIComponent, FormComponentParams, FormFieldParams } from '../types'
11
11
  import { useAction } from '../hooks/useAction'
@@ -56,11 +56,43 @@ export const FormRenderer: Component<FormRendererProps> = (props) => {
56
56
  })
57
57
  }
58
58
 
59
- // Initialize form data with default values
59
+ // Auto-submit countdown state (v4.2.0)
60
+ const [countdown, setCountdown] = createSignal<number | null>(null)
61
+ let countdownTimer: ReturnType<typeof setInterval> | null = null
62
+ const [userInteracted, setUserInteracted] = createSignal(false)
63
+
64
+ const cancelCountdown = () => {
65
+ if (countdownTimer) {
66
+ clearInterval(countdownTimer)
67
+ countdownTimer = null
68
+ }
69
+ setCountdown(null)
70
+ }
71
+
72
+ const handleUserInteraction = () => {
73
+ if (!userInteracted()) {
74
+ setUserInteracted(true)
75
+ cancelCountdown()
76
+ }
77
+ }
78
+
79
+ onCleanup(() => cancelCountdown())
80
+
81
+ /**
82
+ * Check if all required fields have prefill values
83
+ */
84
+ const allRequiredPrefilled = (): boolean => {
85
+ return params().fields
86
+ .filter((f) => f.required)
87
+ .every((f) => f.prefill != null)
88
+ }
89
+
90
+ // Initialize form data with default values, applying prefill (v4.2.0)
60
91
  const initializeForm = (clearStorage = false) => {
61
92
  const initial: Record<string, any> = {}
62
93
  for (const field of params().fields) {
63
- initial[field.name] = field.defaultValue ?? getFieldDefault(field.type)
94
+ // prefill takes priority over defaultValue
95
+ initial[field.name] = field.prefill ?? field.defaultValue ?? getFieldDefault(field.type)
64
96
  }
65
97
  setFormData(initial)
66
98
  setErrors({})
@@ -91,7 +123,29 @@ export const FormRenderer: Component<FormRendererProps> = (props) => {
91
123
  }
92
124
  })
93
125
 
126
+ // Auto-submit countdown (v4.2.0)
127
+ createEffect(() => {
128
+ const delay = params().autoSubmitDelay
129
+ if (!delay || !allRequiredPrefilled() || userInteracted()) return
130
+
131
+ let remaining = Math.ceil(delay / 1000)
132
+ setCountdown(remaining)
133
+
134
+ countdownTimer = setInterval(() => {
135
+ remaining--
136
+ if (remaining <= 0) {
137
+ cancelCountdown()
138
+ // Trigger submit programmatically
139
+ const form = document.querySelector(`#form-${props.component.id}`) as HTMLFormElement | null
140
+ if (form) form.requestSubmit()
141
+ } else {
142
+ setCountdown(remaining)
143
+ }
144
+ }, 1000)
145
+ })
146
+
94
147
  const handleFieldChange = (name: string, value: any) => {
148
+ handleUserInteraction()
95
149
  setFormData((prev) => ({ ...prev, [name]: value }))
96
150
  // Clear error on change
97
151
  if (errors()[name]) {
@@ -179,7 +233,7 @@ export const FormRenderer: Component<FormRendererProps> = (props) => {
179
233
  </h3>
180
234
  </Show>
181
235
 
182
- <form onSubmit={handleSubmit} noValidate>
236
+ <form id={`form-${props.component.id}`} onSubmit={handleSubmit} noValidate>
183
237
  <div class={layoutClass()}>
184
238
  <For each={params().fields}>
185
239
  {(field) => (
@@ -203,6 +257,22 @@ export const FormRenderer: Component<FormRendererProps> = (props) => {
203
257
  </div>
204
258
  </Show>
205
259
 
260
+ {/* Auto-submit countdown (v4.2.0) */}
261
+ <Show when={countdown() != null}>
262
+ <div class="mt-4 flex items-center gap-3 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md">
263
+ <span class="text-sm text-blue-700 dark:text-blue-300">
264
+ {params().submitLabel || 'Submit'} in {countdown()}s...
265
+ </span>
266
+ <button
267
+ type="button"
268
+ onClick={() => { cancelCountdown(); setUserInteracted(true) }}
269
+ class="text-sm text-blue-600 dark:text-blue-400 underline hover:text-blue-800 dark:hover:text-blue-200"
270
+ >
271
+ Cancel
272
+ </button>
273
+ </div>
274
+ </Show>
275
+
206
276
  <div class="flex gap-2 pt-4 mt-4 border-t border-gray-200 dark:border-gray-700">
207
277
  <button
208
278
  type="submit"
@@ -6,13 +6,17 @@
6
6
  */
7
7
 
8
8
  import { Component, Show, For, Switch, Match, createSignal, createEffect, onCleanup } from 'solid-js'
9
- import type { ScratchpadState, ScratchpadSection, VerifiedTextContent, DataPreviewContent, MapSectionContent } from '../types/chat-bus'
9
+ import type { ScratchpadState, ScratchpadSection, VerifiedTextContent, DataPreviewContent, MapSectionContent, AgentCardContent, SplitStepperContent, AgentHandoffContent, BriefingDiffContent } from '../types/chat-bus'
10
10
  import type { FormFieldParams, ChartComponentParams } from '../types'
11
11
  import { FormFieldRenderer } from './FormFieldRenderer'
12
12
  import { VerifiedText } from './VerifiedText'
13
13
  import { DataPreviewSection } from './DataPreviewSection'
14
14
  import { MapRenderer } from './MapRenderer'
15
15
  import { ChartJSRenderer } from './ChartJSRenderer'
16
+ import { AgentCard, AgentStatusBadge } from './AgentCard'
17
+ import { SplitStepper } from './SplitStepper'
18
+ import { AgentHandoff } from './AgentHandoff'
19
+ import { BriefingDiff } from './BriefingDiff'
16
20
 
17
21
  export interface ScratchpadPanelProps {
18
22
  state: ScratchpadState
@@ -177,6 +181,14 @@ export const ScratchpadPanel: Component<ScratchpadPanelProps> = (props) => {
177
181
  <div class="flex items-center gap-2">
178
182
  <span class="text-base">&#128221;</span>
179
183
  <h3 class="text-sm font-semibold text-gray-900 dark:text-white">{props.state.title}</h3>
184
+ {/* AgentStatusBadge — auto-detected from agent_card sections (v4.1.0) */}
185
+ {(() => {
186
+ const agentSection = props.state.sections.find(s => s.type === 'agent_card')
187
+ if (!agentSection) return null
188
+ const ac = parseContent(agentSection.content) as AgentCardContent | null
189
+ if (!ac?.name) return null
190
+ return <AgentStatusBadge agentName={ac.name} status={ac.status || 'idle'} />
191
+ })()}
180
192
  <Show when={isCollapsible()}>
181
193
  <svg class={`w-3.5 h-3.5 text-gray-400 transition-transform ${collapsed() ? '-rotate-90' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
182
194
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
@@ -363,7 +375,7 @@ const SectionRenderer: Component<{
363
375
  onSubmit?: (sectionId: string, values: Record<string, unknown>) => void
364
376
  }> = (props) => {
365
377
  return (
366
- <div class="px-4 py-3">
378
+ <div class="px-4 py-3 animate-[slideDown_0.2s_ease-out]">
367
379
  <h4 class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2">{props.section.title}</h4>
368
380
  <Switch>
369
381
  <Match when={props.section.type === 'data'}><DataSection content={parseContent(props.section.content)} /></Match>
@@ -383,6 +395,10 @@ const SectionRenderer: Component<{
383
395
  <Match when={props.section.type === 'data_preview'}><DataPreviewSection content={parseContent(props.section.content) as DataPreviewContent} /></Match>
384
396
  <Match when={props.section.type === 'map'}>{(() => { const c = parseContent(props.section.content) as MapSectionContent; return <MapRenderer params={{ geojson: c.geojson, center: c.center, zoom: c.zoom, geojsonStyle: c.style, popup: c.popup, layers: c.layers, height: c.height || '300px', fitBounds: true }} /> })()}</Match>
385
397
  <Match when={props.section.type === 'chart'}>{(() => { const c = parseContent(props.section.content) as ChartComponentParams; return <ChartJSRenderer component={{ id: props.section.id, type: 'chart', position: { colStart: 1, colSpan: 12 }, params: { ...c, renderer: 'native', height: (c as any)?.height || '250px' } }} /> })()}</Match>
398
+ <Match when={props.section.type === 'agent_card'}><AgentCard content={parseContent(props.section.content) as AgentCardContent} /></Match>
399
+ <Match when={props.section.type === 'split_stepper'}><SplitStepper content={parseContent(props.section.content) as SplitStepperContent} /></Match>
400
+ <Match when={props.section.type === 'agent_handoff'}><AgentHandoff content={parseContent(props.section.content) as AgentHandoffContent} /></Match>
401
+ <Match when={props.section.type === 'briefing_diff'}><BriefingDiff content={parseContent(props.section.content) as BriefingDiffContent} /></Match>
386
402
  <Match when={true}><pre class="text-xs text-gray-500 overflow-auto">{JSON.stringify(props.section.content, null, 2)}</pre></Match>
387
403
  </Switch>
388
404
  </div>
@@ -654,30 +670,63 @@ const ActionSection: Component<{
654
670
  content: unknown
655
671
  onAction?: (action: string, data?: unknown) => void
656
672
  }> = (props) => {
657
- const actions = () => {
658
- if (Array.isArray(props.content)) return props.content as Array<{ label: string; value?: string; action?: string; variant?: string; icon?: string }>
659
- const obj = props.content as Record<string, unknown> | null
660
- if (obj && Array.isArray(obj.actions)) {
661
- console.warn('[MCP-UI] ActionSection: content should be an array, got { actions: [...] }. Unwrapping automatically.')
662
- return obj.actions as Array<{ label: string; value?: string; action?: string; variant?: string; icon?: string }>
663
- }
664
- return []
673
+ const data = () => {
674
+ const c = props.content as any
675
+ if (Array.isArray(c)) return { actions: c, title: undefined, preview: undefined, validation: undefined }
676
+ if (c && Array.isArray(c.actions)) return { actions: c.actions, title: c.title, preview: c.preview, validation: c.validation }
677
+ return { actions: [], title: undefined, preview: undefined, validation: undefined }
665
678
  }
679
+
666
680
  return (
667
- <div class="flex flex-wrap gap-2">
668
- <For each={actions()}>
669
- {(item) => (
670
- <button type="button" onClick={() => props.onAction?.(item.value || item.action || item.label, item)}
671
- class={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${
672
- item.variant === 'primary' ? 'bg-blue-600 text-white hover:bg-blue-700'
673
- : item.variant === 'danger' ? 'bg-red-600 text-white hover:bg-red-700'
674
- : 'border border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
675
- }`}>
676
- <Show when={item.icon}><span class="mr-1">{item.icon}</span></Show>
677
- {item.label}
678
- </button>
681
+ <div>
682
+ {/* Confirm checkpoint: title + preview (v4.1.0) */}
683
+ <Show when={data().title}>
684
+ <p class="text-sm font-medium text-gray-800 dark:text-gray-200 mb-2">{data().title}</p>
685
+ </Show>
686
+ <Show when={data().preview}>
687
+ {(preview) => (
688
+ <div class="mb-2 p-2 rounded bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-xs text-gray-600 dark:text-gray-400">
689
+ <Show when={preview().count != null}>
690
+ <span class="font-medium text-gray-800 dark:text-gray-200">{preview().count}</span> items
691
+ </Show>
692
+ <Show when={preview().summary}>
693
+ <span class="ml-1">&mdash; {preview().summary}</span>
694
+ </Show>
695
+ </div>
679
696
  )}
680
- </For>
697
+ </Show>
698
+ <Show when={data().validation && data().validation.confidence != null}>
699
+ <div class="mb-2 flex items-center gap-2 text-xs">
700
+ <span classList={{
701
+ 'text-green-600 dark:text-green-400': data().validation.confidence >= 0.8,
702
+ 'text-amber-600 dark:text-amber-400': data().validation.confidence >= 0.5 && data().validation.confidence < 0.8,
703
+ 'text-red-600 dark:text-red-400': data().validation.confidence < 0.5,
704
+ }}>
705
+ {Math.round(data().validation.confidence * 100)}% verified
706
+ </span>
707
+ <Show when={data().validation.hallucinated?.length > 0}>
708
+ <span class="text-amber-600">({data().validation.hallucinated.length} unverified)</span>
709
+ </Show>
710
+ </div>
711
+ </Show>
712
+
713
+ {/* Action buttons */}
714
+ <div class="flex flex-wrap gap-2">
715
+ <For each={data().actions as Array<{ label: string; value?: string; action?: string; variant?: string; icon?: string }>}>
716
+ {(item) => (
717
+ <button type="button" on:click={() => props.onAction?.(item.value || item.action || item.label, item)}
718
+ class={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${
719
+ item.variant === 'primary' ? 'bg-blue-600 text-white hover:bg-blue-700'
720
+ : item.variant === 'danger' ? 'bg-red-600 text-white hover:bg-red-700'
721
+ : item.variant === 'secondary' ? 'border border-blue-300 dark:border-blue-600 text-blue-700 dark:text-blue-300 hover:bg-blue-50 dark:hover:bg-blue-900/20'
722
+ : 'border border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
723
+ }`}>
724
+ <Show when={item.icon}><span class="mr-1">{item.icon}</span></Show>
725
+ {item.label}
726
+ </button>
727
+ )}
728
+ </For>
729
+ </div>
681
730
  </div>
682
731
  )
683
732
  }
@@ -737,18 +786,21 @@ const FeedbackSection: Component<{
737
786
  }> = (props) => {
738
787
  const [comment, setComment] = createSignal('')
739
788
  const [showComment, setShowComment] = createSignal(false)
789
+ const [submitted, setSubmitted] = createSignal<string | null>(null)
740
790
  const data = () => {
741
791
  const c = props.content as any
742
- // Support both formats: options array (universal) and approve/reject (simple)
743
792
  const options = c?.options || [
744
- { value: c?.approve?.value || 'approve', label: c?.approve?.label || 'Yes', icon: '👍', variant: 'primary' },
745
- { value: c?.reject?.value || 'reject', label: c?.reject?.label || 'No', icon: '👎' },
793
+ { value: c?.approve?.value || 'approve', label: c?.approve?.label || 'Yes', icon: '\uD83D\uDC4D', variant: 'primary' },
794
+ { value: c?.reject?.value || 'reject', label: c?.reject?.label || 'No', icon: '\uD83D\uDC4E' },
746
795
  ]
747
796
  return {
748
797
  question: c?.question || '',
749
798
  options: options as Array<{ value: string; label: string; icon?: string; variant?: string; needsComment?: boolean }>,
750
799
  allowFreeText: c?.allowFreeText ?? c?.allowComment ?? false,
751
800
  placeholder: c?.placeholder || c?.commentPlaceholder || 'Add a comment...',
801
+ // v4.1.0: per-step feedback
802
+ agentId: c?.agentId as string | undefined,
803
+ stepId: c?.stepId as string | undefined,
752
804
  }
753
805
  }
754
806
 
@@ -757,35 +809,65 @@ const FeedbackSection: Component<{
757
809
  setShowComment(true)
758
810
  return
759
811
  }
760
- props.onAction?.('feedback', { option: option.value, comment: comment() })
812
+ setSubmitted(option.value)
813
+ const payload = {
814
+ option: option.value,
815
+ comment: comment(),
816
+ ...(data().agentId ? { agentId: data().agentId } : {}),
817
+ ...(data().stepId ? { stepId: data().stepId } : {}),
818
+ }
819
+ console.info('[MCP-UI:HITL] user responded', {
820
+ agentId: data().agentId, stepId: data().stepId, action: option.value,
821
+ })
822
+ props.onAction?.('feedback', payload)
761
823
  }
762
824
 
763
825
  return (
764
826
  <div class="space-y-3">
765
827
  <p class="text-sm text-gray-700 dark:text-gray-300">{data().question}</p>
766
- <div class="flex flex-wrap gap-2">
767
- <For each={data().options}>
768
- {(option) => (
769
- <button type="button" onClick={() => handleOption(option)}
770
- class={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors flex items-center gap-1 ${
771
- option.variant === 'primary' ? 'bg-blue-600 text-white hover:bg-blue-700'
772
- : option.variant === 'danger' ? 'bg-red-600 text-white hover:bg-red-700'
773
- : 'border border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
774
- }`}>
775
- <Show when={option.icon}><span>{option.icon}</span></Show>
776
- {option.label}
777
- </button>
778
- )}
779
- </For>
780
- </div>
781
- <Show when={data().allowFreeText || showComment()}>
782
- <div class="flex gap-1">
783
- <input type="text" value={comment()} onInput={(e) => setComment(e.currentTarget.value)}
784
- placeholder={data().placeholder} autofocus={showComment()}
785
- class="flex-1 px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:border-blue-400 outline-none" />
786
- <button type="button" onClick={() => props.onAction?.('feedback', { option: 'comment', comment: comment() })}
787
- class="px-3 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700">Send</button>
828
+
829
+ {/* Already submitted — show micro-badge */}
830
+ <Show when={submitted()}>
831
+ <div class="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
832
+ <span classList={{
833
+ 'text-green-600': submitted() === 'approve',
834
+ 'text-red-600': submitted() === 'reject',
835
+ 'text-blue-600': submitted() !== 'approve' && submitted() !== 'reject',
836
+ }}>
837
+ {submitted() === 'approve' ? '\u2705' : submitted() === 'reject' ? '\u274C' : '\uD83D\uDCAC'} {submitted()}
838
+ </span>
839
+ <Show when={comment()}>
840
+ <span class="italic">&mdash; {comment()}</span>
841
+ </Show>
842
+ </div>
843
+ </Show>
844
+
845
+ {/* Buttons hidden after submit */}
846
+ <Show when={!submitted()}>
847
+ <div class="flex flex-wrap gap-2">
848
+ <For each={data().options}>
849
+ {(option) => (
850
+ <button type="button" on:click={() => handleOption(option)}
851
+ class={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors flex items-center gap-1 ${
852
+ option.variant === 'primary' ? 'bg-blue-600 text-white hover:bg-blue-700'
853
+ : option.variant === 'danger' ? 'bg-red-600 text-white hover:bg-red-700'
854
+ : 'border border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
855
+ }`}>
856
+ <Show when={option.icon}><span>{option.icon}</span></Show>
857
+ {option.label}
858
+ </button>
859
+ )}
860
+ </For>
788
861
  </div>
862
+ <Show when={data().allowFreeText || showComment()}>
863
+ <div class="flex gap-1">
864
+ <input type="text" value={comment()} onInput={(e) => setComment(e.currentTarget.value)}
865
+ placeholder={data().placeholder} autofocus={showComment()}
866
+ class="flex-1 px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:border-blue-400 outline-none" />
867
+ <button type="button" on:click={() => { setSubmitted('comment'); props.onAction?.('feedback', { option: 'comment', comment: comment(), agentId: data().agentId, stepId: data().stepId }) }}
868
+ class="px-3 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700">Send</button>
869
+ </div>
870
+ </Show>
789
871
  </Show>
790
872
  </div>
791
873
  )
@@ -0,0 +1,111 @@
1
+ /**
2
+ * SplitStepper — parallel agent steppers side by side
3
+ * v4.1.0: AITL sprint — 2-3 columns, synthesis row at bottom
4
+ *
5
+ * @experimental
6
+ */
7
+
8
+ import { For, Show } from 'solid-js'
9
+ import type { SplitStepperContent } from '../types/chat-bus'
10
+
11
+ export interface SplitStepperProps {
12
+ content: SplitStepperContent
13
+ }
14
+
15
+ const STEP_ICONS: Record<string, string> = {
16
+ done: '\u2705',
17
+ active: '\uD83D\uDD04',
18
+ pending: '\u23F3',
19
+ skipped: '\u23ED\uFE0F',
20
+ error: '\u274C',
21
+ }
22
+
23
+ const AGENT_STATUS_COLORS: Record<string, string> = {
24
+ done: 'border-green-400 dark:border-green-600',
25
+ active: 'border-blue-400 dark:border-blue-500',
26
+ pending: 'border-gray-300 dark:border-gray-600',
27
+ error: 'border-red-400 dark:border-red-600',
28
+ }
29
+
30
+ export function SplitStepper(props: SplitStepperProps) {
31
+ const c = () => props.content
32
+
33
+ if (typeof console !== 'undefined') {
34
+ console.info('[MCP-UI:SplitStepper] mounted', {
35
+ agents: c().agents.map(a => `${a.id}:${a.status}`),
36
+ synthesis: c().synthesis?.status,
37
+ })
38
+ }
39
+
40
+ return (
41
+ <div class="split-stepper">
42
+ {/* Agent columns */}
43
+ <div class="grid gap-3" style={{ "grid-template-columns": `repeat(${Math.min(c().agents.length, 3)}, 1fr)` }}>
44
+ <For each={c().agents}>
45
+ {(agent) => (
46
+ <div class={`rounded-lg border-2 ${AGENT_STATUS_COLORS[agent.status] || AGENT_STATUS_COLORS.pending} p-3`}>
47
+ {/* Agent header */}
48
+ <div class="flex items-center gap-2 mb-2">
49
+ <span class="font-medium text-sm text-gray-900 dark:text-white truncate">{agent.name}</span>
50
+ <span class={`w-2 h-2 rounded-full flex-shrink-0 ${
51
+ agent.status === 'done' ? 'bg-green-500' :
52
+ agent.status === 'active' ? 'bg-blue-500 animate-pulse' :
53
+ agent.status === 'error' ? 'bg-red-500' :
54
+ 'bg-gray-400'
55
+ }`} />
56
+ </div>
57
+
58
+ {/* Steps */}
59
+ <div class="space-y-1">
60
+ <For each={agent.steps}>
61
+ {(step) => (
62
+ <div class="flex items-center gap-2 text-xs">
63
+ <span class="flex-shrink-0 w-4 text-center">{STEP_ICONS[step.status] || STEP_ICONS.pending}</span>
64
+ <span classList={{
65
+ 'text-gray-900 dark:text-white font-medium': step.status === 'active',
66
+ 'text-gray-500 dark:text-gray-400': step.status !== 'active',
67
+ 'line-through opacity-50': step.status === 'skipped',
68
+ }}>
69
+ {step.label}
70
+ </span>
71
+ </div>
72
+ )}
73
+ </For>
74
+ </div>
75
+ </div>
76
+ )}
77
+ </For>
78
+ </div>
79
+
80
+ {/* Synthesis row */}
81
+ <Show when={c().synthesis}>
82
+ {(syn) => (
83
+ <div class={`mt-3 p-3 rounded-lg border-2 text-center ${
84
+ syn().status === 'done' ? 'border-green-400 dark:border-green-600 bg-green-50 dark:bg-green-900/10' :
85
+ syn().status === 'active' ? 'border-blue-400 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/10' :
86
+ 'border-dashed border-gray-300 dark:border-gray-600'
87
+ }`}>
88
+ <div class="flex items-center justify-center gap-2 text-sm">
89
+ <Show when={syn().status === 'active'}>
90
+ <div class="w-3 h-3 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
91
+ </Show>
92
+ <Show when={syn().status === 'done'}>
93
+ <span>{STEP_ICONS.done}</span>
94
+ </Show>
95
+ <Show when={syn().status === 'pending'}>
96
+ <span class="text-gray-400">{STEP_ICONS.pending}</span>
97
+ </Show>
98
+ <span classList={{
99
+ 'font-medium text-blue-700 dark:text-blue-300': syn().status === 'active',
100
+ 'font-medium text-green-700 dark:text-green-300': syn().status === 'done',
101
+ 'text-gray-500 dark:text-gray-400': syn().status === 'pending',
102
+ }}>
103
+ {syn().label}
104
+ </span>
105
+ </div>
106
+ </div>
107
+ )}
108
+ </Show>
109
+ </div>
110
+ )
111
+ }
@@ -64,6 +64,19 @@ export type { CodeBlockRendererProps } from './CodeBlockRenderer'
64
64
  export { MapRenderer } from './MapRenderer'
65
65
  export type { MapRendererProps } from './MapRenderer'
66
66
 
67
+ // Agent AITL (v4.1.0)
68
+ export { AgentCard, AgentStatusBadge } from './AgentCard'
69
+ export type { AgentCardProps, AgentStatusBadgeProps } from './AgentCard'
70
+
71
+ export { SplitStepper } from './SplitStepper'
72
+ export type { SplitStepperProps } from './SplitStepper'
73
+
74
+ export { AgentHandoff } from './AgentHandoff'
75
+ export type { AgentHandoffProps } from './AgentHandoff'
76
+
77
+ export { BriefingDiff } from './BriefingDiff'
78
+ export type { BriefingDiffProps } from './BriefingDiff'
79
+
67
80
  // Data Verification (v3.1.0 — anti-hallucination)
68
81
  export { VerifiedText } from './VerifiedText'
69
82
  export type { VerifiedTextProps } from './VerifiedText'
package/src/index.ts CHANGED
@@ -48,6 +48,12 @@ export { dispatchScratchpad, useScratchpadState } from './stores/scratchpad-stor
48
48
  export { VerifiedText } from './components/VerifiedText'
49
49
  export { DataPreviewSection } from './components/DataPreviewSection'
50
50
 
51
+ // Agent AITL Components (v4.1.0)
52
+ export { AgentCard, AgentStatusBadge } from './components/AgentCard'
53
+ export { SplitStepper } from './components/SplitStepper'
54
+ export { AgentHandoff } from './components/AgentHandoff'
55
+ export { BriefingDiff } from './components/BriefingDiff'
56
+
51
57
  // Autocomplete Components
52
58
  export { GhostText, GhostTextInput } from './components/GhostText'
53
59
  export { AutocompleteDropdown } from './components/AutocompleteDropdown'
@@ -68,6 +74,10 @@ export type { ChatPromptProps } from './components/ChatPrompt'
68
74
  export type { ScratchpadPanelProps } from './components/ScratchpadPanel'
69
75
  export type { VerifiedTextProps } from './components/VerifiedText'
70
76
  export type { DataPreviewSectionProps } from './components/DataPreviewSection'
77
+ export type { AgentCardProps, AgentStatusBadgeProps } from './components/AgentCard'
78
+ export type { SplitStepperProps } from './components/SplitStepper'
79
+ export type { AgentHandoffProps } from './components/AgentHandoff'
80
+ export type { BriefingDiffProps } from './components/BriefingDiff'
71
81
  export type { GhostTextProps, GhostTextInputProps } from './components/GhostText'
72
82
  export type { AutocompleteDropdownProps } from './components/AutocompleteDropdown'
73
83
  export type { AutocompleteFormFieldProps, AutocompleteFormFieldParams } from './components/AutocompleteFormField'
@@ -271,4 +281,9 @@ export type {
271
281
  DataPreviewColumn,
272
282
  DataPreviewContent,
273
283
  MapSectionContent,
284
+ // Agent AITL types (v4.1.0)
285
+ AgentCardContent,
286
+ SplitStepperContent,
287
+ AgentHandoffContent,
288
+ BriefingDiffContent,
274
289
  } from './types/chat-bus'