@webmcp-auto-ui/ui 2.5.25 → 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 +2 -2
- package/package.json +1 -1
- package/src/agent/AgentConsole.svelte +10 -19
- package/src/agent/EphemeralBubble.svelte +68 -1
- package/src/agent/LLMSelector.svelte +18 -8
- package/src/agent/McpConnector.svelte +11 -21
- package/src/agent/{GemmaLoader.svelte → ModelLoader.svelte} +1 -1
- package/src/agent/SettingsPanel.svelte +24 -36
- package/src/agent/WebMCPserversList.svelte +84 -0
- package/src/index.ts +4 -1
- package/src/widgets/WidgetRenderer.svelte +4 -11
- package/src/widgets/rich/D3Widget.svelte +7 -2
- package/src/widgets/rich/Sankey.svelte +22 -4
- package/src/wm/FloatingLayout.svelte +2 -0
- package/src/wm/LinkIndicators.svelte +8 -15
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
|
-
`
|
|
28
|
+
`ModelLoader` · `TokenBubble` · `EphemeralBubble` · `RemoteMCPserversDemo` · `SettingsPanel`
|
|
29
29
|
|
|
30
|
-
`
|
|
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
|
@@ -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 {
|
|
@@ -47,12 +48,6 @@
|
|
|
47
48
|
setTimeout(() => { copyLabel = 'copy'; }, 2000);
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
/** Extract provenance tag from tool log detail */
|
|
51
|
-
function parseProvenance(detail: string): { tag: 'recette' | 'impro' | null; rest: string } {
|
|
52
|
-
if (detail.startsWith('[recette] ')) return { tag: 'recette', rest: detail.slice(10) };
|
|
53
|
-
if (detail.startsWith('[impro] ')) return { tag: 'impro', rest: detail.slice(8) };
|
|
54
|
-
return { tag: null, rest: detail };
|
|
55
|
-
}
|
|
56
51
|
</script>
|
|
57
52
|
|
|
58
53
|
<div class="agent-console {cls}">
|
|
@@ -81,12 +76,12 @@
|
|
|
81
76
|
tabindex={log.detail.length > 80 ? 0 : undefined}>
|
|
82
77
|
<span class="ac-ts">{fmtTime(log.ts)}</span>
|
|
83
78
|
<span class="ac-type" style="color:{typeColor[log.type] ?? 'var(--color-text2, #888)'}">{log.type}</span>
|
|
84
|
-
{#if log.type === '
|
|
85
|
-
{@const
|
|
86
|
-
{
|
|
87
|
-
|
|
88
|
-
{
|
|
89
|
-
<span class="ac-detail">{
|
|
79
|
+
{#if log.type === 'recipe'}
|
|
80
|
+
{@const sepIdx = log.detail.indexOf(' · ')}
|
|
81
|
+
{@const rid = sepIdx > 0 ? log.detail.slice(0, sepIdx) : log.detail}
|
|
82
|
+
{@const rest = sepIdx > 0 ? log.detail.slice(sepIdx + 3) : ''}
|
|
83
|
+
<span class="ac-tag ac-tag-recipe">📄 {rid}</span>
|
|
84
|
+
<span class="ac-detail">{rest}</span>
|
|
90
85
|
{:else}
|
|
91
86
|
<span class="ac-detail">
|
|
92
87
|
{log.detail}
|
|
@@ -235,13 +230,9 @@
|
|
|
235
230
|
border-radius: 2px;
|
|
236
231
|
line-height: 1.6;
|
|
237
232
|
}
|
|
238
|
-
.ac-tag-
|
|
239
|
-
color: #
|
|
240
|
-
background: rgba(
|
|
241
|
-
}
|
|
242
|
-
.ac-tag-impro {
|
|
243
|
-
color: #fb923c;
|
|
244
|
-
background: rgba(251, 146, 60, 0.1);
|
|
233
|
+
.ac-tag-recipe {
|
|
234
|
+
color: #ec4899;
|
|
235
|
+
background: rgba(236, 72, 153, 0.1);
|
|
245
236
|
}
|
|
246
237
|
|
|
247
238
|
.ac-clickable {
|
|
@@ -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>
|
|
@@ -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',
|
|
10
|
-
{ value: 'gemma-e2b',
|
|
11
|
-
{ value: 'gemma-e4b',
|
|
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:
|
|
16
|
-
wasm:
|
|
17
|
-
|
|
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
|
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
onconnect?: () => void;
|
|
14
14
|
ondisconnect?: () => void;
|
|
15
15
|
class?: string;
|
|
16
|
-
compact?: boolean;
|
|
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
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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,13 +6,12 @@
|
|
|
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;
|
|
14
12
|
contextRAGEnabled?: boolean;
|
|
15
13
|
ragResidueSize?: number;
|
|
14
|
+
visualTrace?: boolean;
|
|
16
15
|
cacheEnabled?: boolean;
|
|
17
16
|
temperature?: number;
|
|
18
17
|
topK?: number;
|
|
@@ -26,13 +25,12 @@
|
|
|
26
25
|
effectivePrompt = '',
|
|
27
26
|
maxTokens = $bindable(4096),
|
|
28
27
|
maxContextTokens = $bindable(150_000),
|
|
29
|
-
maxTools = $bindable(8),
|
|
30
|
-
maxMessages = $bindable(8),
|
|
31
28
|
maxResultLength = $bindable(10000),
|
|
32
29
|
compressHistory = $bindable(false),
|
|
33
30
|
compressPreview = $bindable(500),
|
|
34
31
|
contextRAGEnabled = $bindable(false),
|
|
35
32
|
ragResidueSize = $bindable(200),
|
|
33
|
+
visualTrace = $bindable(false),
|
|
36
34
|
cacheEnabled = $bindable(true),
|
|
37
35
|
temperature = $bindable(0.7),
|
|
38
36
|
topK = $bindable(10),
|
|
@@ -106,15 +104,15 @@
|
|
|
106
104
|
<label class="text-[9px] font-mono text-text2 uppercase tracking-wider">System Prompt</label>
|
|
107
105
|
<div class="flex items-center gap-2">
|
|
108
106
|
{#if promptSaved && customMode}
|
|
109
|
-
<span class="text-[9px] font-mono text-teal transition-opacity">✓
|
|
107
|
+
<span class="text-[9px] font-mono text-teal transition-opacity">✓ applied</span>
|
|
110
108
|
{/if}
|
|
111
109
|
{#if hasEffective}
|
|
112
110
|
{#if customMode}
|
|
113
111
|
<button class="text-[9px] font-mono text-accent2 hover:text-accent transition-colors"
|
|
114
|
-
onclick={resetToAuto}>
|
|
112
|
+
onclick={resetToAuto}>reset</button>
|
|
115
113
|
{:else}
|
|
116
114
|
<button class="text-[9px] font-mono text-accent hover:text-text1 transition-colors"
|
|
117
|
-
onclick={enterCustomMode}>
|
|
115
|
+
onclick={enterCustomMode}>customize</button>
|
|
118
116
|
{/if}
|
|
119
117
|
{/if}
|
|
120
118
|
</div>
|
|
@@ -125,15 +123,15 @@
|
|
|
125
123
|
value={displayedPrompt}
|
|
126
124
|
rows={8}
|
|
127
125
|
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="
|
|
126
|
+
placeholder="Auto-generated prompt"
|
|
129
127
|
></textarea>
|
|
130
|
-
<div class="text-[8px] font-mono text-text2/50">
|
|
128
|
+
<div class="text-[8px] font-mono text-text2/50">auto-generated prompt — click customize to edit</div>
|
|
131
129
|
{:else}
|
|
132
130
|
<textarea
|
|
133
131
|
bind:value={systemPrompt}
|
|
134
132
|
rows={5}
|
|
135
133
|
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="
|
|
134
|
+
placeholder="System instructions for the agent…"
|
|
137
135
|
></textarea>
|
|
138
136
|
{/if}
|
|
139
137
|
</div>
|
|
@@ -186,36 +184,14 @@
|
|
|
186
184
|
class="w-full accent-accent" />
|
|
187
185
|
</div>
|
|
188
186
|
|
|
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
187
|
<!-- Max result length -->
|
|
212
188
|
<div>
|
|
213
189
|
<div class="flex justify-between items-baseline mb-1">
|
|
214
190
|
<span class="text-[9px] font-mono text-text2 uppercase tracking-wider">Max result (chars)</span>
|
|
215
|
-
<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>
|
|
216
192
|
</div>
|
|
217
193
|
<input type="range" bind:value={maxResultLength}
|
|
218
|
-
min={500} max={
|
|
194
|
+
min={500} max={50000} step={500}
|
|
219
195
|
class="w-full accent-accent" />
|
|
220
196
|
</div>
|
|
221
197
|
|
|
@@ -223,7 +199,7 @@
|
|
|
223
199
|
<div>
|
|
224
200
|
<label class="flex items-center gap-2 cursor-pointer select-none mb-1">
|
|
225
201
|
<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">
|
|
202
|
+
<span class="text-[9px] font-mono text-text2 uppercase tracking-wider">Truncate history</span>
|
|
227
203
|
</label>
|
|
228
204
|
{#if compressHistory}
|
|
229
205
|
<div class="flex justify-between items-baseline mb-1">
|
|
@@ -248,7 +224,7 @@
|
|
|
248
224
|
</div>
|
|
249
225
|
<div class="pl-5">
|
|
250
226
|
<div class="flex justify-between items-baseline mb-1">
|
|
251
|
-
<span class="text-[9px] font-mono text-text2 uppercase tracking-wider">
|
|
227
|
+
<span class="text-[9px] font-mono text-text2 uppercase tracking-wider">Inline residue (chars)</span>
|
|
252
228
|
<span class="font-mono text-xs text-text1">{ragResidueSize}</span>
|
|
253
229
|
</div>
|
|
254
230
|
<input type="range" bind:value={ragResidueSize}
|
|
@@ -256,6 +232,18 @@
|
|
|
256
232
|
class="w-full accent-accent" />
|
|
257
233
|
</div>
|
|
258
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}
|
|
259
247
|
</section>
|
|
260
248
|
|
|
261
249
|
<!-- Cache (disabled for WASM/Gemma — prompt caching is provider-dependent) -->
|
|
@@ -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 '▶'}</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
|
@@ -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
|
|
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';
|
|
@@ -95,6 +97,7 @@ export type { ChatFeedItem, ChatBubble, ChatBlock } from './agent/ChatPanel.svel
|
|
|
95
97
|
export { default as AgentConsole } from './agent/AgentConsole.svelte';
|
|
96
98
|
export { default as SettingsPanel } from './agent/SettingsPanel.svelte';
|
|
97
99
|
export { default as RemoteMCPserversDemo } from './agent/RemoteMCPserversDemo.svelte';
|
|
100
|
+
export { default as WebMCPserversList } from './agent/WebMCPserversList.svelte';
|
|
98
101
|
export { default as EphemeralBubble } from './agent/EphemeralBubble.svelte';
|
|
99
102
|
export { default as TokenBubble } from './agent/TokenBubble.svelte';
|
|
100
103
|
export { default as DiagnosticModal } from './agent/DiagnosticModal.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(
|
|
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
|
|
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
|
|
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
|
-
*
|
|
12
|
-
*
|
|
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
|
|
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.
|
|
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
|
|