feat(dashboard): Chat tab with HTTP+SSE agent gateway (Gemini-first)#91
Merged
Conversation
…rowser - App.tsx: add "library" AppView; Library renders independent of Zenoh connection (pure REST fetch from spatialstore:8080) - LibraryView.tsx: sessions sidebar grouped by date; thumbnail grid (sorted by frame_index for temporal order); lightbox for full PNG; lazy pagination (48 frames/page); VITE_SPATIALSTORE_URL env override Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a Chat tab to the React dashboard that streams LLM responses from
the daemon-side agent runtime via HTTP+SSE, with Gemini as the recommended
provider (free tier via AI Studio) and Claude API key as opt-in.
Backend:
- GeminiProvider: new ModelProvider impl with API-key auth, streaming
via :streamGenerateContent?alt=sse, full tool-calling support
- AnyProvider::generate_stream override: routes to each provider's real
streaming impl (previously fell back to single-delta default)
- POST /api/v1/agents/{id}/turns: SSE chat route with bearer auth,
fail-closed OAuth gate (403 oauth_chat_disabled for Claude OAuth)
- daemon/manifest queryable: serves http_endpoint+version for dashboard
discovery, separate from per-agent AgentManifest
- Gemini-first provider selection in agent setup wizard and auto-detect
- CORS fail-closed: BUBBALOOP_DASHBOARD_ORIGIN env var required; if
unset, CORS layer not mounted (browsers blocked, CLI/curl still works)
- BUBBALOOP_HTTP_BIND: configurable bind address (default 127.0.0.1:8088)
- OAuth startup WARN citing Anthropic Feb 2026 ToS enforcement
Frontend:
- ChatView: token-paste UI, agent discovery+selector, message log with
per-kind entry styling (user/assistant/tool/tool_result/error/system/
connection_lost), stop button via AbortController, delta buffering
- parseSseStream: minimal SSE ReadableStream frame parser
- discoverAgents: joins daemon+agent manifests by machine_id
- loadToken/saveToken: localStorage with key bubbaloop_mcp_token
- OAuthGate: 403 explainer with three migration paths
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…, tool JSON, sticky nav
- Full-viewport layout: `height: 100vh; overflow: hidden` on `.app`; `.chat-body` scrolls
independently while navbar and input bar remain fixed
- Input bar: full-width, pinned at bottom (`flex-shrink: 0`), outside the scroll container;
44×44px icon circle send/stop buttons (paper-plane SVG) — no `width: 100%` on mobile
- Textarea: `rows={1}` default; auto-grows up to 6 rows via `field-sizing: content`
- Agent selector: pill chip → card panel with backdrop; single-agent shows non-interactive chip;
color-coded by provider (Gemini=#4285F4, Claude=#D4A574, Ollama=#7C3AED); ellipsis on long names
- Tool entries: icon + name row; `<details>` below (not inline) for args; tool_result 80-char
preview with expand; no horizontal scroll; 12px font on JSON/result
- Scroll guard: `userScrolledRef` + `chatBodyRef`; auto-scroll only when user is near bottom;
resets to bottom when user sends a new message
- Removed Library tab (temporary) from App.tsx
- Vite proxy: `/api → http://127.0.0.1:8088` for HTTPS→HTTP mixed-content fix
- daemon_manifest.rs: `BUBBALOOP_HTTP_PUBLIC_URL` env var separates advertised URL from bind addr
- agent.ts: simplified to query only agent manifests (daemon manifest dropped); relative URL via proxy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- index.css: add `overflow: hidden` to html/body; change #root from `min-height: 100%` to `height: 100%; overflow: hidden` - App.tsx: change .app from `height: 100vh` to `height: 100%` so it inherits the locked chain; raise .app-header z-index to 200 - ChatView.tsx: raise agent-backdrop/panel z-index to 150/151 so they render below the navbar (200) — backdrop no longer swallows nav clicks Without overflow: hidden on body/#root, the browser would scroll the page when chat content overflowed, pushing the navbar off screen. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract runDiscovery() — removes duplicate discovery logic between
useEffect and handleRefresh
- Inline AgentPill into AgentSelector — was only called from one place
- Replace OAuthGate component with a chat entry kind "oauth_blocked",
rendered inline by EntryRow — removes separate render branch
- Remove chat-body-inner wrapper div — max-width/margin merged onto
.chat-body directly
- Flatten chat-header-left/right divs — single flex row, margin-left auto
on reset button
- Merge .tool-result-summary into .tool-summary (both classes on element)
- Remove empty .chat-view {} rule in mobile media query
No behaviour changes. TypeScript clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
C1: rename AgentEvent.tool_input → input to match Rust wire field name
(gateway.rs:61 is `pub input`); tool args were silently never shown
H1: OAuth gate — reject empty env vars; GEMINI_API_KEY="" now keeps the
gate active instead of bypassing it
H3: abort in-flight SSE stream on ChatView unmount via useEffect cleanup;
prevents React state-updates-on-unmounted-component warnings and
closes the daemon-side drain task promptly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Missing attribute caused usageMetadata JSON field to not map to usage_metadata, silently staying None (via #[serde(default)]). Result: usage tokens were always 0 in streaming responses. Caught by gemini_sse_single_text_frame test in CI. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This was referenced May 15, 2026
edgarriba
added a commit
that referenced
this pull request
May 19, 2026
Ships 10 commits worth of features and fixes since v0.0.13: Features - agent: grab_frame tool + Gemini Vision provider + image-in-chat (#96) - dashboard: Chat tab with HTTP+SSE agent gateway (#91) - agent: client-side turn cancellation via Zenoh cancel topic (#94, #95) - chat: multi-provider login status, 404 on unknown agent, Claude OAuth risk warning (#93) - mcp: 'bubbaloop mcp --token' prints bearer token for .mcp.json (#90) Fixes - agent: grab_frame reads binary JPEG payload + JSON metadata attachment from camera nodes (#99) - chat: clear Responding state after multi-camera grab_frame (#98) - mcp: eliminate token race between gateway and agent runtime (#89) Chores - dashboard: remove LibraryView component (#92, replaced by Chat tab) Docs - Comprehensive sync across README, CHANGELOG (backfilled v0.0.8 to v0.0.13), CLAUDE.md, ARCHITECTURE.md, ROADMAP.md, concept/reference/ dashboard docs to match the new feature surface (#100) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
GeminiProviderimplementing theModelProvidertrait — auth viaGEMINI_API_KEYenv var or~/.bubbaloop/gemini-key, streaming via:streamGenerateContent?alt=sse, tool calling mapped to/from Gemini'sfunction_call/function_responsepartsPOST /api/v1/agents/{id}/turnsSSE route in the daemon — bearer auth (reuses MCP middleware), publishes to agent Zenoh inbox, drains outbox to SSE stream; blocks Claude OAuth with HTTP 403DaemonManifestZenoh queryable atbubbaloop/global/{machine_id}/daemon/manifest;BUBBALOOP_HTTP_PUBLIC_URLenv var separates advertised URL from bind addressAbortControllerstop, delta buffering, scroll guard, mobile-friendly agent picker, expandable tool JSONhtml/body/#root/appalloverflow: hidden), navbar z-index 200 above agent panel (151/150), full-width pinned input bar/api → http://127.0.0.1:8088— avoids HTTPS→HTTP mixed-content blocks when dashboard runs over HTTPS (WebCodecs requirement)Provider auth policy
GEMINI_API_KEYANTHROPIC_API_KEY~/.bubbaloop/oauth-credentials.jsonoauth_chat_disabledper Anthropic Feb 2026 ToSTest plan
curl -N -H "Authorization: Bearer $TOKEN" -d '{"message":"hello"}' http://localhost:8088/api/v1/agents/gemini/turns→ SSE delta frames thendoneoauth_chat_disabledcodebubbaloop agent setupshows Gemini as first option🤖 Generated with Claude Code