feat(chat): MCP tool apps — custom inline renderers for 12 tools
12 of 33 MCP tools now render as rich inline cards instead of raw JSON: EntityCard, HealthSummary, LXCList, EntityTable, KnowledgeResults, BlastRadius, ChangeLog, FleetSnapshot, MetricChart. Architecture: - Server: annotateJSONResult() wraps queryRows with __renderer hints - Registry: match/dispatch system maps tool names to Svelte components - Chat: inline dispatch with 5-card limit, overflow to collapsed group - ToolCallGroup: unmatched prop, hides when all matched, ARIA labels Tests: 3 new Go tests for annotateJSONResult (wrap, no-op, multi-row).
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
import TaskContextPanel from '$lib/components/TaskContextPanel.svelte'
|
||||
import ToolCallGroup from '$lib/components/ToolCallGroup.svelte'
|
||||
import InlineApproval from '$lib/components/InlineApproval.svelte'
|
||||
import { getToolRenderer } from '$lib/tool-renderers'
|
||||
import { Button } from '$lib/components/ui/button'
|
||||
import { Textarea } from '$lib/components/ui/textarea'
|
||||
import ArrowUpIcon from '@lucide/svelte/icons/arrow-up'
|
||||
@@ -16,6 +17,19 @@
|
||||
let input = $state('')
|
||||
let messagesEnd = $state<HTMLDivElement | null>(null)
|
||||
|
||||
const MAX_INLINE_CARDS = 5
|
||||
|
||||
function getInlineTools(msg: { tools: any[] }): any[] {
|
||||
const matched = msg.tools.filter((t) => getToolRenderer(t))
|
||||
if (matched.length <= MAX_INLINE_CARDS) return matched
|
||||
return matched.slice(0, MAX_INLINE_CARDS)
|
||||
}
|
||||
|
||||
function getRemaining(msg: { tools: any[] }, inline: any[]): any[] {
|
||||
const inlineIds = new Set(inline.map((t) => t.id))
|
||||
return msg.tools.filter((t) => !inlineIds.has(t.id))
|
||||
}
|
||||
|
||||
// Resizable right rail (session graph). Persisted so it survives reloads.
|
||||
const RAIL_MIN = 260
|
||||
const RAIL_MAX = 620
|
||||
@@ -113,7 +127,17 @@
|
||||
<div class="max-w-[85%] rounded-2xl rounded-br-sm bg-primary px-4 py-2.5 text-sm text-primary-foreground whitespace-pre-wrap">{msg.text}</div>
|
||||
{:else}
|
||||
<div class="flex w-full flex-col gap-2">
|
||||
<ToolCallGroup tools={msg.tools} active={$streaming && i === $messages.length - 1} />
|
||||
{#each getInlineTools(msg) as tool (tool.id)}
|
||||
{@const renderer = getToolRenderer(tool)}
|
||||
{#if renderer}
|
||||
<renderer.component {tool} />
|
||||
{/if}
|
||||
{/each}
|
||||
<ToolCallGroup
|
||||
tools={msg.tools}
|
||||
unmatched={getRemaining(msg, getInlineTools(msg))}
|
||||
active={$streaming && i === $messages.length - 1}
|
||||
/>
|
||||
{#if msg.text}
|
||||
<div class="prose-chat max-w-none text-sm leading-relaxed">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags — sanitized via DOMPurify -->
|
||||
|
||||
Reference in New Issue
Block a user