From d4890f72c097099dedec67e24e8296839920c611 Mon Sep 17 00:00:00 2001 From: Mathias Picker <48158184+MathiasWP@users.noreply.github.com> Date: Thu, 17 Sep 2026 14:37:19 +0200 Subject: [PATCH] Show what an agent sent in History, as it happens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MCP server has always recorded its sends — it writes to the same database as the window, so `query_response` can read a body back — but the window only read that database at launch. A request an agent made appeared in History after the next relaunch, and then as a bare URL: the server files its sends under one synthetic request per collection rather than an endpoint's id, so the name lookup came up empty. History now catches up on focus, when the tab is opened, and every few seconds while it stays open. The list is merged rather than replaced, so a loaded body or a send still streaming is untouched. Entries from the server wear an `MCP` badge, and `mcp` matches in the search box. Every entry now has a name. One with no request to be named after — sent by an agent, from scratch, or by a request since deleted — takes the name of the endpoint its URL hits in that collection, the loader's or a saved request's, and failing that its path, the way endpoint rows read in Collections. A row without a name looked like a rendering fault. --- .changeset/agent-sends-in-history.md | 23 +++++ src/lib/components/Sidebar.svelte | 122 +++++++++++++++++++++++---- src/lib/history.svelte.ts | 49 ++++++++++- src/routes/+page.svelte | 9 +- 4 files changed, 184 insertions(+), 19 deletions(-) create mode 100644 .changeset/agent-sends-in-history.md diff --git a/.changeset/agent-sends-in-history.md b/.changeset/agent-sends-in-history.md new file mode 100644 index 0000000..5eb3d6e --- /dev/null +++ b/.changeset/agent-sends-in-history.md @@ -0,0 +1,23 @@ +--- +"fiber": patch +--- + +Requests an agent sends through the MCP server now show up in History as they +happen, wearing an `MCP` badge, and every entry in the list has a name. + +The server has always recorded what it sends — it writes to the same database +as the window, so `query_response` can read a body back — but the window only +read that database at launch. A send made from an agent's window appeared in +History after the next relaunch, and then as a bare URL: the server files its +requests under one synthetic request per collection rather than an endpoint's +id, so the name lookup came up empty. The same was true of anything sent from +scratch or whose request has since been deleted, which made a row without a +name look like a rendering fault rather than what it was. + +History now catches up when the window regains focus, when the tab is opened, +and every few seconds while it stays open; the list is merged rather than +replaced, so a body you have loaded or a send still streaming is untouched. An +entry with no request to be named after takes the name of the endpoint its URL +hits — the loader's or a saved request's — and failing that its path, the way +endpoint rows read in Collections. `mcp` matches in the search box, so the badge +is something you can filter by. diff --git a/src/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte index 9cc16b6..fd2d2e7 100644 --- a/src/lib/components/Sidebar.svelte +++ b/src/lib/components/Sidebar.svelte @@ -2,6 +2,7 @@ import { ContextMenu, Dialog, Tooltip } from 'bits-ui'; import { applyPathParams, + endpointKey, LOOSE_SECTION_ID, methodColor, normalizeBaseUrl, @@ -307,6 +308,30 @@ history.stopViewing(); } + /** + * Opening History is a look at the list, so it is the moment to make sure the + * list is current: the MCP server writes to the same database from another + * process, and nothing else tells this window when it has. + */ + function showHistory() { + session.sidebarTab = 'history'; + history.refresh(); + } + + /** + * And while History stays open, keep looking. An agent typically sends in + * bursts while you watch the tab, so a refresh only on opening it would show + * the first request of a run and none that followed. A list of a few hundred + * rows from SQLite is cheap, and a refresh that finds nothing new changes + * nothing, so polling costs no re-render. + */ + const HISTORY_POLL_MS = 5_000; + $effect(() => { + if (session.sidebarTab !== 'history') return; + const timer = setInterval(() => history.refresh(), HISTORY_POLL_MS); + return () => clearInterval(timer); + }); + /** * Same reason as above: the MCP tab isn't History either, so the entry you * were looking at stops overriding the response pane. @@ -456,27 +481,89 @@ }); /** - * The name of the request an entry came from, when there still is one. + * Every endpoint's name by what it hits — `"METHOD /path"` — per section. * - * Plenty of entries outlive their request: sent from scratch, sent by a - * loader or the MCP server, or the request has since been deleted. Those get - * no name rather than an invented one — the URL underneath is their identity. + * For the entries that have no request to be named after. The MCP server + * sends under one synthetic request per collection rather than an endpoint's + * id, so its entries never matched `requestNames`, and a run of agent calls + * showed up as a column of bare URLs. The endpoint they hit is still known: + * the loader's manifest lists it, or a saved request does. */ - function requestName(entry: HistoryEntry): string | null { - return requestNames.get(entry.requestId) ?? null; + const endpointNames = $derived.by(() => { + const names = new Map>(); + for (const section of collections.sections) { + const mine = new Map(); + for (const endpoint of collections.loaderCaches[section.id]?.endpoints ?? []) { + mine.set(endpointKey(endpoint.method, endpoint.path), endpoint.name); + } + // User-named after loader-named, so a rename of an endpoint wins. + for (const request of section.requests) { + mine.set(endpointKey(request.method, request.path), request.name); + } + for (const entry of section.overlay) mine.set(entry.id, entry.name); + names.set(section.id, mine); + } + return names; + }); + + const sectionsById = $derived( + new Map(collections.sections.map((section) => [section.id, section])) + ); + + /** + * The path an entry's URL has under its section's base, or under no base at + * all — `/users/42` either way, so that it reads like the endpoint rows in + * Collections. Never empty: a request to the base URL itself is `/`. + */ + function pathOf(entry: HistoryEntry): string { + const base = entry.sectionId ? sectionsById.get(entry.sectionId)?.baseUrl : undefined; + let path: string; + if (base && entry.url.startsWith(normalizeBaseUrl(base))) { + path = entry.url.slice(normalizeBaseUrl(base).length); + } else { + try { + path = new URL(entry.url).pathname; + } catch { + path = entry.url; + } + } + path = path.split(/[?#]/, 1)[0]; + return path.startsWith('/') ? path : `/${path}`; + } + + /** + * What to call an entry. + * + * The request it came from, when that still exists; else the endpoint its + * URL hits, when the collection knows one; else the path. Every entry gets a + * name — a row without one looked like a rendering fault rather than like a + * request sent from scratch, by a loader, or by an agent. + */ + function requestName(entry: HistoryEntry): string { + const own = requestNames.get(entry.requestId); + if (own) return own; + const path = pathOf(entry); + if (entry.sectionId) { + const named = endpointNames.get(entry.sectionId)?.get(endpointKey(entry.method, path)); + if (named) return named; + } + return path; } /** * Matched against the request's name, method, URL and status — so `404`, - * `POST` and the name now on the row all work. + * `POST` and the name now on the row all work — and against `mcp`, so the + * badge on a row is something you can search for too. */ const visibleHistory = $derived.by(() => { const needle = historyQuery.trim().toLowerCase(); if (!needle) return history.entries; return history.entries.filter((entry) => { const status = entry.response ? String(entry.response.status) : entry.error ? 'error' : ''; - const name = requestName(entry) ?? ''; - return `${name} ${entry.method} ${entry.url} ${status}`.toLowerCase().includes(needle); + const via = entry.viaMcp ? 'mcp' : ''; + return `${requestName(entry)} ${entry.method} ${entry.url} ${status} ${via}` + .toLowerCase() + .includes(needle); }); }); @@ -818,7 +905,7 @@ class="px-2 py-1 rounded text-xs transition-colors {session.sidebarTab === 'history' ? 'bg-raised text-text' : 'text-muted hover:text-text'}" - onclick={() => (session.sidebarTab = 'history')} + onclick={() => showHistory()} > History @@ -1323,12 +1410,15 @@ spinner, error icon and code all occupy the same space, so a row doesn't jump about as a request settles. --> - {#if name} - {name} - {:else} - - + {name} + {#if entry.viaMcp} + + MCP {/if} collections.refreshStale()} + onfocus={() => { + collections.refreshStale(); + history.refresh(); + }} onpagehide={() => session.flush()} />