Skip to content

feat(dashboard): Chat tab with HTTP+SSE agent gateway (Gemini-first)#91

Merged
edgarriba merged 8 commits into
mainfrom
feat/dashboard-chat-gemini
May 15, 2026
Merged

feat(dashboard): Chat tab with HTTP+SSE agent gateway (Gemini-first)#91
edgarriba merged 8 commits into
mainfrom
feat/dashboard-chat-gemini

Conversation

@edgarriba

Copy link
Copy Markdown
Member

Summary

  • New GeminiProvider implementing the ModelProvider trait — auth via GEMINI_API_KEY env var or ~/.bubbaloop/gemini-key, streaming via :streamGenerateContent?alt=sse, tool calling mapped to/from Gemini's function_call/function_response parts
  • POST /api/v1/agents/{id}/turns SSE route in the daemon — bearer auth (reuses MCP middleware), publishes to agent Zenoh inbox, drains outbox to SSE stream; blocks Claude OAuth with HTTP 403
  • DaemonManifest Zenoh queryable at bubbaloop/global/{machine_id}/daemon/manifest; BUBBALOOP_HTTP_PUBLIC_URL env var separates advertised URL from bind address
  • Chat tab in the React dashboard — agent discovery via Zenoh, HTTP+SSE fetch with AbortController stop, delta buffering, scroll guard, mobile-friendly agent picker, expandable tool JSON
  • Layout fixes — scroll chain locked (html/body/#root/app all overflow: hidden), navbar z-index 200 above agent panel (151/150), full-width pinned input bar
  • Vite proxy /api → http://127.0.0.1:8088 — avoids HTTPS→HTTP mixed-content blocks when dashboard runs over HTTPS (WebCodecs requirement)

Provider auth policy

Provider Auth Status
Gemini (recommended) GEMINI_API_KEY Free tier: 15 RPM / 1M tokens/day
Claude API key ANTHROPIC_API_KEY Pay-per-token, sanctioned
Ollama None Local, unchanged
Claude OAuth ~/.bubbaloop/oauth-credentials.json Blocked — HTTP 403 oauth_chat_disabled per Anthropic Feb 2026 ToS

Test plan

  • curl -N -H "Authorization: Bearer $TOKEN" -d '{"message":"hello"}' http://localhost:8088/api/v1/agents/gemini/turns → SSE delta frames then done
  • Chat tab → paste token → agent discovered → send message → streaming response
  • Stop button aborts fetch mid-stream
  • Tool call shows expandable args below tool row; result shows preview with expand
  • Navbar stays pinned when chat log scrolls (mobile and desktop)
  • Input bar stays pinned at bottom; textarea grows with content
  • Agent picker: single agent = non-interactive chip; multiple = dropdown
  • Claude OAuth daemon → chat route returns 403 with oauth_chat_disabled code
  • bubbaloop agent setup shows Gemini as first option

🤖 Generated with Claude Code

edgarriba and others added 6 commits May 14, 2026 22:03
…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-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

edgarriba and others added 2 commits May 15, 2026 11:23
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>
@edgarriba edgarriba merged commit 4140a1a into main May 15, 2026
1 check passed
@edgarriba edgarriba deleted the feat/dashboard-chat-gemini branch May 15, 2026 09:56
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant