Skip to content

feat(app): replace Dashboard with profile dropdown popover #231

Description

@jeonghun-jj-lee

Important

Problem — The Home/Dashboard page occupies the primary titlebar button and the entire landing view but delivers low value for its weight. The widget grid, chrome strip controls (defaults capsule, sessions flyout, connections popover, settings), and legacy card layout are going away. The one piece users want quick access to — their identity — has no lightweight surface outside the full-screen dashboard.

Approach — Kill the entire Home page view and replace the titlebar button with a profile popover: a compact, inline-editable identity card (avatar, name, role, affiliation, research area, bio, outbound links) implemented as a Kobalte Popover anchored to the titlebar. Server-side widget infrastructure stays intact for future side-panel work. App launch goes straight to a blank chat. Onboarding data flows into profile.json so the dropdown is populated immediately after onboarding completes.

Scope — in: remove Home page + chrome strip + widget UI components + mod+b keybind; add profile button + popover with inline editing + avatar upload + link pills; add role/github/custom_link/description fields to profile endpoint; bridge onboarding events into profile.json; change app launch to blank chat. out: widget side-panel (future), chrome strip relocation (accessible via command palette/status bar), stats/remembers/platform pills.

Assumptions — The server-side widget routes remain useful for the future side-panel and for the amicode_author_widget tool's preview (pinning becomes a no-op until the panel ships). Settings and connections are accessible elsewhere (command palette, status bar) and don't need immediate relocation. The onboarding events stream (events.jsonl) is the source of truth for collected identity data; profile.json is the serving layer the UI reads from.

Acceptance Criteria

Dashboard removal

  • The Home page view no longer renders; no route, no toggle state, no mod+b keybind
  • App launches to a blank chat tab (no landing page, no redirect to Home)
  • Server-side widget routes (/amicode/widgets, /amicode/widget-frame, /amicode/widget-code, /amicode/dashboard) remain functional
  • Deleted UI files: widget-grid.tsx, widget-frame.tsx, widget-bridge.ts, widget-tokens.ts, widget-allowlist.ts, home-cards.tsx; Home page exports from home.tsx; Defaults capsule and Sessions flyout components

Profile popover

  • A person-icon button appears in the titlebar at the former Home button position (left of tab strip)
  • Clicking the button opens a Kobalte Popover (bottom-start, gutter 8px) showing the profile identity card
  • The popover participates in chrome-dropdown coordination (auto-closes other flyouts)
  • The card displays: avatar tile (rounded square, 48px), name, role, affiliation, research area, and short bio
  • Avatar fallback: accent-yellow rounded square with bold initials (first + last) when no photo; person silhouette when no name
  • Avatar upload: clicking the tile opens a local file picker; the uploaded image is stored under ~/.amico/ and served back
  • Three link pills at the bottom: Google Scholar, GitHub, Custom — each with a recognizable icon
  • Filled pills open the URL externally; empty pills render faint/disabled
  • Custom link pill shows user-defined label as tooltip on hover
  • Pencil button (top-right) toggles edit mode: all fields become inputs, link pills show URL inputs, avatar shows upload overlay
  • Affiliation editing retains the institution autocomplete with logo resolution
  • Edits save via POST /amicode/profile on confirm; Escape cancels
  • Empty state (no profile): placeholder avatar + placeholder text in all fields, card opens in edit mode with name auto-focused

Profile endpoint changes

  • GET /amicode/profile response includes new fields: role, description, github, custom_link: {url, label}
  • POST /amicode/profile accepts new fields: role, description, github, custom_link_url, custom_link_label
  • New endpoint POST /amicode/profile-avatar accepts a file upload, stores it, returns the serving path

Onboarding → profile.json bridge

  • ENTITY_FIELDS.profile in onboarding.ts expanded to include description, research_area, experiment_kind (currently silently dropped by sanitizePayload)
  • When onboarding_completed fires, the collected profile fields are written to ~/.amico/profile.json so the dropdown is pre-populated without waiting for the distiller
  • Field mapping: onboarding name → profile name; rolerole; orgaffiliation; research_areafocus; description (handoff) → description
  • The bridge is additive (merges into existing profile.json, never clobbers fields already set by the inline editor)
  • goals, intent, and experiment_kind are recorded in events.jsonl (for the distiller/vault) but do NOT flow to profile.json (they have no dropdown field)

Testing Decisions

Extend existing profile endpoint tests (if any) to cover new fields (role, description, github, custom_link) and the avatar upload endpoint. Add a test that verifies the onboarding-completion bridge writes the expected fields to profile.json. UI components are manually tested via the running app — no new E2E test infrastructure required for this change.

Key Decisions

  • Widget server infra kept — the registry, manifest parser, runtime, frame HTML, and dashboard state manager all remain. Only the client-side rendering layer (grid, frame component, bridge, tokens, allowlist) is deleted. This makes the future side-panel a UI-only addition.
  • Kobalte Popover over manual positioning — cleaner API, auto-positioning, accessibility built-in. Integrates with chrome-dropdown via announceChromeDropdown("profile").
  • Avatar stored locally~/.amico/profile-avatar.<ext>, not a URL. Served by the existing static file serving or a dedicated route. No external image hosting dependency.
  • Three fixed link slots — Scholar, GitHub, Custom. Not a dynamic list. The custom slot has a URL + label (tooltip). This keeps the UI predictable and avoids unbounded link management.
  • No keybindmod+b is removed entirely, not reassigned. The profile dropdown is button-only.
  • Initials fallback on accent yellow — brand-colored tile with dark initials. Falls further back to person silhouette icon when no name is available at all.
  • Onboarding bridge is eager, not distiller-dependentprofile.json is written synchronously on onboarding_completed so the dropdown works immediately. The distiller still materializes the richer vault PROFILE.md in the background (for Amico context), but the UI-facing profile.json doesn't wait for it.
  • Bridge is additive merge — if the user already edited their profile via the dropdown (e.g. set affiliation before onboarding completed), those values are preserved. Onboarding values only fill in empty fields.

Constraints & Invariants

  • The profile popover must not break when the profile endpoint is unreachable (show "Profile unavailable" fallback text)
  • The amicode_author_widget tool must continue to work (preview in chat renders; pin-to-dashboard is a no-op until side panel ships)
  • No new npm dependencies beyond what Kobalte Popover already provides
  • Institution autocomplete and logo resolution reuse the existing implementation from the About You card
  • sanitizePayload must never store credential-looking values (the existing SECRET_RE guard applies to all new fields)
  • The onboarding bridge must not regress the distiller path — events.jsonl remains the source of truth for the vault materialization

Prior Art

  • AboutYouCard in @opencode-ai/ui/amicode/home-cards.tsx — the inline editing UX, institution autocomplete, logo resolution, clipboard bridge
  • GlobalConnectionsPopover in app/src/components/status-popover.tsx — Kobalte Popover pattern with chrome-dropdown coordination
  • chrome-dropdown.ts in app/src/utils/ — the one-at-a-time flyout coordination signal
  • profile.ts in opencode/src/server/amicode/ (and its twin in amicode/packages/extension/src/amicode_service/profile.ts) — the profile data source and save endpoint
  • onboarding.ts in amicode/packages/extension/opencode-plugin/ — the events stream, ENTITY_FIELDS, sanitizePayload, and triggerOnboardingDistill

Source

Design brainstormed in session 2026-08-22.

Notes

  • The widget grid may return as a side-panel feature — the server infra is preserved specifically for that path
  • Chrome strip controls (defaults capsule, sessions flyout, connections popover, settings) are removed from the UI but remain accessible via command palette and status bar; relocation to other surfaces is a separate future decision
  • The profile endpoint exists in TWO codebases: harmoniqs/opencode (the fork, serves the web app) and harmoniqs/amicode (the extension, serves the VS Code webview). The onboarding.ts bridge lives in the amicode extension only. Implementation must update both profile services to accept the new fields, and add the bridge in the amicode extension's onboarding_completed handler.

Metadata

Metadata

Assignees

No one assigned

    Labels

    hitlNeeds a human decision/review

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions