feat: Higgsfield connector + inline video/image previews in chat - #285
feat: Higgsfield connector + inline video/image previews in chat#285xDenside wants to merge 2 commits into
Conversation
Higgsfield's hosted MCP (image, video and audio generation on the user's own credits) joins the Connectors catalog as a featured, pinned card. Sign in reuses the existing MCP OAuth flow: the MCP host publishes its own authorization-server metadata in front of Clerk, so discovery, dynamic client registration and PKCE work unchanged. Catalog entries may now name their OAuth scopes (`oauthScopes`), passed through `mcpOauthLogin(name, url, scopes?)` to `mcp_oauth_login`; Higgsfield asks for `openid email offline_access` so a refresh token is issued. Empty or absent keeps today's behaviour for every other connector. Pinned connectors sort ahead of installed servers in the grid. Verified live against a signed-in account: consent page names Atomic Chat, tokens carry a rotating refresh token (24 h access), refreshed tokens are accepted by the MCP, `tools/list` returns 101 tools. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cqVAA5BaFeZVTuwpqhwyU
…iles inline
Tool cards render a media gallery above the raw result for any image or
video the result points at: URLs with a decodable extension, plus typed
generation records (`{ type, results: { rawUrl, thumbnailUrl } }`,
`result_url`) even without an extension. Works for chat mode's MCP content
array and agent mode's JSON-string outcome. Assistant prose that links a
video gets a player under the text; `video/*` file parts become a video
trace block next to the image and audio ones.
Agent-written local media used to hit rehype-harden's blocked placeholder
(no relative or `file:` sources). Markdown images, links and bare `file://`
URLs that resolve to a local file are rewritten into
`https://atomic.local/media?path=…` links, resolved against the message's
known files and then the thread's workspace root; the link renderer turns
them back into a `<video>` or `<img>` served via the asset protocol, or an
open-with-OS link for other files. The agent persona now says the chat
renders media, so the model stops disclaiming inline previews.
`media-src` gains `https:` in the CSP, matching `img-src`; without it no
remote video could play. A failed load degrades to the URL as a link. No
autoplay, metadata preload only.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015cqVAA5BaFeZVTuwpqhwyU
|
Thanks for this, @xDenside — it's a big diff that stays legible, and the two ADRs do a lot of the reviewing for me. A few things I want to note before the findings, because they're the parts that would have been easy to get wrong: Reusing the existing MCP OAuth flow rather than inventing one for Higgsfield is the right call, and making Verified on your branch merged onto current
I can't compile the Rust side here (no GTK dev libraries on this runner), so Five things. The first two are the ones I'd like before merge. 1. It doesn't merge —
|
| message | per scan | cumulative over the stream |
|---|---|---|
| 516 chars, no URLs | 0.0012 ms | 0.1 ms |
| 1,480 chars, 20 URLs | 0.92 ms | 255 ms |
| 8,920 chars, 80 URLs | 3.50 ms | 2,722 ms |
The /https?:\/\// early-out means a link-free answer is genuinely free — nice. But an answer that cites sources pays 0.9–3.5 ms of main-thread work per frame, which is most of a frame budget on its own, and 0.25–2.7 s cumulative across one response. Wrapping it in useMemo on block.text doesn't remove the recompute during streaming, but it does stop it firing on every unrelated re-render of the list; keying it off the finished text (or skipping the scan while isStreaming) removes it entirely. Given 2026-09-04-bound-streaming-reasoning-render-cost exists, this is a cost the project has already decided it doesn't want to pay twice.
3. The prose-video player isn't gated on isAgentMessage — should it be?
The markdown rewriting is (isAgentMessage ? linkAgentFileReferences(linkAgentLocalMedia(...)) : block.text), but extractTextVideos(block.text) sits outside that ternary, in the role !== 'user' branch. So every assistant message in ordinary chat now gets a player for any .mp4/.webm/.mov URL it happens to mention, and <video preload="metadata"> fetches from that host immediately — no click.
That's a new automatic outbound request triggered by model output, in an app whose pitch is "private, 100% offline". A model summarising a fetched page, or repeating a URL from a search result, can now make the app beacon an arbitrary host without the user doing anything. The CSP change is what permits it, and I think widening media-src to match img-src is defensible on its own — it's the unprompted fetch I'd want a decision on rather than a side effect. Options, in the order I'd pick them: gate the prose player to agent messages (matching the rewriting right above it); or keep it everywhere but render a poster/placeholder that loads on click; or preload="none". Your call — you've thought about the media surface more than I have — but it should be in the ADR either way.
4. Local-file links widen from "files this session touched" to "any path the model writes"
Worth a look because the shape is a familiar one. I ran the two pipelines side by side on the same prose:
main today:
[Q3 report](/private/tmp/payload.sh) → left inert by the hardening
with this PR:
[Q3 report](https://atomic.local/open-file?path=%2Fprivate%2Ftmp%2Fpayload.sh)
resolveAgentLocalPath returns any isAbsoluteAgentPath candidate, whether or not it's among the message's known references, so clicking that link calls opener().openPath() on a path the model chose, under a label the model chose. Before this PR, only paths the agent had actually touched (tool inputs, attachments) became clickable.
I'm not claiming this is a privilege escalation for the agent itself — it already has os.fs and shell. The concern is the label/href mismatch reaching the user's click, on output that can be steered by whatever a fetch or search tool pulled in. Two cheap mitigations, either is fine: restrict the open-file rewrite to paths in context.references (leaving the media rewrite as-is, since rendering a picture is inert), or show the resolved path in the link title so the user can see where it goes. Media links are fine unchanged.
5. Prettier: 8 files
prettier --check flags 15 of your files; 7 of those fail identically on main (INDEX.md, message-trace-parts.ts, $threadId.tsx, services/__tests__/mcp.test.ts, services/mcp/{default,tauri,types}.ts) — pre-existing drift, please leave them. These 8 are new:
docs/decisions/2026-09-09-add-higgsfield-as-an-oauth-connector-with-catalog-declared-scopes.md
docs/decisions/2026-09-09-play-remote-video-and-images-from-tool-results-inline-in-chat.md
web-app/src/components/ai-elements/tools/tool-media.tsx
web-app/src/components/ai-elements/tools/tool-output.test.tsx
web-app/src/components/ai-elements/tools/tool.tsx
web-app/src/containers/MessageItem.tsx
web-app/src/lib/tool-media.test.ts
web-app/src/routes/connectors/__tests__/index.test.tsx
MessageItem.tsx is the fragment you added around the markdown block — the children weren't re-indented under it. agent-file-links.ts and its test are clean, for what it's worth.
Nits
pinnedand the one-connector rule. The doc comment says "keep it to one", but nothing enforces it and the sort silently accepts several. A.find()for the first pinned entry, or a comment onMCP_CONNECTORSitself, would make the invariant survive the next catalog edit. Separately: pinning a paid third-party service above the user's own installed servers is a product call more than a code one — flagging it so it's a decision, since the ADR doesn't cover the ordering.- The ~149K-token tool block. Your checklist notes the follow-up but says no issue was filed. Given
DEFAULT_MCP_TOOL_LIST_TIMEOUT_SECSexists precisely because tool listing has bitten this app before, I'd like the issue open before this merges rather than after. - No CI has run on this branch — check runs come back
total_count: 0.
Rebase for 1, memoize for 2, answer 3, and I'm happy to take it. The Higgsfield wiring itself I have no notes on 🎬
Generated by Claude Code
Describe Your Changes
Higgsfield connector (
feat(connectors))oauthScopes), passed throughmcpOauthLogin(name, url, scopes?)tomcp_oauth_login. Higgsfield asks foropenid email offline_accessso a refresh token is issued; absent/empty keeps current behaviour for every other connector.tools/listreturns 101 tools. Note for follow-up: that tool block is ~149K tokens; a default tool allowlist for this connector is the obvious next step and is not part of this PR.Inline media in chat (
feat(chat)){ type, results: { rawUrl, thumbnailUrl } },result_url) even without one. Covers chat mode's MCP content array and agent mode's JSON-string outcome.video/*file parts become a video trace block next to image and audio.,[Preview x.mp4](file:///…), barefile://) used to hit rehype-harden's blocked placeholder. They are rewritten intohttps://atomic.local/media?path=…links, resolved against the message's known files then the thread's workspace root, and rendered as<video>/<img>through the asset protocol (other files: open-with-OS link). The agent persona now states the chat renders media, so the model stops disclaiming previews.media-srcgainshttps:(matchingimg-src); no remote video could play without it. Failed loads degrade to a link; no autoplay.Two ADRs under
docs/decisions/document both. Verified with Vitest (connectors, services, tool output, trace parts, message/markdown suites),tsc -band eslint; the Rust changes compiled in a local universal macOS build.Fixes Issues
Self Checklist
🤖 Generated with Claude Code
https://claude.ai/code/session_015cqVAA5BaFeZVTuwpqhwyU