@webmcp-auto-ui/ui 2.5.25 → 2.5.26

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webmcp-auto-ui/ui",
3
- "version": "2.5.25",
3
+ "version": "2.5.26",
4
4
  "description": "Svelte 5 UI components — primitives, widgets, window manager",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "type": "module",
@@ -27,6 +27,7 @@
27
27
  prompt: '#6366f1',
28
28
  schema: '#06b6d4',
29
29
  warning: '#eab308',
30
+ recipe: '#ec4899',
30
31
  };
31
32
 
32
33
  function fmtTime(ts: number): string {
@@ -87,6 +88,12 @@
87
88
  <span class="ac-tag" class:ac-tag-recette={prov.tag === 'recette'} class:ac-tag-impro={prov.tag === 'impro'}>{prov.tag}</span>
88
89
  {/if}
89
90
  <span class="ac-detail">{prov.rest}</span>
91
+ {:else if log.type === 'recipe'}
92
+ {@const sepIdx = log.detail.indexOf(' · ')}
93
+ {@const rid = sepIdx > 0 ? log.detail.slice(0, sepIdx) : log.detail}
94
+ {@const rest = sepIdx > 0 ? log.detail.slice(sepIdx + 3) : ''}
95
+ <span class="ac-tag ac-tag-recipe">📄 {rid}</span>
96
+ <span class="ac-detail">{rest}</span>
90
97
  {:else}
91
98
  <span class="ac-detail">
92
99
  {log.detail}
@@ -243,6 +250,10 @@
243
250
  color: #fb923c;
244
251
  background: rgba(251, 146, 60, 0.1);
245
252
  }
253
+ .ac-tag-recipe {
254
+ color: #ec4899;
255
+ background: rgba(236, 72, 153, 0.1);
256
+ }
246
257
 
247
258
  .ac-clickable {
248
259
  cursor: pointer;
@@ -1,9 +1,39 @@
1
1
  <script lang="ts">
2
2
  import { fly } from 'svelte/transition';
3
+ import { renderMarkdown } from '../primitives/markdown-renderer.js';
3
4
 
4
5
  interface EphemeralMsg { id: string; role: 'user' | 'assistant'; html: string; }
5
6
  interface Props { ephemeral: EphemeralMsg[]; }
6
7
  let { ephemeral }: Props = $props();
8
+
9
+ // Detect if content has any markdown markers worth parsing.
10
+ // If not, we skip marked entirely and fall back to {@html} for the
11
+ // pre-existing HTML snippets (e.g. "<strong>tool_name</strong>").
12
+ const MD_RE = /(^|\n)\s*(#{1,6}\s|[-*+]\s|\d+\.\s|>\s)|\*\*|__|`|```|~~~|!\[|\[[^\]]+\]\(/;
13
+ function looksLikeMarkdown(src: string): boolean {
14
+ return MD_RE.test(src);
15
+ }
16
+
17
+ // Gracefully close dangling code fences during streaming so that
18
+ // a half-received ``` block still renders as code instead of
19
+ // swallowing the rest of the message.
20
+ function closeDanglingFences(src: string): string {
21
+ const fences = (src.match(/```/g) ?? []).length;
22
+ if (fences % 2 === 1) return src + '\n```';
23
+ const tildes = (src.match(/~~~/g) ?? []).length;
24
+ if (tildes % 2 === 1) return src + '\n~~~';
25
+ return src;
26
+ }
27
+
28
+ function renderContent(src: string): string {
29
+ if (!src) return '';
30
+ if (!looksLikeMarkdown(src)) return src;
31
+ try {
32
+ return renderMarkdown(closeDanglingFences(src));
33
+ } catch {
34
+ return src;
35
+ }
36
+ }
7
37
  </script>
8
38
 
9
39
  <div class="flex flex-col gap-2 items-start w-full">
@@ -13,7 +43,7 @@
13
43
  out:fly={{ y: -32, duration: 450, opacity: 0 }}
14
44
  class="ephemeral-msg {msg.role}"
15
45
  >
16
- {@html msg.html}
46
+ {@html renderContent(msg.html)}
17
47
  </div>
18
48
  {/each}
19
49
  </div>
@@ -41,4 +71,41 @@
41
71
  color: var(--color-text1);
42
72
  align-self: flex-start;
43
73
  }
74
+ /* Markdown tweaks scoped to the ephemeral bubble — keep margins tight. */
75
+ .ephemeral-msg :global(p) { margin: 0.25rem 0; }
76
+ .ephemeral-msg :global(p:first-child) { margin-top: 0; }
77
+ .ephemeral-msg :global(p:last-child) { margin-bottom: 0; }
78
+ .ephemeral-msg :global(h1),
79
+ .ephemeral-msg :global(h2),
80
+ .ephemeral-msg :global(h3),
81
+ .ephemeral-msg :global(h4) {
82
+ font-weight: 600;
83
+ margin: 0.35rem 0 0.25rem;
84
+ font-size: 0.78rem;
85
+ }
86
+ .ephemeral-msg :global(ul),
87
+ .ephemeral-msg :global(ol) {
88
+ margin: 0.3rem 0;
89
+ padding-left: 1.1rem;
90
+ }
91
+ .ephemeral-msg :global(li) { margin: 0.1rem 0; }
92
+ .ephemeral-msg :global(code) {
93
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
94
+ font-size: 0.66rem;
95
+ background: rgba(0, 0, 0, 0.28);
96
+ padding: 0.05rem 0.25rem;
97
+ border-radius: 0.2rem;
98
+ }
99
+ .ephemeral-msg :global(pre) {
100
+ background: rgba(0, 0, 0, 0.35);
101
+ border-radius: 0.3rem;
102
+ padding: 0.5rem;
103
+ margin: 0.35rem 0;
104
+ overflow-x: auto;
105
+ font-size: 0.66rem;
106
+ }
107
+ .ephemeral-msg :global(pre code) { background: transparent; padding: 0; }
108
+ .ephemeral-msg :global(strong) { font-weight: 600; }
109
+ .ephemeral-msg :global(em) { font-style: italic; }
110
+ .ephemeral-msg :global(a) { color: rgb(96, 165, 250); text-decoration: underline; }
44
111
  </style>
@@ -13,7 +13,8 @@
13
13
  onconnect?: () => void;
14
14
  ondisconnect?: () => void;
15
15
  class?: string;
16
- compact?: boolean; // hide token field
16
+ compact?: boolean; // hide token field entirely
17
+ showToken?: boolean; // toggle controlled from outside
17
18
  }
18
19
 
19
20
  let {
@@ -29,9 +30,9 @@
29
30
  ondisconnect,
30
31
  class: cls = '',
31
32
  compact = false,
33
+ showToken = $bindable(false),
32
34
  }: Props = $props();
33
35
 
34
- let showToken = $state(false);
35
36
  let debounceTimer: ReturnType<typeof setTimeout> | null = null;
36
37
 
37
38
  function handleUrlInput(e: Event) {
@@ -82,25 +83,14 @@
82
83
  <McpStatus {connecting} {connected} name={serverName || 'not connected'} />
83
84
  </div>
84
85
 
85
- {#if !compact}
86
- <div class="flex items-center gap-2">
87
- <button
88
- onclick={() => showToken = !showToken}
89
- class="text-text2 hover:text-text1 transition-colors flex-shrink-0"
90
- title="Bearer token"
91
- >
92
- <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>
93
- </button>
94
- {#if showToken}
95
- <input
96
- type="password"
97
- value={token}
98
- oninput={(e) => { token = (e.target as HTMLInputElement).value; onTokenChange?.(token); }}
99
- placeholder="Bearer token (optional)"
100
- class="flex-1 font-mono text-xs bg-surface2 border border-border2 rounded px-2 h-7 text-text1 outline-none placeholder:text-text2/40 focus:border-accent/50 transition-colors"
101
- />
102
- {/if}
103
- </div>
86
+ {#if !compact && showToken}
87
+ <input
88
+ type="password"
89
+ value={token}
90
+ oninput={(e) => { token = (e.target as HTMLInputElement).value; onTokenChange?.(token); }}
91
+ placeholder="Bearer token (optional)"
92
+ class="font-mono text-xs bg-surface2 border border-border2 rounded px-2 h-7 text-text1 outline-none placeholder:text-text2/40 focus:border-accent/50 transition-colors"
93
+ />
104
94
  {/if}
105
95
 
106
96
  {#if error}
@@ -6,8 +6,6 @@
6
6
  effectivePrompt?: string;
7
7
  maxTokens?: number;
8
8
  maxContextTokens?: number;
9
- maxTools?: number;
10
- maxMessages?: number;
11
9
  maxResultLength?: number;
12
10
  compressHistory?: boolean;
13
11
  compressPreview?: number;
@@ -26,8 +24,6 @@
26
24
  effectivePrompt = '',
27
25
  maxTokens = $bindable(4096),
28
26
  maxContextTokens = $bindable(150_000),
29
- maxTools = $bindable(8),
30
- maxMessages = $bindable(8),
31
27
  maxResultLength = $bindable(10000),
32
28
  compressHistory = $bindable(false),
33
29
  compressPreview = $bindable(500),
@@ -106,15 +102,15 @@
106
102
  <label class="text-[9px] font-mono text-text2 uppercase tracking-wider">System Prompt</label>
107
103
  <div class="flex items-center gap-2">
108
104
  {#if promptSaved && customMode}
109
- <span class="text-[9px] font-mono text-teal transition-opacity">✓ applique</span>
105
+ <span class="text-[9px] font-mono text-teal transition-opacity">✓ applied</span>
110
106
  {/if}
111
107
  {#if hasEffective}
112
108
  {#if customMode}
113
109
  <button class="text-[9px] font-mono text-accent2 hover:text-accent transition-colors"
114
- onclick={resetToAuto}>reinitialiser</button>
110
+ onclick={resetToAuto}>reset</button>
115
111
  {:else}
116
112
  <button class="text-[9px] font-mono text-accent hover:text-text1 transition-colors"
117
- onclick={enterCustomMode}>personnaliser</button>
113
+ onclick={enterCustomMode}>customize</button>
118
114
  {/if}
119
115
  {/if}
120
116
  </div>
@@ -125,15 +121,15 @@
125
121
  value={displayedPrompt}
126
122
  rows={8}
127
123
  class="w-full bg-surface2/50 border border-border2/50 rounded-lg px-3 py-2 text-xs font-mono text-text2 outline-none resize-none cursor-default"
128
- placeholder="Prompt auto-genere"
124
+ placeholder="Auto-generated prompt"
129
125
  ></textarea>
130
- <div class="text-[8px] font-mono text-text2/50">prompt auto-generecliquer personnaliser pour editer</div>
126
+ <div class="text-[8px] font-mono text-text2/50">auto-generated prompt click customize to edit</div>
131
127
  {:else}
132
128
  <textarea
133
129
  bind:value={systemPrompt}
134
130
  rows={5}
135
131
  class="w-full bg-surface2 border border-border2 rounded-lg px-3 py-2 text-xs font-mono text-text1 outline-none resize-none focus:border-accent/50 transition-colors placeholder:text-text2/40"
136
- placeholder="Instructions systeme pour l'agent…"
132
+ placeholder="System instructions for the agent…"
137
133
  ></textarea>
138
134
  {/if}
139
135
  </div>
@@ -186,28 +182,6 @@
186
182
  class="w-full accent-accent" />
187
183
  </div>
188
184
 
189
- <!-- Max tools (WASM only) -->
190
- {#if modelType === 'wasm'}
191
- <div>
192
- <div class="flex justify-between items-baseline mb-1">
193
- <span class="text-[9px] font-mono text-text2 uppercase tracking-wider">Max tools (WASM)</span>
194
- <span class="font-mono text-xs text-text1">{maxTools}</span>
195
- </div>
196
- <input type="range" bind:value={maxTools}
197
- min={4} max={20} step={1}
198
- class="w-full accent-accent" />
199
- </div>
200
- <div>
201
- <div class="flex justify-between items-baseline mb-1">
202
- <span class="text-[9px] font-mono text-text2 uppercase tracking-wider">Max messages (WASM)</span>
203
- <span class="font-mono text-xs text-text1">{maxMessages}</span>
204
- </div>
205
- <input type="range" bind:value={maxMessages}
206
- min={2} max={64} step={1}
207
- class="w-full accent-accent" />
208
- </div>
209
- {/if}
210
-
211
185
  <!-- Max result length -->
212
186
  <div>
213
187
  <div class="flex justify-between items-baseline mb-1">
@@ -223,7 +197,7 @@
223
197
  <div>
224
198
  <label class="flex items-center gap-2 cursor-pointer select-none mb-1">
225
199
  <input type="checkbox" bind:checked={compressHistory} class="accent-accent w-3.5 h-3.5" />
226
- <span class="text-[9px] font-mono text-text2 uppercase tracking-wider">Tronquer l'historique</span>
200
+ <span class="text-[9px] font-mono text-text2 uppercase tracking-wider">Truncate history</span>
227
201
  </label>
228
202
  {#if compressHistory}
229
203
  <div class="flex justify-between items-baseline mb-1">
@@ -248,7 +222,7 @@
248
222
  </div>
249
223
  <div class="pl-5">
250
224
  <div class="flex justify-between items-baseline mb-1">
251
- <span class="text-[9px] font-mono text-text2 uppercase tracking-wider">Résidu inline (chars)</span>
225
+ <span class="text-[9px] font-mono text-text2 uppercase tracking-wider">Inline residue (chars)</span>
252
226
  <span class="font-mono text-xs text-text1">{ragResidueSize}</span>
253
227
  </div>
254
228
  <input type="range" bind:value={ragResidueSize}
@@ -0,0 +1,84 @@
1
+ <script lang="ts">
2
+ interface Server {
3
+ id: string;
4
+ label: string;
5
+ description: string;
6
+ widgetCount: number;
7
+ }
8
+
9
+ interface Props {
10
+ servers: Server[];
11
+ enabledServers?: Set<string>;
12
+ onToggle?: (id: string) => void;
13
+ recipeCountByServer?: Record<string, number>;
14
+ toolCountByServer?: Record<string, number>;
15
+ onrecipeclick?: (id: string) => void;
16
+ ontoolclick?: (id: string) => void;
17
+ }
18
+
19
+ let {
20
+ servers,
21
+ enabledServers = new Set<string>(),
22
+ onToggle,
23
+ recipeCountByServer,
24
+ toolCountByServer,
25
+ onrecipeclick,
26
+ ontoolclick,
27
+ }: Props = $props();
28
+
29
+ let collapsed = $state(true);
30
+ </script>
31
+
32
+ <div class="flex flex-col gap-2">
33
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
34
+ <div class="flex items-center gap-1 cursor-pointer select-none"
35
+ onclick={() => collapsed = !collapsed}>
36
+ <span class="text-[9px] font-mono text-text2 uppercase tracking-wider">WebMCP servers</span>
37
+ <span class="text-[9px] text-text2/60 font-mono">({enabledServers.size}/{servers.length})</span>
38
+ <span class="text-[10px] text-text2 ml-auto transition-transform {collapsed ? '' : 'rotate-90'}">{@html '&#x25B6;'}</span>
39
+ </div>
40
+
41
+ {#if !collapsed}
42
+ <div class="flex flex-col gap-1">
43
+ {#each servers as srv (srv.id)}
44
+ {@const enabled = enabledServers.has(srv.id)}
45
+ {@const recipes = recipeCountByServer?.[srv.id] ?? 0}
46
+ {@const tools = toolCountByServer?.[srv.id] ?? 0}
47
+ <div class="group flex items-center gap-2 px-2 py-1.5 rounded border border-border2 bg-surface2 hover:border-accent/30 transition-colors">
48
+ <input
49
+ type="checkbox"
50
+ checked={enabled}
51
+ onchange={() => onToggle?.(srv.id)}
52
+ class="w-3.5 h-3.5 rounded border-border2 accent-accent cursor-pointer flex-shrink-0"
53
+ />
54
+ <div class="flex-1 min-w-0 flex flex-col">
55
+ <span class="font-mono text-xs font-medium text-text1 truncate">{srv.label}</span>
56
+ {#if srv.description}
57
+ <span class="text-[10px] text-text2 truncate">{srv.description}</span>
58
+ {/if}
59
+ {#if enabled && (recipes > 0 || tools > 0)}
60
+ <span class="flex items-center gap-1.5 mt-0.5">
61
+ {#if recipes > 0}
62
+ <button class="text-[10px] font-mono text-accent hover:underline"
63
+ onclick={(e) => { e.stopPropagation(); onrecipeclick?.(srv.id); }}>
64
+ {recipes} recipes
65
+ </button>
66
+ {/if}
67
+ {#if recipes > 0 && tools > 0}
68
+ <span class="text-[10px] text-text2">·</span>
69
+ {/if}
70
+ {#if tools > 0}
71
+ <button class="text-[10px] font-mono text-accent hover:underline"
72
+ onclick={(e) => { e.stopPropagation(); ontoolclick?.(srv.id); }}>
73
+ {tools} tools
74
+ </button>
75
+ {/if}
76
+ </span>
77
+ {/if}
78
+ </div>
79
+ <span class="text-[9px] font-mono text-text2/50 flex-shrink-0">{srv.widgetCount}w</span>
80
+ </div>
81
+ {/each}
82
+ </div>
83
+ {/if}
84
+ </div>
package/src/index.ts CHANGED
@@ -95,6 +95,7 @@ export type { ChatFeedItem, ChatBubble, ChatBlock } from './agent/ChatPanel.svel
95
95
  export { default as AgentConsole } from './agent/AgentConsole.svelte';
96
96
  export { default as SettingsPanel } from './agent/SettingsPanel.svelte';
97
97
  export { default as RemoteMCPserversDemo } from './agent/RemoteMCPserversDemo.svelte';
98
+ export { default as WebMCPserversList } from './agent/WebMCPserversList.svelte';
98
99
  export { default as EphemeralBubble } from './agent/EphemeralBubble.svelte';
99
100
  export { default as TokenBubble } from './agent/TokenBubble.svelte';
100
101
  export { default as DiagnosticModal } from './agent/DiagnosticModal.svelte';