@webmcp-auto-ui/ui 2.5.26 → 2.5.27

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/README.md CHANGED
@@ -25,9 +25,9 @@ Higher-level components with more complex data shapes and interactivity.
25
25
  Layout containers for multi-pane interfaces. `TilingLayout` uses a Fibonacci spiral. `FlexLayout` provides an auto-grid layout with a size slider for adjusting block dimensions. `FloatingLayout` supports collapse/expand (double-click) and a fit-to-content button.
26
26
 
27
27
  ### Agent UI widgets
28
- `GemmaLoader` · `TokenBubble` · `EphemeralBubble` · `RemoteMCPserversDemo` · `SettingsPanel`
28
+ `ModelLoader` · `TokenBubble` · `EphemeralBubble` · `RemoteMCPserversDemo` · `SettingsPanel`
29
29
 
30
- `GemmaLoader` — floating overlay with progress stream, auto-collapses to a pill once model is loaded. `TokenBubble` — real-time metrics display (req/min, input tokens/min, output tokens/min, cached tokens). `EphemeralBubble` — transient notification bubble (moved from app to package). `RemoteMCPserversDemo` — MCP server discovery component listing available demo servers. `SettingsPanel` — sliders with dynamic ranges for temperature, topK, and maxTokens controls.
30
+ `ModelLoader` — floating overlay with progress stream, auto-collapses to a pill once model is loaded. `TokenBubble` — real-time metrics display (req/min, input tokens/min, output tokens/min, cached tokens). `EphemeralBubble` — transient notification bubble (moved from app to package). `RemoteMCPserversDemo` — MCP server discovery component listing available demo servers. `SettingsPanel` — sliders with dynamic ranges for temperature, topK, and maxTokens controls.
31
31
 
32
32
  ### WidgetRenderer
33
33
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webmcp-auto-ui/ui",
3
- "version": "2.5.26",
3
+ "version": "2.5.27",
4
4
  "description": "Svelte 5 UI components — primitives, widgets, window manager",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "type": "module",
@@ -48,12 +48,6 @@
48
48
  setTimeout(() => { copyLabel = 'copy'; }, 2000);
49
49
  }
50
50
 
51
- /** Extract provenance tag from tool log detail */
52
- function parseProvenance(detail: string): { tag: 'recette' | 'impro' | null; rest: string } {
53
- if (detail.startsWith('[recette] ')) return { tag: 'recette', rest: detail.slice(10) };
54
- if (detail.startsWith('[impro] ')) return { tag: 'impro', rest: detail.slice(8) };
55
- return { tag: null, rest: detail };
56
- }
57
51
  </script>
58
52
 
59
53
  <div class="agent-console {cls}">
@@ -82,13 +76,7 @@
82
76
  tabindex={log.detail.length > 80 ? 0 : undefined}>
83
77
  <span class="ac-ts">{fmtTime(log.ts)}</span>
84
78
  <span class="ac-type" style="color:{typeColor[log.type] ?? 'var(--color-text2, #888)'}">{log.type}</span>
85
- {#if log.type === 'tool'}
86
- {@const prov = parseProvenance(log.detail)}
87
- {#if prov.tag}
88
- <span class="ac-tag" class:ac-tag-recette={prov.tag === 'recette'} class:ac-tag-impro={prov.tag === 'impro'}>{prov.tag}</span>
89
- {/if}
90
- <span class="ac-detail">{prov.rest}</span>
91
- {:else if log.type === 'recipe'}
79
+ {#if log.type === 'recipe'}
92
80
  {@const sepIdx = log.detail.indexOf(' · ')}
93
81
  {@const rid = sepIdx > 0 ? log.detail.slice(0, sepIdx) : log.detail}
94
82
  {@const rest = sepIdx > 0 ? log.detail.slice(sepIdx + 3) : ''}
@@ -242,14 +230,6 @@
242
230
  border-radius: 2px;
243
231
  line-height: 1.6;
244
232
  }
245
- .ac-tag-recette {
246
- color: #4ade80;
247
- background: rgba(74, 222, 128, 0.1);
248
- }
249
- .ac-tag-impro {
250
- color: #fb923c;
251
- background: rgba(251, 146, 60, 0.1);
252
- }
253
233
  .ac-tag-recipe {
254
234
  color: #ec4899;
255
235
  background: rgba(236, 72, 153, 0.1);
@@ -1,20 +1,30 @@
1
1
  <script lang="ts">
2
+ import { listTransformersModels } from '@webmcp-auto-ui/agent';
3
+
2
4
  export interface ModelOption {
3
5
  value: string;
4
6
  label: string;
5
- group: 'remote' | 'wasm' | 'local';
7
+ group: 'remote' | 'wasm' | 'transformers' | 'local';
6
8
  }
7
9
 
10
+ const transformersOptions: ModelOption[] = listTransformersModels().map(({ id, entry }) => ({
11
+ value: id,
12
+ label: entry.label,
13
+ group: 'transformers' as const,
14
+ }));
15
+
8
16
  const DEFAULT_MODELS: ModelOption[] = [
9
- { value: 'haiku', label: 'claude-haiku-4-5', group: 'remote' },
10
- { value: 'gemma-e2b', label: 'Gemma E2B (WASM)', group: 'wasm' },
11
- { value: 'gemma-e4b', label: 'Gemma E4B (WASM)', group: 'wasm' },
17
+ { value: 'haiku', label: 'claude-haiku-4-5', group: 'remote' },
18
+ { value: 'gemma-e2b', label: 'Gemma E2B (MediaPipe)', group: 'wasm' },
19
+ { value: 'gemma-e4b', label: 'Gemma E4B (MediaPipe)', group: 'wasm' },
20
+ ...transformersOptions,
12
21
  ];
13
22
 
14
23
  const GROUP_LABELS: Record<string, string> = {
15
- remote: 'Remote',
16
- wasm: 'In-Browser (WASM)',
17
- local: 'Local',
24
+ remote: 'Remote',
25
+ wasm: 'In-Browser (MediaPipe)',
26
+ transformers: 'In-Browser (Transformers.js)',
27
+ local: 'Local',
18
28
  };
19
29
 
20
30
  interface Props {
@@ -26,7 +36,7 @@
26
36
  let { value, onchange, models = DEFAULT_MODELS, class: cls = '' }: Props = $props();
27
37
 
28
38
  const groups = $derived(
29
- ['remote', 'wasm', 'local'].filter(g => models.some(m => m.group === g))
39
+ ['remote', 'wasm', 'transformers', 'local'].filter(g => models.some(m => m.group === g))
30
40
  );
31
41
  </script>
32
42
 
@@ -19,7 +19,7 @@
19
19
  elapsed,
20
20
  loadedMB = 0,
21
21
  totalMB = 0,
22
- modelName = 'Gemma E2B',
22
+ modelName = 'modèle',
23
23
  error = '',
24
24
  fromCache = false,
25
25
  onunload,
@@ -11,6 +11,7 @@
11
11
  compressPreview?: number;
12
12
  contextRAGEnabled?: boolean;
13
13
  ragResidueSize?: number;
14
+ visualTrace?: boolean;
14
15
  cacheEnabled?: boolean;
15
16
  temperature?: number;
16
17
  topK?: number;
@@ -29,6 +30,7 @@
29
30
  compressPreview = $bindable(500),
30
31
  contextRAGEnabled = $bindable(false),
31
32
  ragResidueSize = $bindable(200),
33
+ visualTrace = $bindable(false),
32
34
  cacheEnabled = $bindable(true),
33
35
  temperature = $bindable(0.7),
34
36
  topK = $bindable(10),
@@ -186,10 +188,10 @@
186
188
  <div>
187
189
  <div class="flex justify-between items-baseline mb-1">
188
190
  <span class="text-[9px] font-mono text-text2 uppercase tracking-wider">Max result (chars)</span>
189
- <span class="font-mono text-xs text-text1">{formatNumber(maxResultLength)}</span>
191
+ <span class="font-mono text-xs text-text1">{maxResultLength >= 50000 ? '∞' : formatNumber(maxResultLength)}</span>
190
192
  </div>
191
193
  <input type="range" bind:value={maxResultLength}
192
- min={500} max={20000} step={500}
194
+ min={500} max={50000} step={500}
193
195
  class="w-full accent-accent" />
194
196
  </div>
195
197
 
@@ -230,6 +232,18 @@
230
232
  class="w-full accent-accent" />
231
233
  </div>
232
234
  {/if}
235
+
236
+ <!-- Visual trace -->
237
+ <label class="flex items-center gap-2.5 cursor-pointer select-none">
238
+ <input type="checkbox" bind:checked={visualTrace} class="accent-accent w-3.5 h-3.5" />
239
+ <span class="text-xs font-mono text-text1">Visual trace</span>
240
+ <span class="text-[8px] font-mono text-text2/40 ml-auto">experimental</span>
241
+ </label>
242
+ {#if visualTrace}
243
+ <div class="text-[9px] font-mono text-text2/60 pl-5 mb-2">
244
+ Live DAG of agent execution
245
+ </div>
246
+ {/if}
233
247
  </section>
234
248
 
235
249
  <!-- Cache (disabled for WASM/Gemma — prompt caching is provider-dependent) -->
package/src/index.ts CHANGED
@@ -86,7 +86,9 @@ export type { BusMessage } from './messaging/bus.svelte.js';
86
86
 
87
87
  // Agent UI components
88
88
  export { default as LLMSelector } from './agent/LLMSelector.svelte';
89
- export { default as GemmaLoader } from './agent/GemmaLoader.svelte';
89
+ export { default as ModelLoader } from './agent/ModelLoader.svelte';
90
+ /** @deprecated Use ModelLoader instead. Alias maintained for backward compatibility. */
91
+ export { default as GemmaLoader } from './agent/ModelLoader.svelte';
90
92
  export { default as McpStatus } from './agent/McpStatus.svelte';
91
93
  export { default as AgentProgress } from './agent/AgentProgress.svelte';
92
94
  export { default as McpConnector } from './agent/McpConnector.svelte';
@@ -249,22 +249,15 @@
249
249
  {/if}
250
250
 
251
251
  <style>
252
- .vanilla-container :global(svg) {
252
+ .vanilla-container > :global(svg),
253
+ .vanilla-container > :global(canvas),
254
+ .vanilla-container > :global(img) {
253
255
  width: 100%;
254
256
  height: auto;
255
257
  max-height: 100%;
256
258
  display: block;
257
259
  }
258
- .vanilla-container :global(canvas) {
259
- width: 100%;
260
- height: auto;
261
- max-height: 100%;
262
- display: block;
263
- }
264
- .vanilla-container :global(img) {
265
- width: 100%;
266
- height: auto;
267
- max-height: 100%;
260
+ .vanilla-container > :global(img) {
268
261
  object-fit: contain;
269
262
  }
270
263
  </style>
@@ -227,6 +227,11 @@
227
227
  const w = width || 400;
228
228
  const h = Math.max(250, Math.round(w * 0.65));
229
229
 
230
+ // Truncate labels so they stay inside the viewBox. Hover <title> shows the full label.
231
+ const maxChars = Math.max(10, Math.floor(w / 120));
232
+ const truncate = (text: string): string =>
233
+ text.length > maxChars ? text.slice(0, Math.max(1, maxChars - 1)) + '…' : text;
234
+
230
235
  const accent = cssVar('--color-accent', '#6c5ce7');
231
236
  const accent2 = cssVar('--color-accent2', '#e17055');
232
237
  const groups = Array.from(new Set(nodes.map((n) => n.group ?? 0)));
@@ -295,9 +300,9 @@
295
300
  .attr('y', 4)
296
301
  .attr('font-size', '10px')
297
302
  .attr('fill', 'var(--color-text1, #111)')
298
- .text((n) => n.label ?? n.id);
303
+ .text((n) => truncate(String(n.label ?? n.id)));
299
304
 
300
- node.append('title').text((n) => n.label ?? n.id);
305
+ node.append('title').text((n) => String(n.label ?? n.id));
301
306
 
302
307
  sim.on('tick', () => {
303
308
  link
@@ -1,16 +1,24 @@
1
1
  <script lang="ts">
2
- export interface SankeyNode { id: string; label: string; color?: string; }
2
+ export interface SankeyNode { id: string; label: string; color?: string; summary?: string; }
3
3
  export interface SankeyLink { source: string; target: string; value: number; label?: string; }
4
4
  export interface SankeySpec { title?: string; nodes?: SankeyNode[]; links?: SankeyLink[]; }
5
5
  interface Props { spec: Partial<SankeySpec>; onnodeclick?: (n: SankeyNode) => void; onlinkclick?: (l: SankeyLink) => void; }
6
6
  let { spec, onnodeclick, onlinkclick }: Props = $props();
7
+ let host: HTMLElement | undefined = $state();
7
8
  const nodes=$derived<SankeyNode[]>(Array.isArray(spec.nodes)?spec.nodes:[]);
8
9
  const links=$derived<SankeyLink[]>(Array.isArray(spec.links)?spec.links:[]);
9
10
  const nodeMap=$derived(new Map(nodes.map(n=>[n.id,n])));
10
11
  const maxVal=$derived(Math.max(...links.map(l=>l.value),1));
11
12
  const sorted=$derived([...links].sort((a,b)=>b.value-a.value));
13
+ function dispatchNodeDblclick(node: SankeyNode | undefined) {
14
+ if (!node || !host) return;
15
+ host.dispatchEvent(new CustomEvent('widget:node-dblclick', {
16
+ detail: { nodeId: node.id, nodeData: node },
17
+ bubbles: true,
18
+ }));
19
+ }
12
20
  </script>
13
- <div class="bg-surface border border-border rounded-lg p-3 md:p-4 font-sans">
21
+ <div bind:this={host} class="bg-surface border border-border rounded-lg p-3 md:p-4 font-sans">
14
22
  {#if spec.title}<h3 class="text-sm font-semibold text-text1 mb-3">{spec.title}</h3>{/if}
15
23
  {#if !nodes.length||!links.length}<p class="text-text2 text-sm">No flow data</p>
16
24
  {:else}
@@ -25,11 +33,21 @@
25
33
  <!-- svelte-ignore a11y_click_events_have_key_events -->
26
34
  <!-- svelte-ignore a11y_no_static_element_interactions -->
27
35
  <div class="flex items-center gap-2 text-xs cursor-pointer hover:opacity-80 transition-opacity" role="button" tabindex="0" onclick={()=>onlinkclick?.(link)} onkeydown={(e)=>{if(e.key==='Enter')onlinkclick?.(link)}}>
28
- <span class="text-text2 min-w-[80px] truncate font-mono" style="color:{sc};">{src?.label??link.source}</span>
36
+ <span
37
+ class="text-text2 min-w-[80px] truncate font-mono"
38
+ style="color:{sc};"
39
+ title={src?.summary ?? src?.label ?? link.source}
40
+ ondblclick={(e)=>{e.stopPropagation(); dispatchNodeDblclick(src); onnodeclick?.(src!);}}
41
+ >{src?.label??link.source}</span>
29
42
  <div class="flex-1 bg-surface2 rounded-full overflow-hidden" style="height:{barH}px;">
30
43
  <div class="rounded-full h-full" style="width:{pct}%;background:linear-gradient(to right,{sc},{tc});"></div>
31
44
  </div>
32
- <span class="text-text2 min-w-[80px] truncate text-right font-mono" style="color:{tc};">{tgt?.label??link.target}</span>
45
+ <span
46
+ class="text-text2 min-w-[80px] truncate text-right font-mono"
47
+ style="color:{tc};"
48
+ title={tgt?.summary ?? tgt?.label ?? link.target}
49
+ ondblclick={(e)=>{e.stopPropagation(); dispatchNodeDblclick(tgt); onnodeclick?.(tgt!);}}
50
+ >{tgt?.label??link.target}</span>
33
51
  <span class="text-text2 min-w-[40px] text-right font-mono">{link.value}</span>
34
52
  </div>
35
53
  {/each}
@@ -125,6 +125,7 @@
125
125
 
126
126
  function resizeStart(id: string, e: MouseEvent) {
127
127
  e.stopPropagation();
128
+ e.preventDefault();
128
129
  rdrag = id; rsx = e.clientX; rsy = e.clientY;
129
130
  const lw = lmap.get(id); rww = lw?.width ?? defaultWidth; rwh = lw?.height ?? defaultHeight;
130
131
  focus(id);
@@ -181,6 +182,7 @@
181
182
  <!-- ── Desktop: scrollable floating canvas ─────────────────────────────── -->
182
183
  <!-- svelte-ignore a11y_no_static_element_interactions -->
183
184
  <div class="absolute inset-0 overflow-auto"
185
+ class:select-none={drag !== null || rdrag !== null}
184
186
  onmousemove={mm} onmouseup={mu} onmouseleave={mu}>
185
187
  <div class="relative" style="width:{canvasW}px;height:{canvasH}px;">
186
188
  {#each windows as win (win.id)}
@@ -8,8 +8,10 @@
8
8
  * Usage: place inside a title bar div. The component renders inline
9
9
  * (flex items) and is invisible when the widget has no links.
10
10
  *
11
- * Requires the bus to expose hasLinks/getLinks/getGroup (added by another agent).
12
- * Gracefully degrades if those methods don't exist yet.
11
+ * Consumes the FONC bus link API: `bus.hasLinks(busId)` and
12
+ * `bus.getLinks(busId)` (see packages/ui/src/messaging/bus.svelte.ts).
13
+ * `getLinks` returns `string[]` of group IDs; the first one drives the
14
+ * indicator color.
13
15
  */
14
16
  import { bus } from '../messaging/bus.svelte.js';
15
17
  import { groupColor } from './link-utils.js';
@@ -20,22 +22,13 @@
20
22
  }
21
23
  let { busId }: Props = $props();
22
24
 
23
- // ── Reactive link state (safe if bus methods don't exist yet) ──────
24
- const linked = $derived(
25
- typeof (bus as any).hasLinks === 'function' ? (bus as any).hasLinks(busId) : false
26
- );
25
+ // ── Reactive link state ────────────────────────────────────────────
26
+ const linked = $derived(bus.hasLinks(busId));
27
27
 
28
- const links = $derived(
29
- typeof (bus as any).getLinks === 'function' ? (bus as any).getLinks(busId) : []
30
- );
28
+ const links = $derived(bus.getLinks(busId));
31
29
 
32
30
  /** First group ID (a widget may belong to multiple groups) */
33
- const groupId = $derived.by((): string | null => {
34
- if (!Array.isArray(links) || links.length === 0) return null;
35
- // Each link has a groupId property
36
- const first = links[0];
37
- return typeof first === 'object' && first?.groupId ? String(first.groupId) : null;
38
- });
31
+ const groupId = $derived(links.length > 0 ? links[0] : null);
39
32
 
40
33
  const color = $derived(groupId ? groupColor(groupId) : 'transparent');
41
34