|
29 | 29 | import * as Table from '$lib/components/ui/table/index.js'; |
30 | 30 |
|
31 | 31 | import { cn } from '$lib/utils'; |
32 | | - import { idAsKey, type PublicRegistryAgent } from '$lib/threads'; |
| 32 | + import { idAsKey, type PublicRegistryAgent, type Registry } from '$lib/threads'; |
33 | 33 | import { Session } from '$lib/session.svelte'; |
34 | 34 | import { tools } from '$lib/mcptools'; |
35 | 35 |
|
|
69 | 69 | import CopyButton from '$lib/components/copy-button.svelte'; |
70 | 70 | import Pip from '$lib/components/pip.svelte'; |
71 | 71 | import OptionField from './OptionField.svelte'; |
| 72 | + import { page } from '$app/state'; |
| 73 | +
|
| 74 | + function sourceToRegistryId(source: AgentSource): RegistryAgentIdentifier['registrySourceId'] { |
| 75 | + switch (source) { |
| 76 | + case 'local': |
| 77 | + return { type: 'local' }; |
| 78 | +
|
| 79 | + case 'marketplace': |
| 80 | + return { type: 'marketplace' }; |
| 81 | +
|
| 82 | + case 'linked': |
| 83 | + return { type: 'linked', linkedServerId: 'default' }; |
| 84 | + } |
| 85 | + } |
| 86 | +
|
| 87 | + const addAgent = async (name: string, source: any, version: string) => { |
| 88 | + try { |
| 89 | + const existingCount = $formData.agents.filter((a) => a.id.name === name).length; |
| 90 | + const registrySourceId = sourceToRegistryId(source as AgentSource); |
| 91 | + const detailed = await ctx.server |
| 92 | + .lookupAgent({ name, version, registrySourceId }) |
| 93 | + .catch((e) => { |
| 94 | + toast.error(`${e}`); |
| 95 | + console.error(e); |
| 96 | + return null; |
| 97 | + }); |
| 98 | + if (detailed) { |
| 99 | + try { |
| 100 | + $formData.agents.push({ |
| 101 | + id: { |
| 102 | + name, |
| 103 | + version, |
| 104 | + registrySourceId |
| 105 | + }, |
| 106 | + name: name + (existingCount > 0 ? `-${existingCount}` : ''), |
| 107 | + description: '', |
| 108 | + providerType: 'local', |
| 109 | + provider: { |
| 110 | + runtime: Object.keys(detailed.registryAgent.runtimes)[0] as any, |
| 111 | + remote_request: { |
| 112 | + maxCost: { type: 'micro_coral', amount: 1000 }, |
| 113 | + serverSource: { type: 'servers', servers: [] } |
| 114 | + } |
| 115 | + }, |
| 116 | + customToolAccess: new Set(), |
| 117 | + blocking: false, |
| 118 | + options: {} |
| 119 | + }); |
| 120 | + detailedAgent = null; |
| 121 | + $formData.agents = $formData.agents; |
| 122 | + selectedAgent = $formData.agents.length - 1; |
| 123 | + } catch (error) { |
| 124 | + console.error('Failed to add agent:', error); |
| 125 | + } |
| 126 | + } |
| 127 | + } catch (error) { |
| 128 | + console.error('Failed to lookup agent:', error); |
| 129 | + } |
| 130 | + }; |
| 131 | +
|
| 132 | + const AGENT_REGEX = /^(marketplace|linked|local):(.+?)@(\d+\.\d+\.\d+)$/; |
| 133 | + const agentsQuery = page.url.searchParams.get('agents'); |
| 134 | +
|
| 135 | + type AgentSource = 'marketplace' | 'linked' | 'local'; |
| 136 | + let parsedAgents: ParsedAgent[] = []; |
| 137 | +
|
| 138 | + onMount(async () => { |
| 139 | + try { |
| 140 | + const result = parseAgentsQuery(agentsQuery); |
| 141 | + parsedAgents = result.agents; |
| 142 | +
|
| 143 | + for (const agent of parsedAgents) { |
| 144 | + console.log( |
| 145 | + 'following url instructions to add agent: ' + |
| 146 | + agent.name + |
| 147 | + '@' + |
| 148 | + agent.version + |
| 149 | + ' from ' + |
| 150 | + agent.source + |
| 151 | + ' ' |
| 152 | + ); |
| 153 | + await addAgent(agent.name, agent.source, agent.version); |
| 154 | + } |
| 155 | + } catch (err) { |
| 156 | + console.error('Failed to parse agents:', err); |
| 157 | + } |
| 158 | + }); |
| 159 | + interface ParsedAgent { |
| 160 | + source: AgentSource; |
| 161 | + name: string; |
| 162 | + version: string; |
| 163 | + raw: string; |
| 164 | + } |
| 165 | +
|
| 166 | + function parseAgentsQuery(query: string | null) { |
| 167 | + if (!query) return { agents: [], errors: [] as string[] }; |
| 168 | +
|
| 169 | + const agentsFromQuery: ParsedAgent[] = []; |
| 170 | + const errors: string[] = []; |
| 171 | +
|
| 172 | + for (const raw of query.split(',')) { |
| 173 | + const trimmed = raw.trim(); |
| 174 | + if (!trimmed) continue; |
| 175 | +
|
| 176 | + const match = trimmed.match(AGENT_REGEX); |
| 177 | +
|
| 178 | + if (!match) { |
| 179 | + errors.push(`Invalid agent format: "${trimmed}"`); |
| 180 | + continue; |
| 181 | + } |
| 182 | +
|
| 183 | + const [, source, name, version] = match; |
| 184 | +
|
| 185 | + agentsFromQuery.push({ |
| 186 | + source: source as AgentSource, |
| 187 | + name: name ?? '', |
| 188 | + version: version ?? '', |
| 189 | + raw: trimmed |
| 190 | + }); |
| 191 | + } |
| 192 | +
|
| 193 | + return { agents: agentsFromQuery, errors }; |
| 194 | + } |
| 195 | +
|
| 196 | + let lastDeletedAgent: { |
| 197 | + agent: any; |
| 198 | + index: number; |
| 199 | + } | null = $state(null); |
| 200 | +
|
| 201 | + const removeAgent = (index: number) => { |
| 202 | + if (index < 0 || index >= $formData.agents.length) return; |
| 203 | +
|
| 204 | + const agent = $formData.agents[index]; |
| 205 | +
|
| 206 | + lastDeletedAgent = { |
| 207 | + agent, |
| 208 | + index |
| 209 | + }; |
| 210 | +
|
| 211 | + $formData.agents.splice(index, 1); |
| 212 | + $formData.agents = $formData.agents; |
| 213 | +
|
| 214 | + // Maintain selection invariants |
| 215 | + if (selectedAgent !== null) { |
| 216 | + if (selectedAgent === index) { |
| 217 | + selectedAgent = 0; |
| 218 | + } else if (selectedAgent > index) { |
| 219 | + selectedAgent--; |
| 220 | + } |
| 221 | + } |
| 222 | +
|
| 223 | + toast(`Agent "${lastDeletedAgent.agent.name}" deleted`, { |
| 224 | + action: { |
| 225 | + label: 'Undo', |
| 226 | + onClick: restoreAgent |
| 227 | + } |
| 228 | + }); |
| 229 | + }; |
| 230 | +
|
| 231 | + const restoreAgent = () => { |
| 232 | + if (!lastDeletedAgent) return; |
| 233 | +
|
| 234 | + $formData.agents.splice(lastDeletedAgent.index, 0, lastDeletedAgent.agent); |
| 235 | +
|
| 236 | + $formData.agents = $formData.agents; |
| 237 | + toast.success('Agent "' + lastDeletedAgent.agent.name + '" restored'); |
| 238 | +
|
| 239 | + selectedAgent = lastDeletedAgent.index; |
| 240 | +
|
| 241 | + lastDeletedAgent = null; |
| 242 | + }; |
72 | 243 |
|
73 | 244 | type CreateSessionRequest = NonNullable< |
74 | 245 | operations['createSession']['requestBody'] |
|
88 | 259 | let currentTab = $state('agent'); |
89 | 260 |
|
90 | 261 | let sendingForm = $state(false); |
| 262 | +
|
| 263 | + let catalogsLoaded = $derived(Object.keys(ctx.server.catalogs).length > 0); |
| 264 | +
|
91 | 265 | // svelte-ignore state_referenced_locally |
92 | 266 | let form = superForm(defaults(zod4(formSchema)), { |
93 | 267 | SPA: true, |
|
110 | 284 | try { |
111 | 285 | sendingForm = true; |
112 | 286 | const body = await asJson; |
113 | | - // console.log({ body }); |
114 | 287 | const res = await ctx.server.api.POST('/api/v1/sessions/{namespace}', { |
115 | 288 | params: { |
116 | 289 | path: { namespace: ctx.server.namespace } |
|
358 | 531 |
|
359 | 532 | const isMobile = new IsMobile(); |
360 | 533 |
|
361 | | - const addAgent = async (agent: any) => { |
362 | | - const catalog = Object.values(ctx.server.catalogs).at(0); |
363 | | -
|
364 | | - try { |
365 | | - if (!agent) { |
366 | | - throw new Error('No agents found in registry'); |
367 | | - } |
368 | | -
|
369 | | - if (!catalog) { |
370 | | - throw new Error('Catalog failed to load'); |
371 | | - } |
372 | | -
|
373 | | - if (!Array.isArray(agent.versions) || agent.versions.length === 0) { |
374 | | - throw new Error('Agent has no available versions'); |
375 | | - } |
376 | | -
|
377 | | - // Resolve detailed catalog info for this agent + version |
378 | | - const detailed = await ctx.server.lookupAgent({ |
379 | | - name: agent.name, |
380 | | - version: agent.versions[0], |
381 | | - registrySourceId: { ...catalog.identifier } |
382 | | - }); |
383 | | -
|
384 | | - if (!detailed?.registryAgent?.runtimes) { |
385 | | - throw new Error('Agent runtimes are missing from catalog'); |
386 | | - } |
387 | | -
|
388 | | - const runtimes = Object.keys(detailed.registryAgent.runtimes); |
389 | | - if (runtimes.length === 0) { |
390 | | - throw new Error('Agent has no supported runtimes'); |
391 | | - } |
392 | | -
|
393 | | - const runtime = runtimes[0] as any; |
394 | | -
|
395 | | - const existingCount = $formData.agents.filter((a) => a.id.name === agent.name).length; |
396 | | - selectedAgent = null; |
397 | | -
|
398 | | - $formData.agents.push({ |
399 | | - id: { |
400 | | - name: agent.name, |
401 | | - version: agent.versions[0], |
402 | | - registrySourceId: { ...catalog.identifier } |
403 | | - }, |
404 | | - name: agent.name + (existingCount > 0 ? `-${existingCount}` : ''), |
405 | | - description: '', |
406 | | - provider: { |
407 | | - remote_request: { |
408 | | - maxCost: { type: 'micro_coral', amount: 1000 }, |
409 | | - serverSource: { type: 'servers', servers: [] } |
410 | | - }, |
411 | | - runtime |
412 | | - }, |
413 | | - providerType: 'local', |
414 | | - customToolAccess: new Set(), |
415 | | - blocking: false, |
416 | | - options: {} |
417 | | - }); |
418 | | -
|
419 | | - detailedAgent = null; |
420 | | - $formData.agents = $formData.agents; |
421 | | - selectedAgent = $formData.agents.length - 1; |
422 | | - } catch (err) { |
423 | | - const message = err instanceof Error ? err.message : 'An unexpected error occurred'; |
424 | | - toast.error(message); |
425 | | - } |
426 | | - }; |
427 | | -
|
428 | | - let lastDeletedAgent: { |
429 | | - agent: any; |
430 | | - index: number; |
431 | | - } | null = $state(null); |
432 | | -
|
433 | | - const removeAgent = (index: number) => { |
434 | | - if (index < 0 || index >= $formData.agents.length) return; |
435 | | -
|
436 | | - const agent = $formData.agents[index]; |
437 | | -
|
438 | | - lastDeletedAgent = { |
439 | | - agent, |
440 | | - index |
441 | | - }; |
442 | | -
|
443 | | - $formData.agents.splice(index, 1); |
444 | | - $formData.agents = $formData.agents; |
445 | | -
|
446 | | - // Maintain selection invariants |
447 | | - if (selectedAgent !== null) { |
448 | | - if (selectedAgent === index) { |
449 | | - selectedAgent = 0; |
450 | | - } else if (selectedAgent > index) { |
451 | | - selectedAgent--; |
452 | | - } |
453 | | - } |
454 | | -
|
455 | | - toast(`Agent "${lastDeletedAgent.agent.name}" deleted`, { |
456 | | - action: { |
457 | | - label: 'Undo', |
458 | | - onClick: restoreAgent |
459 | | - } |
460 | | - }); |
461 | | - }; |
462 | | -
|
463 | | - const restoreAgent = () => { |
464 | | - if (!lastDeletedAgent) return; |
465 | | -
|
466 | | - $formData.agents.splice(lastDeletedAgent.index, 0, lastDeletedAgent.agent); |
467 | | -
|
468 | | - $formData.agents = $formData.agents; |
469 | | - toast.success('Agent "' + lastDeletedAgent.agent.name + '" restored'); |
470 | | -
|
471 | | - selectedAgent = lastDeletedAgent.index; |
472 | | -
|
473 | | - lastDeletedAgent = null; |
474 | | - }; |
475 | | -
|
476 | 534 | const UNGROUPED = '__ungrouped'; |
477 | 535 |
|
478 | 536 | let groupedOptions = $derived( |
|
664 | 722 | <Command.Root> |
665 | 723 | <Command.Input placeholder="Search agents..." /> |
666 | 724 | <Command.List> |
667 | | - {#each Object.values(ctx.server.catalogs).map((catalog) => catalog) as catalog} |
668 | | - <Command.Group heading={`${catalog.identifier.type}`}> |
669 | | - {#each Object.values(ctx.server.catalogs).flatMap( (catalog) => Object.values(catalog.agents) ) as agent} |
| 725 | + {#each Object.values(ctx.server.catalogs) as catalog} |
| 726 | + <Command.Group heading={catalog.identifier.type}> |
| 727 | + {#each Object.values(catalog.agents) as agent} |
670 | 728 | <HoverCard.Root> |
671 | | - <HoverCard.Trigger class="m-0" |
672 | | - ><Command.Item |
673 | | - class=" w-full cursor-pointer border-b px-4 py-2" |
674 | | - onSelect={() => addAgent(agent)} |
| 729 | + <HoverCard.Trigger class="m-0"> |
| 730 | + <Command.Item |
| 731 | + class="w-full cursor-pointer border-b px-4 py-2" |
| 732 | + onSelect={() => |
| 733 | + addAgent( |
| 734 | + agent.name, |
| 735 | + catalog.identifier.type, |
| 736 | + agent.versions[0]! |
| 737 | + )} |
675 | 738 | > |
676 | 739 | <span class="grow">{agent.name}</span> |
677 | | - <!-- <IconHeartRegular /> --> |
678 | | - </Command.Item></HoverCard.Trigger |
679 | | - > |
| 740 | + </Command.Item> |
| 741 | + </HoverCard.Trigger> |
| 742 | + |
680 | 743 | <HoverCard.Content |
681 | 744 | side="right" |
682 | 745 | class="max-w-1/2 min-w-full whitespace-pre-wrap" |
|
787 | 850 | {#if $formData.agents.length !== 0} |
788 | 851 | <Graph agents={$formData.agents} groups={$formData.groups} bind:selectedAgent /> |
789 | 852 | {:else} |
790 | | - <Card.Root class="m-auto w-1/4"> |
791 | | - <Card.Header> |
792 | | - <Card.Title>Session creator</Card.Title> |
793 | | - </Card.Header> |
794 | | - <Card.Content class="flex flex-col gap-2 text-sm "> |
795 | | - <span>Sessions let agents coordinate.</span> |
796 | | - |
797 | | - <span>Agents appear as nodes in a graph.</span> |
798 | | - |
799 | | - <span>Connections represent agent groups.</span> |
800 | | - </Card.Content> |
801 | | - <Card.Footer> |
802 | | - <Button |
803 | | - class="grow {selectedAgent !== null && |
804 | | - $formData.agents.length > selectedAgent |
805 | | - ? '' |
806 | | - : 'bg-accent/90'} w-fit truncate " |
807 | | - onclick={addAgent} |
808 | | - > |
809 | | - <span>Add an agent</span> |
810 | | - </Button> |
811 | | - </Card.Footer> |
812 | | - </Card.Root> |
| 853 | + <p> |
| 854 | + No agents added yet. Use the "Add agents" menu to add agents to your session. |
| 855 | + </p> |
813 | 856 | {/if} |
814 | 857 | </Tabs.Content> |
815 | 858 | </Tabs.Root> |
|
0 commit comments