Status: P1–P8 implemented (builds; interactive parity verification ongoing) Supersedes:
macos/(SwiftUI) — kept in parallel until the gpui app reaches feature parity. Single source of truth for the GUI rewrite. The product spec remainsdocs/SPEC.md.
The macOS GUI is currently SwiftUI (macos/). We are rebuilding it in Rust with
gpui (the GPU-accelerated
UI framework from the Zed team). Goals:
- One language across the stack. CLI, TUI, MCP server, and now the GUI are all Rust, so domain types and (potentially) logic are shared instead of re-implemented in Swift.
- Native performance. gpui renders on Metal;
uniform_listvirtualizes the 10k-task list to hit the <100ms target. - Lower maintenance surface. No SPM/Swift toolchain, no duplicated model layer.
| Decision | Choice | Rationale |
|---|---|---|
| Data access | Through the relayd daemon (relay_core::daemon::DaemonClient, auto-spawned) — tool name + JSON over a per-DB Unix socket. Live updates via a subscribe connection (daemon push). |
turso's experimental_multiprocess_wal does not serialize concurrent multi-process access (a held connection exclusively locks -wal). The earlier fix (GUI opens the DB directly with connect-on-demand + app-level flock, live updates via a notify fs-watch) worked but kept per-op open cost and left CLI/MCP contending. Superseded by a single DB-owning daemon (relayd): only it opens the DB (one long-lived connection), and GUI/CLI/MCP are thin socket clients. See docs/DAEMON.md. |
| UI components | gpui-component (longbridge) | gpui core ships no text input, dropdown, or table. gpui-component provides Input (with IME), Dropdown, virtualized Table/List, theming — close to a Linear-like app out of the box. |
| gpui dependency | Pinned git rev of zed-industries/zed (same rev gpui-component tracks) |
main and crates.io 0.2.2 diverge (Application::new() vs gpui_platform::application()). Pinning a rev avoids surprise breakage; gpui-component already tracks main via git. |
| SwiftUI app | Kept in parallel, retired only after parity | Low risk; allows rollback. macos/ stays buildable during the migration. |
┌─────────────┐ tool name + JSON over a per-DB Unix socket ┌──────────────────────┐
│ gpui GUI │ ─────────────────────────────────────────────▶ │ relayd (relayd/ crate)│
│ (gui/ crate)│ ◀──── ChangeEvent push (subscribe conn) ─────── │ owns tasks.turso.db │
└─────────────┘ │ (one long-lived conn)│
▲ └──────────────────────┘
│ same daemon: `rly` CLI/TUI, `rly serve` (MCP for Claude Code) │
└─────────────────────────────────────────────────────────────────┘
The GUI never opens the SQLite/turso file. It launches/supervises the daemon (launchd,
reusing the SwiftUI DaemonManager design) and speaks MCP to it. On any write, the daemon
broadcasts resources/list_changed; the GUI re-fetches affected lists.
relay/
├── shared/ # existing — cross-crate types (RFC3339 time helpers)
├── cli/ # existing — `rly` (CLI + TUI + MCP server)
├── gui/ # NEW — `relay-gui` binary (gpui app)
│ ├── Cargo.toml
│ └── src/
│ ├── main.rs # app bootstrap, window, menus, key bindings
│ ├── app.rs # root view (3-pane), global AppState entity
│ ├── theme.rs # colors/spacing tokens (status & priority palettes)
│ ├── mcp/ # MCP HTTP client (rmcp client transport)
│ │ ├── client.rs # connect, call_tool<T>, resources/list_changed stream
│ │ └── daemon.rs # launchd supervision + health probe (port 7777)
│ ├── store/ # data layer: entities holding fetched state
│ │ ├── mod.rs
│ │ ├── store.rs # Store entity: tasks/projects/tags + refresh on notify
│ │ └── models.rs # serde models mirroring cli/src/models (or re-used)
│ └── views/
│ ├── sidebar.rs # filters / projects / saved views
│ ├── task_list.rs # uniform_list, grouped by status, keyboard nav
│ ├── task_row.rs # status icon, title, priority, due date
│ ├── task_detail.rs # metadata, description, tags, subtasks, comments
│ ├── task_editor.rs # create/edit form (gpui-component Input/Dropdown)
│ ├── popovers.rs # status/priority/due/project/assignee pickers
│ ├── quick_add.rs # Cmd+Shift+Space floating quick-add
│ └── setup.rs # daemon status / endpoint / LaunchAgent panel
└── docs/GUI_GPUI.md # this file
gpui at the pinned Zed rev uses std::hint::cold_path, stabilized in Rust 1.95.0.
The whole workspace is therefore pinned to 1.95.0 via rust-toolchain.toml (the same
version Zed's rev pins). Building with ≤1.94 fails with E0658: unstable feature cold_path.
gpui-component's Icon/IconName SVGs load through the app's AssetSource, not
through gpui_component::init. The app must register gpui-component-assets's Assets
via gpui_platform::application().with_assets(gpui_component_assets::Assets). Without it
every Icon renders blank (the SVG can't be found) while hand-built div shapes and
emoji text still show — a confusing partial-blank UI. The gpui-component-assets dep is
pinned to the same git rev as gpui-component. (Bundled fonts are separate: they go
through cx.text_system().add_fonts, not the AssetSource.)
rmcp ships a first-class streamable-HTTP client; no hand-rolled JSON-RPC needed.
rmcp = { version = "1.8", features = [
"client", "reqwest", "transport-streamable-http-client-reqwest",
] }- Transport:
StreamableHttpClientTransport::from_uri("http://127.0.0.1:7777/mcp"). - Connect:
handler.serve(transport).await?→ aRunningService<RoleClient, _>(the peer). - Call a tool:
client.call_tool(CallToolRequestParams::new("task_list").with_arguments(map)).await?; read JSON fromresult.structured_content(preferred) orresult.content[..].as_text().text. - Live updates: implement
rmcp::handler::client::ClientHandlerand overrideon_resource_list_changed(&self, ctx)— fires on the SSE stream when the daemon broadcasts. Bridge it to gpui by holding a channel sender / weak entity handle in the handler and waking the UI to re-fetch. The whole client API is async and needs a tokio runtime.
The daemon only registers a session for resources/list_changed broadcasts when
that session calls a resource handler (resources/list / resources/read) —
see cli/src/mcp/notify.rs + handler.rs store_peer. A client that calls only
tools is never notified. The GUI therefore calls resources/list once right
after connecting (mcp/client.rs) to opt into live updates. Validated end-to-end:
an external task_create fires on_resource_list_changed → Store::reload.
Daemon version caveat: live updates require the daemon and GUI to use a compatible
rmcp. A daemon built with rmcp 1.5 returns EOF on the client's standalone GET SSE stream (no notifications); rmcp 1.8 on both sides works. The bundledrly(P7) is built from this tree, so they always match. Connect + fetch works regardless of skew; only push notifications are affected.
cli/src/models types derive Serialize/Deserialize. To avoid duplication we will try to
reuse them from relay-cli as a library dependency (the crate already exposes lib.rs).
If pulling in relay-cli drags heavy deps (turso, axum, rmcp server) into the GUI build,
we fall back to a thin models.rs in gui/ that mirrors the JSON shape. Decision is made
in Phase 1 after measuring build impact.
- 3-pane layout: sidebar / task list / detail
- Sidebar: All Tasks, Inbox, Completed filters; Projects (counts); Saved Views
- Task list grouped by status; virtualized (
uniform_list) - Inline status & priority editing (chips in the detail pane)
- Task detail: title/ID/parent, metadata, description, tags, subtasks, links, comments
- Create task (inline form: title) →
task_create - Complete / delete task; status & priority changes
- Search (FTS via
searchtool) + clearable override - Saved views list + apply (
view_list/view_apply) - Quick add (Cmd-N opens the inline create form)
- Setup panel: daemon status, MCP endpoint, client config JSON
- Menus + Quit (Cmd-Q), New Task (Cmd-N)
- Live updates via
resources/list_changed -
.appbundle + bundledrlybinary, ad-hoc signing (gui/bundle.sh)
Deferred / not yet at full SwiftUI parity (follow-ups):
- Markdown rendering of descriptions; comment composer (write)
- Global quick-add panel (Cmd+Shift+Space) with click-to-cycle meta-chip pickers (status / priority / assignee / project) + Cmd+Enter create
- System-wide quick-capture panels (Raycast-style) — two configurable
global shortcuts (
config.tomlquick_capture_hotkeydefaultcmd-ctrl-t,new_task_hotkeydefaultcmd-ctrl-n) that summon a capture panel from any app. Each opens its own floating window (WindowKind::PopUp→ a macOS nonactivatingNSPanelabove the front app; the main Relay window is not brought forward). Quick-add = title + priority; new-task = title + description + priority + hand-off. Enter/⌘Enter creates (fire-and-forget via the shared daemon) and closes; Esc closes. Registered via theglobal-hotkeycrate (macOS CarbonRegisterEventHotKey); presses bridge into gpui to open the panel. Both shortcuts editable in Settings › Quick capture. Seegui/src/views/capture.rs,gui/src/hotkey.rs, and the bridge ingui/src/main.rs. - Board (Kanban) view with drag-and-drop between status columns
- Command palette (Cmd-K) keyboard navigation (↑/↓ move, Enter confirms)
- Inline popovers for due date (quick-add "+ Due" still display-only); tag editing
- Drag-to-reorder within a list; main task-list keyboard navigation
- launchd install from the app; app icon; codesigning + notarization
A screenshot-diff pass against docs/design/* (see the screenshot harness note in
the agent memory) fixed several mockup divergences:
- Icon assets registered — all
IconNameSVGs were blank; see "Icon assets (load-bearing)" above. - Task-row anatomy — order is now checkbox · status · id · title · …priority
(priority moved from the left to the right meta cluster; status moved ahead of
the id), matching
Task List. - Detail status picker — ordered by workflow (Backlog→…→Cancelled) via
TaskStatus::WORKFLOW_ORDER; the list grouping keeps the active-firstORDER. - Project Overview — Milestones and Documents render side by side (1fr 1fr
grid) with Updates full-width below, per
Project Overview. - In-progress status glyph — a clipped half-disc (overflow_hidden on a rounded inner box), matching the design's partial pie.
Known remaining (data/perf-driven, not rendering bugs): the projects list omits per-row health badges and lead avatars when that data is absent.
A second mockup-driven pass (docs/design/{Property Pickers,New Task,New Task - Add Sub-task,Quick Add,Review Inbox,Settings,Task Detail}.dc.html) reworks the
editing/creation surfaces around a single shared component:
- Shared property pickers (
views/property_picker.rs) — one popover family for status / priority / assignee / due-date (presets + month calendar) / project, anchored under a property chip with full keyboard nav (↑↓ move, number-key jump, ↵ confirm, esc close, type-to-filter). Replaces the three duplicated status/priority renderings (detail chips, bulk squares, quick-add cycle chips). All edits funnel throughtask_update(assignee/project/priority/status/due_date/ milestone_id) — no core change needed for the pickers. - Task Detail — meta chips become the edit affordance (open the picker); the always-on STATUS/PRIORITY chip grids are removed from the actions box; title and description gain inline "Edit" affordances.
- Quick Add — meta chips open the shared pickers (was click-to-cycle); the active chip gets the selected/accent style; the "+ Due" chip is now functional.
- New Task modal (
views/new_task.rs, ⌘N) — full 720px creation modal: title, description, property grid (shared pickers), a "Hand off to Relay AI" toggle (setsassignee=ai), and an inline sub-task composer (↵ adds + opens next, ⇥ for per-subtask mini pickers). Replaces the title-only inline create form. Quick Add (⌘⇧Space) stays as the lightweight path; list-group "+" reuses Quick Add with the target status/milestone pre-filled. - Review Inbox — card wall → dense virtualized list rows (compact toolbar header, "Awaiting human confirmation" banner, AI avatar, id+title, one-line summary, PR link indicator, relative timestamp, hover Approve/Send-back).
- Settings → Coding tools — single-tool form → multi-tool gallery (selectable
cards, DEFAULT badge, + Add tool, Save/Duplicate/Delete). Requires
core/src/api.rscoding_tools_view/coding_tools_saveto return/persist the fulltoolsmap instead of collapsing to one (theCodingToolsConfig.toolsBTreeMap already supports it; the GUI save previously dropped sibling tools).
app.rs was split from a ~7,200-line monolith into per-screen modules under
gui/src/views/ (each an impl RelayApp block, or free functions for the
presentational ones). app.rs (~3,150 lines) now holds the RelayApp struct,
the view-state enums, the core state/IO methods, the shared free helpers, and the
Render dispatch. Cross-module access works because the struct fields and the
view-invoked methods are pub(crate).
views/sidebar.rs— sidebar (workspace header / Tasks / Projects / Views / footer)views/toolbar.rs— breadcrumb, project tab bar, view-options + filter popovers, selection (bulk) bar, issues toolbarviews/detail.rs— full-page task detail + comment composerviews/project.rs— project list / overview (burnup) / create-edit formviews/overlays.rs— command palette + quick-add panelviews/settings.rs— settings / setup paneviews/board.rs,views/review_inbox.rs,views/task_list.rs,views/title_bar.rs,views/states.rs— as before
Reads: task_list, task_get, project_list, tag_list, comment_list,
task_list_links, project_list_links, milestone_list, project_list_updates,
project_progress, view_list, view_apply, search.
Writes: task_create, task_update, task_move, task_complete, task_reorder,
task_delete, task_add_tag, task_remove_tag, task_add_dependency,
task_remove_dependency, project_create, project_update, project_archive,
project_delete, project_add_link, project_remove_link, tag_create,
comment_create, view_create, view_delete, task_add_link, task_remove_link.
The project-workspace revamp (docs/SPEC.md §3) adds project-level document links
and a working directory, plus (later phases) updates, milestones, and progress:
project_add_link(project, link_type, url, title?)/project_list_links(project)/project_remove_link(id)— document links on a project (mirrorstask_*_link).project_create/project_updategain aworking_dirfield (local checkout path).project_post_update(project, health, body, author)/project_list_updates(project)(newest first) /project_delete_update(id)— Linear-style status updates (F3).health∈on_track/at_risk/off_track;bodyis markdown. Distinct fromproject_update(which edits project metadata).milestone_create(project, name, description?, target_date?)/milestone_list(project)/milestone_update(id, name?, description?, target_date?, status?)/milestone_delete(id)— project milestones (F4).task_create/task_updategain amilestone_idfield so an issue can be placed in a milestone. The Issues tab can group by Status or Milestone.project_progress(project, from?, to?)— burnup series ({day, scope, started, completed}per day) for a project (F5). Recomputed analytically from task timestamps and cached inproject_progress_snapshot; the Overview pane renders it with a gpui-componentAreaChart. Task gainsstarted_at(set on first InProgress); the HTTP daemon runs a daily snapshot job.
- Entry point: pin a Zed
rev; verifyApplication::new()vsgpui_platform::application()against that rev before writing bootstrap. - State/reactivity:
Entity<Store>holds fetched data; viewscx.observe(&store)and re-render oncx.notify(). No automatic dependency tracking — every mutation calls notify. - Long list:
uniform_list("tasks", count, cx.processor(|this, range, _, _| ...)). - Async:
cx.spawn(async move |weak, cx| { cx.background_executor().spawn(io).await; weak.update(cx, ...) }). MCP calls and thelist_changedsubscription run on background tasks, update entities on the UI thread. - Text input / dropdowns / table: from gpui-component, not hand-rolled.
- Menus:
cx.set_menus([...]), actions viaactions!,KeyBinding::new(...). - Packaging:
cargo-bundle(Zed fork,zed-deploybranch) →.app; bundlerlyatRelay.app/Contents/MacOS/rly; reuse SwiftUI launchd plist scheme (co.driveshaft.relay.daemon).
Each phase is a separate branch (stacked: each off the previous), all
implemented and building. Branches: feat/gui-foundation → feat/gui-data-layer
→ feat/gui-task-list → feat/gui-task-detail → feat/gui-editing →
feat/gui-search-views → feat/gui-packaging → feat/gui-cutover.
| Phase | Branch | Deliverable |
|---|---|---|
| P1 Foundation | feat/gui-foundation |
gui/ crate in workspace; gpui + gpui-component deps build; empty styled window opens; theme tokens; CI builds. |
| P2 Data layer | feat/gui-data-layer |
MCP HTTP client (call_tool<T>, list_changed stream); daemon probe/supervise; Store entity; models decision; smoke test against a running daemon. |
| P3 Task list + sidebar | feat/gui-task-list |
Sidebar (filters/projects/views), virtualized task list grouped by status, keyboard nav, live refresh. Read-only. |
| P4 Task detail | feat/gui-task-detail |
Detail pane: metadata, markdown desc, tags, subtasks, links, comments, activity. |
| P5 Editing | feat/gui-editing |
Create/edit sheets, inline popovers (status/priority/due/project/assignee), reorder, complete, delete — all writes. |
| P6 Search + saved views + quick add | feat/gui-search-views |
FTS search, saved views CRUD, Cmd+Shift+Space quick add. |
| P7 Packaging + setup | feat/gui-packaging |
Menus, shortcuts, Setup window, cargo-bundle .app, bundled rly, launchd, signing; run.sh/install.sh equivalents. |
| P8 Cutover | feat/gui-cutover |
Docs/README updated, retire macos/ (move to legacy/ or delete) once parity verified. |
Phases 3–6 can overlap once P2 lands (independent view work). Sub-agents may own individual
views in parallel against the stable Store API.
- gpui API churn — mitigated by pinning a
revand isolating bootstrap inmain.rs. - Thin docs — learn from
zed/crates/gpui/examples/and gpui-component examples. - IME / Japanese input — rely on gpui-component Input; verify early in P2/P5.
- rmcp client over HTTP — confirm
rmcpexposes a streamable-http client (the daemon uses the server side). If not, use a plainreqwest+ SSE JSON-RPC client. Resolved in P2. - Build weight of depending on
relay-cli— measured in P1/P2; fallback is local models.
## ビルドキャッシュ管理(数十GB対策)
gpui は zed monorepo(git) をビルドし ~1,000 クレート(wgpu/font-kit 含む)をコンパイルするため、
`target/debug` が数十GB化しやすい。緩和策:
- `Cargo.toml [profile.dev] debug = "line-tables-only"` で debuginfo を削減済み(バックトレースは維持)。
- 古い成果物の定期削除に `cargo-sweep` を使う:
- インストール: `cargo install cargo-sweep`
- 30日より古い成果物を削除: `cargo sweep --time 30`
- (実行はユーザー判断。破壊的なので自動化しない。)
- 抜本策の検討経緯は `docs/superpowers/specs/2026-07-25-tauri-poc-design.md` を参照。