Skip to content

feat(web): port full Goose Desktop UI to standalone web app#2

Merged
itsablabla merged 6 commits intobasefrom
devin/1776252051-jada-code-web-ui
Apr 15, 2026
Merged

feat(web): port full Goose Desktop UI to standalone web app#2
itsablabla merged 6 commits intobasefrom
devin/1776252051-jada-code-web-ui

Conversation

@itsablabla
Copy link
Copy Markdown
Owner

@itsablabla itsablabla commented Apr 15, 2026

Summary

Ports the full Goose Desktop UI (ui/desktop/) to work as a standalone web application served by nginx + goosed REST API. This is the complete desktop app with Home dashboard, Chat, Recipes, Skills, Apps, Scheduler, Extensions, and Settings — not just the chat component.

Two phases of work in this PR:

Phase 1 — Goose2 chat UI (earlier commits)

  • Web shims for Tauri IPC → WebSocket JSON-RPC to ACP server
  • Basic chat functionality via ui/goose2/ component

Phase 2 — Full Desktop UI port (latest commit)

  • web-shim.ts — Drop-in replacement for all ~50 window.electron.* IPC methods:
    • Settings → localStorage with jada_setting_ prefix
    • Event bus (on/off/emit) for Electron IPC channels
    • API connection via /api proxy to goosed REST server
    • File ops, dialogs, desktop features → web-compatible stubs
    • Secret key generation per browser session (sessionStorage)
  • web-entry.ts — Installs shims before renderer loads (critical ordering)
  • electron-stub.ts — Stub for IpcRendererEvent type import
  • vite.web.config.mts — Standalone Vite config (no Electron Forge):
    • React + Tailwind plugins
    • electron module aliased to stub
    • Electron-only packages externalized
    • Dev server proxy for /api → goosed
  • index.web.html — Web-specific entry HTML with theme initialization

Deployed and verified E2E:

  • goosed REST API running as systemd service on port 3270
  • nginx serves static web UI + proxies /api/* to goosed (injects X-Secret-Key)
  • Live at https://coder.garzaos.online
  • Nextcloud External Site #5 ("Coder") already configured

E2E verified: Full desktop UI loads with all sidebar items, chat works with Kimi K2.5 via OpenRouter.

Desktop UI live

Review & Testing Checklist for Human

  • Navigate to https://coder.garzaos.online — verify full UI loads with sidebar (Home, Chat, Recipes, Skills, Apps, Scheduler, Extensions, Settings)
  • Send a chat message — verify Kimi K2.5 responds via streaming
  • Click through Recipes, Skills, Apps, Scheduler, Extensions, Settings views
  • Verify Nextcloud embedding at https://next.garzaos.online/apps/external/5/
  • Check browser console for errors (some stub warnings are expected for unsupported desktop features)

Notes

  • goosed runs as a systemd service (goosed.service) on port 3270 with TLS disabled (nginx handles HTTPS)
  • The dist/ directory is not committed — build with pnpm exec vite build --config vite.web.config.mts in ui/desktop/
  • Provider config (GOOSE_PROVIDER=openrouter, GOOSE_MODEL=moonshotai/kimi-k2) is set via goosed's config API
  • nginx injects X-Secret-Key header on all /api/ proxied requests so the frontend doesn't need to know the server secret

Link to Devin session: https://app.devin.ai/sessions/f4e16f12bfa34fc6bdc3d73a833a5d91
Requested by: @itsablabla


Open with Devin

- Custom ACP client (JSON-RPC 2.0 over HTTP/SSE)
- React 19 + Vite + Tailwind CSS 4
- Streaming chat with tool call visualization
- Connection status indicator
- Markdown rendering with GFM support
- Nginx config for production deployment
- Dockerfile for containerized builds
@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 7 potential issues.

View 6 additional findings in Devin Review.

Open in Devin Review

messages: [
{
role: "user",
content: { type: "text", text },
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 content sent as single object instead of array, violating own AcpMessage interface

The sendMessage method at ui/web/src/lib/acp-client.ts:133 sends content: { type: "text", text } (a single object), but the AcpMessage interface defined at ui/web/src/lib/acp-client.ts:6-9 declares content: ContentPart[] (an array). The backend server's convert_acp_prompt_to_message at crates/goose-acp/src/server.rs:835 also expects a Vec<ContentBlock> (array). Sending a plain object where an array is expected will cause the server to fail to deserialize the user message.

Suggested change
content: { type: "text", text },
content: [{ type: "text", text }],
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

this.sessionId = acpSessionId;
}

// Read SSE response
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Comment // Read SSE response restates what the code does (AGENTS.md violation)

AGENTS.md rule: "Comments: Never add comments that restate what code does." The comment // Read SSE response at line 92 simply labels the code that follows (res.text(), text.split, JSON.parse), which is self-evidently reading and parsing an SSE response.

Suggested change
// Read SSE response
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

const event = JSON.parse(data) as AcpStreamEvent;
onEvent(event);
} catch {
// skip malformed JSON
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Comment // skip malformed JSON restates what the code does (AGENTS.md violation)

AGENTS.md rule: "Comments: Never add comments that restate what code does" and "Never: Comment self-evident operations." The comment // skip malformed JSON inside an empty catch block after JSON.parse is self-evident from the code structure.

Suggested change
// skip malformed JSON
} catch {
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}
}

// Error
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Comment // Error is a self-evident operation label (AGENTS.md violation)

AGENTS.md rule: "Never: Comment self-evident operations (// Initialize, // Return result)." The comment // Error at line 170 is directly analogous to the banned // Initialize example — it simply labels the error-handling branch that immediately follows (if (event.error)).

Suggested change
// Error
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

textareaRef.current?.focus();
}, []);

// Auto-resize textarea
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Comment // Auto-resize textarea restates what the code does (AGENTS.md violation)

AGENTS.md rule: "Comments: Never add comments that restate what code does." The comment // Auto-resize textarea restates the effect body that sets ta.style.height — the code is self-documenting.

Suggested change
// Auto-resize textarea
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

for (const item of result.content) {
const contentItem = item as Record<string, unknown>;
if (contentItem.type === "text" && contentItem.text) {
// Append or update text content
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Comment // Append or update text content restates what the code does (AGENTS.md violation)

AGENTS.md rule: "Comments: Never add comments that restate what code does." The comment at line 74 restates the immediately following logic which finds an existing text part and either appends to it or pushes a new one.

Suggested change
// Append or update text content
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

if (event.result && typeof event.result === "object") {
const result = event.result as Record<string, unknown>;

// Handle content array in result
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Comment // Handle content array in result restates what the code does (AGENTS.md violation)

AGENTS.md rule: "Comments: Never add comments that restate what code does." The comment at line 69 simply restates the condition if (result.content && Array.isArray(result.content)) that follows.

Suggested change
// Handle content array in result
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Port the full Goose Desktop UI (React/Vite) to work as a standalone
web app communicating with the Jada Code ACP server over WebSocket.

Key changes:
- Web shims for @tauri-apps/api (core, event, dialog, opener)
- WebSocket transport replacing Tauri IPC invoke() calls
- Event bus bridging ACP notifications to React hooks
- Vite web config with module aliasing for shims
- Nginx config for static file serving
- Fix acp_prepare_session handler (sessionId vs localSessionId bug)
- Fix ACP protocol fields: cwd, mcpServers, prompt
- Handle session/update notifications with snake_case variants

Verified E2E: browser → WebSocket → ACP server → Kimi K2.5 → streaming
response rendered in chat UI.
devin-ai-integration[bot]

This comment was marked as resolved.

Address Devin Review findings:
- Reset `initialized` flag when WebSocket closes so reconnect
  re-runs the initialize/notifications handshake
- Fix undefined `notifType` → `updateType` in default switch case
- Add window.electron web shim (web-shim.ts) replacing ~50 Electron IPC methods
  - Settings → localStorage with jada_setting_ prefix
  - Event bus for on/off/emit channels
  - API connection via /api proxy to goosed REST server
  - File ops, dialogs, desktop features → web-compatible stubs
- Add web entry point (web-entry.ts) that installs shims before renderer
- Add electron-stub.ts for type-level imports (IpcRendererEvent)
- Add web-only Vite config (vite.web.config.mts) without Electron Forge
- Add index.web.html with theme initialization for web builds
- Builds as static SPA served by nginx, proxying /api to goosed
@devin-ai-integration devin-ai-integration Bot changed the title feat(web): add React web UI for Jada Code ACP server feat(web): port full Goose Desktop UI to standalone web app Apr 15, 2026
devin-ai-integration[bot]

This comment was marked as resolved.

- fetchMetadata: remove misleading no-cors fetch that always returns empty
  string due to opaque response; document that metadata fetching is not
  available in web context
- sessionMap: clear stale session entries on WebSocket close to prevent
  prompt failures after reconnection
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 18 additional findings in Devin Review.

Open in Devin Review

Comment thread ui/web/nginx.conf Outdated
proxy_pass http://jada-code-backend:3284/acp;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Hardcoded Connection "upgrade" header sent on all /acp requests, including non-WebSocket HTTP POSTs

In ui/web/nginx.conf, the /acp location block unconditionally sets Connection "upgrade" for every proxied request. The ui/web ACP client (ui/web/src/lib/acp-client.ts) communicates via HTTP POST with SSE streaming—not WebSocket—so regular POST requests receive Connection: upgrade with an empty Upgrade header. This is a protocol violation (RFC 7230 §6.7) that prevents HTTP keep-alive connection reuse and may confuse strict HTTP backends. The sibling config at ui/goose2/nginx.web.conf:1-4 solves this correctly by using a map block to conditionally set the header based on $http_upgrade.

Prompt for agents
The /acp location block in ui/web/nginx.conf hardcodes proxy_set_header Connection "upgrade" on line 17, which sends the upgrade signal even for regular HTTP POST requests used by the ACP client. Fix this by adding a map block (like the one in ui/goose2/nginx.web.conf lines 1-4) at the top of the server config that maps $http_upgrade to a $connection_upgrade variable: when $http_upgrade is empty use "close", otherwise use "upgrade". Then replace the hardcoded string on line 17 with proxy_set_header Connection $connection_upgrade.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid point. Note that ui/web/nginx.conf is from the Phase 1 chat-only UI and is not used in the current deployment — the desktop UI port uses ui/goose2/nginx.web.conf which already has the correct map block (lines 1-4). That said, fixing it for correctness if anyone ever uses this config.

Use map block to set Connection header based on $http_upgrade,
matching the pattern already used in ui/goose2/nginx.web.conf.
Prevents sending upgrade header on regular HTTP POST requests.
@devin-ai-integration
Copy link
Copy Markdown
Contributor

E2E Test Results — Jada Code Desktop Web Port

Tested the full Goose Desktop UI web port at https://coder.garzaos.online against goosed REST API (Kimi K2.5 via OpenRouter). All 5 tests passed.

Devin session

Test Results (5/5 passed)
Test Result Details
App loads with full sidebar Passed All 9 nav items visible, dashboard stats, model shown in status bar, "Successfully loaded 8 extensions" toast
Chat E2E streaming Passed "What is the capital of France?" → "The capital of France is Paris." — session auto-named "France capital"
Sidebar navigation (all views) Passed Recipes, Skills, Apps, Scheduler, Extensions, Settings — all rendered without crash
Settings reads provider config Passed Shows "moonshotai/kimi-k2" with "OpenRouter" provider, 8 settings tabs working
Extensions list loads Passed 8 default + 7 available extensions displayed with toggles and descriptions
Screenshots

Home — Full sidebar with dashboard stats

Home

Chat E2E — Kimi K2.5 streaming response

Chat

Settings — Provider configuration loaded from goosed API

Settings

Home after navigation — Stats updated (3 sessions, 48.06K tokens)

Home return

Minor observations (non-blocking)
  • Dashboard heading typewriter animation briefly shows garbled characters mid-animation on navigation transitions. This is a cosmetic timing issue in the original Goose codebase, not introduced by the web port.

@itsablabla itsablabla merged commit 3d8c49f into base Apr 15, 2026
3 checks passed
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