diff --git a/CLAUDE.md b/CLAUDE.md
index c92ed49f7..50cc65c15 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -4,8 +4,14 @@ Read and follow [`AGENTS.md`](AGENTS.md) for repository layout, commands, coding
## Design System
-Always read [`DESIGN.md`](DESIGN.md) before making any visual or UI decision.
-All fonts, colors, spacing, layout, the orchestrator-led flow, the topbar, and the
-spawn modal are defined there. The UI matches **emdash** with one main orchestrator and
-a refined-blue accent. Do not deviate without explicit user approval. In QA/review,
-flag any renderer code that doesn't match DESIGN.md.
+Always read [`DESIGN.md`](DESIGN.md) before making any visual or UI decision —
+**start with the "clone agent-orchestrator verbatim" banner at the top**, which
+governs the current look.
+
+The renderer **clones the agent-orchestrator web app verbatim**
+(`~/Projects/agent-orchestrator/packages/web/src`) in looks and design, with a
+refined-blue accent and the terminal keeping its own palette. This **supersedes the
+older "match emdash" framing** in DESIGN.md (per explicit user decision 2026-06-10).
+Build new UI from shadcn primitives (`components/ui/*`) where a component fits. Do not
+deviate without explicit user approval. In QA/review, flag any renderer code that
+diverges from **agent-orchestrator** — do **not** re-flag emdash mismatches.
diff --git a/DESIGN.md b/DESIGN.md
index e255c9349..898758513 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -5,6 +5,43 @@
> - Radix/shadcn + xterm, in `frontend/src/renderer`). Read this before any visual
> or UI change. Created by `/design-consultation` on 2026-06-09.
+## ⚠️ Design direction — clone agent-orchestrator verbatim (SUPERSEDES emdash · 2026-06-10)
+
+By explicit user decision (2026-06-10), the renderer **clones the
+agent-orchestrator web app verbatim** in looks and design. This **supersedes the
+"match emdash" direction** documented in _Aesthetic Direction_ and the palette
+sections below — where they conflict, **agent-orchestrator wins**. Do not re-flag
+"this doesn't match emdash" in QA/review; flag divergence from **agent-orchestrator**.
+
+- **Reference (the user's own app):** `~/Projects/agent-orchestrator/packages/web/src`
+ — `app/globals.css`, `app/mc-board.css`, `app/mc-sidebar.css`,
+ `components/{ProjectSidebar,Dashboard,SessionCard,SessionDetailHeader,SessionInspector,StatusBadge}.tsx`.
+- **Palette (live in `frontend/src/renderer/styles.css` `:root`):** `--bg #0a0b0d`,
+ `--bg-1 #15171b`, `--fg #f4f5f7`, `--fg-muted #9ba1aa`, `--fg-passive #646a73`,
+ hairline white-alpha borders, accent `--accent #4d8dff`; status: working=orange
+ `#f59f4c`, needs-you=amber `#e8c14a`, mergeable=green `#74b98a`, fail=red `#ef6b6b`.
+ The sidebar rail is the cooler `#08090b`.
+- **Cloned surfaces:** the four-column gradient kanban board, the `ProjectSidebar`
+ (brand + project disclosure + nested session rows + Settings menu footer), the
+ session topbar (Kanban back button + identity + breathing `StatusBadge` pill), and
+ the shared `DashboardTopbar`/`DashboardSubhead` chrome (Coding/Reviews tabs · "N
+ working" pill · subhead) reused across board/review/PR/settings.
+- **Build with shadcn primitives** where a component fits (`components/ui/*`:
+ dropdown-menu, select, card, table, tooltip, …); agent-orchestrator's own
+ hand-rolled CSS components are structure/behaviour reference only.
+- The one carried-over divergence still holds: the **accent is refined blue**, and
+ the **terminal keeps its own palette**. Everything else tracks agent-orchestrator.
+- **Approved divergence (2026-06-10):** on macOS, a titlebar cluster (sidebar toggle +
+ back/forward history arrows, `TitlebarNav`) sits beside the traffic lights,
+ VS Code-style — the web reference has no window chrome, so no analogue exists.
+- **Approved divergence (2026-06-10):** the session inspector rail is fully
+ collapsible, built on the shadcn resizable primitive (`pnpm dlx shadcn add
+resizable`, react-resizable-panels v4 `collapsible` panel + imperative API,
+ user-requested). The panel animates to 0% via a flex-grow transition while the
+ content keeps a stable min-width (yyork-style, no mid-animation reflow). Toggled
+ by a `PanelRight` icon button in the session topbar and ⌘⇧B; open state + split
+ width persist. The AO reference keeps the rail always visible.
+
## Product Context
- **What this is:** ReverbCode is an Electron desktop app for supervising many parallel
@@ -44,6 +81,11 @@ ReverbCode is **orchestrator-led**, which is the one thing that differs from emd
## Aesthetic Direction
+> **Superseded (2026-06-10):** see the _Design direction — clone agent-orchestrator
+> verbatim_ banner at the top. The emdash framing below is retained for history; the
+> live look tracks agent-orchestrator (same flat near-black / hairline family, so most
+> of this still reads true).
+
- **Direction:** match **emdash** exactly — flat, near-black, hairline-bordered,
utilitarian. Industrial control surface, calm chrome, the terminal as the center of gravity.
- **Decoration level:** minimal. Type + 1px hairlines do all the work. No gradients,
diff --git a/backend/internal/daemon/daemon.go b/backend/internal/daemon/daemon.go
index 983b95ace..260ccd555 100644
--- a/backend/internal/daemon/daemon.go
+++ b/backend/internal/daemon/daemon.go
@@ -16,6 +16,7 @@ import (
"github.com/aoagents/agent-orchestrator/backend/internal/httpd"
"github.com/aoagents/agent-orchestrator/backend/internal/runfile"
projectsvc "github.com/aoagents/agent-orchestrator/backend/internal/service/project"
+ reviewsvc "github.com/aoagents/agent-orchestrator/backend/internal/service/review"
"github.com/aoagents/agent-orchestrator/backend/internal/storage/sqlite"
"github.com/aoagents/agent-orchestrator/backend/internal/terminal"
)
@@ -105,6 +106,7 @@ func Run() error {
srv, err := httpd.NewWithDeps(cfg, log, termMgr, httpd.APIDeps{
Projects: projectsvc.New(store),
Sessions: sessionSvc,
+ Reviews: reviewsvc.NewInMemory(),
CDC: store,
Events: cdcPipe.Broadcaster,
Activity: lcStack.LCM,
diff --git a/backend/internal/httpd/api.go b/backend/internal/httpd/api.go
index f4e683853..d3969e8b1 100644
--- a/backend/internal/httpd/api.go
+++ b/backend/internal/httpd/api.go
@@ -13,6 +13,7 @@ import (
"github.com/aoagents/agent-orchestrator/backend/internal/httpd/envelope"
prsvc "github.com/aoagents/agent-orchestrator/backend/internal/service/pr"
projectsvc "github.com/aoagents/agent-orchestrator/backend/internal/service/project"
+ reviewsvc "github.com/aoagents/agent-orchestrator/backend/internal/service/review"
)
// APIDeps bundles every service the API layer's controllers depend on.
@@ -21,6 +22,7 @@ type APIDeps struct {
Sessions controllers.SessionService
Activity controllers.ActivityRecorder
PRs prsvc.ActionManager
+ Reviews reviewsvc.Manager
CDC cdc.Source
Events cdcSubscriber
}
@@ -32,6 +34,7 @@ type API struct {
projects *controllers.ProjectsController
sessions *controllers.SessionsController
prs *controllers.PRsController
+ reviews *controllers.ReviewsController
events *EventsController
}
@@ -48,8 +51,9 @@ func NewAPI(cfg config.Config, deps APIDeps) *API {
Svc: deps.Sessions,
Activity: deps.Activity,
},
- prs: &controllers.PRsController{Svc: deps.PRs},
- events: &EventsController{Source: deps.CDC, Live: deps.Events},
+ prs: &controllers.PRsController{Svc: deps.PRs},
+ reviews: &controllers.ReviewsController{Svc: deps.Reviews},
+ events: &EventsController{Source: deps.CDC, Live: deps.Events},
}
}
@@ -70,6 +74,7 @@ func (a *API) Register(root chi.Router) {
a.projects.Register(r)
a.sessions.Register(r)
a.prs.Register(r)
+ a.reviews.Register(r)
// Sibling REST controllers plug in here.
})
// Long-lived streams intentionally bypass the REST timeout middleware.
diff --git a/backend/internal/httpd/apispec/openapi.yaml b/backend/internal/httpd/apispec/openapi.yaml
index a5f472eff..c91c02e35 100644
--- a/backend/internal/httpd/apispec/openapi.yaml
+++ b/backend/internal/httpd/apispec/openapi.yaml
@@ -412,6 +412,95 @@ paths:
summary: Resolve review threads on a pull request
tags:
- prs
+ /api/v1/reviews:
+ get:
+ operationId: listReviews
+ responses:
+ "200":
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ListReviewsResponse'
+ description: OK
+ "501":
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/APIError'
+ description: Not Implemented
+ summary: List code-review runs
+ tags:
+ - reviews
+ /api/v1/reviews/{id}/send:
+ post:
+ operationId: sendReview
+ parameters:
+ - description: Review run id.
+ in: path
+ name: id
+ required: true
+ schema:
+ description: Review run id.
+ type: string
+ responses:
+ "200":
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ReviewResponse'
+ description: OK
+ "404":
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/APIError'
+ description: Not Found
+ "501":
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/APIError'
+ description: Not Implemented
+ summary: Send a review run's findings to its PR
+ tags:
+ - reviews
+ /api/v1/reviews/execute:
+ post:
+ operationId: executeReview
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ExecuteReviewInput'
+ required: true
+ responses:
+ "201":
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ReviewResponse'
+ description: Created
+ "400":
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/APIError'
+ description: Bad Request
+ "422":
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/APIError'
+ description: Unprocessable Entity
+ "501":
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/APIError'
+ description: Not Implemented
+ summary: Start a code-review run for a session's PR
+ tags:
+ - reviews
/api/v1/sessions:
get:
operationId: listSessions
@@ -1077,6 +1166,14 @@ components:
- state
- lastActivityAt
type: object
+ ExecuteReviewInput:
+ properties:
+ sessionId:
+ description: Session whose PR to review.
+ type: string
+ required:
+ - sessionId
+ type: object
KillSessionResponse:
properties:
freed:
@@ -1098,6 +1195,15 @@ components:
required:
- projects
type: object
+ ListReviewsResponse:
+ properties:
+ reviews:
+ items:
+ $ref: '#/components/schemas/ReviewRun'
+ type: array
+ required:
+ - reviews
+ type: object
ListSessionPRsResponse:
properties:
prs:
@@ -1299,6 +1405,54 @@ components:
- sessionId
- session
type: object
+ ReviewFinding:
+ properties:
+ body:
+ type: string
+ id:
+ type: string
+ line:
+ type: integer
+ path:
+ type: string
+ severity:
+ type: string
+ required:
+ - id
+ - path
+ - line
+ - severity
+ - body
+ type: object
+ ReviewResponse:
+ properties:
+ review:
+ $ref: '#/components/schemas/ReviewRun'
+ required:
+ - review
+ type: object
+ ReviewRun:
+ properties:
+ createdAt:
+ format: date-time
+ type: string
+ findings:
+ items:
+ $ref: '#/components/schemas/ReviewFinding'
+ type: array
+ id:
+ type: string
+ sessionId:
+ type: string
+ status:
+ type: string
+ required:
+ - id
+ - sessionId
+ - status
+ - createdAt
+ - findings
+ type: object
RoleOverride:
properties:
agent:
@@ -1535,5 +1689,7 @@ tags:
name: sessions
- description: Pull-request actions (SCM lane)
name: prs
+- description: Code-review runs and findings
+ name: reviews
- description: Server-sent CDC event stream with durable replay
name: events
diff --git a/backend/internal/httpd/apispec/specgen/build.go b/backend/internal/httpd/apispec/specgen/build.go
index c198eddea..502cc2522 100644
--- a/backend/internal/httpd/apispec/specgen/build.go
+++ b/backend/internal/httpd/apispec/specgen/build.go
@@ -61,6 +61,8 @@ func Build() ([]byte, error) {
"Agent session lifecycle and messaging"),
*(&openapi31.Tag{Name: "prs"}).WithDescription(
"Pull-request actions (SCM lane)"),
+ *(&openapi31.Tag{Name: "reviews"}).WithDescription(
+ "Code-review runs and findings"),
*(&openapi31.Tag{Name: "events"}).WithDescription(
"Server-sent CDC event stream with durable replay"),
}
@@ -157,6 +159,13 @@ var schemaNames = map[string]string{
"ControllersMergePRResponse": "MergePRResponse",
"ControllersResolveCommentsRequest": "ResolveCommentsRequest",
"ControllersResolveCommentsResponse": "ResolveCommentsResponse",
+ // httpd/controllers — review wire envelopes
+ "ControllersListReviewsResponse": "ListReviewsResponse",
+ "ControllersExecuteReviewInput": "ExecuteReviewInput",
+ "ControllersReviewResponse": "ReviewResponse",
+ // service/review entities
+ "ReviewRun": "ReviewRun",
+ "ReviewFinding": "ReviewFinding",
// service/project entities + DTOs
"ProjectProject": "Project",
"ProjectSummary": "ProjectSummary",
@@ -242,9 +251,46 @@ func operations() []operation {
ops = append(ops, projectOperations()...)
ops = append(ops, sessionOperations()...)
ops = append(ops, prOperations()...)
+ ops = append(ops, reviewOperations()...)
return ops
}
+// reviewOperations declares the /reviews operations. Must stay 1:1 with the
+// routes ReviewsController.Register mounts (enforced by the parity test).
+func reviewOperations() []operation {
+ return []operation{
+ {
+ method: http.MethodGet, path: "/api/v1/reviews", id: "listReviews", tag: "reviews",
+ summary: "List code-review runs",
+ resps: []respUnit{
+ {http.StatusOK, controllers.ListReviewsResponse{}},
+ {http.StatusNotImplemented, envelope.APIError{}},
+ },
+ },
+ {
+ method: http.MethodPost, path: "/api/v1/reviews/execute", id: "executeReview", tag: "reviews",
+ summary: "Start a code-review run for a session's PR",
+ reqBody: controllers.ExecuteReviewInput{},
+ resps: []respUnit{
+ {http.StatusCreated, controllers.ReviewResponse{}},
+ {http.StatusBadRequest, envelope.APIError{}},
+ {http.StatusUnprocessableEntity, envelope.APIError{}},
+ {http.StatusNotImplemented, envelope.APIError{}},
+ },
+ },
+ {
+ method: http.MethodPost, path: "/api/v1/reviews/{id}/send", id: "sendReview", tag: "reviews",
+ summary: "Send a review run's findings to its PR",
+ pathParams: []any{controllers.ReviewIDParam{}},
+ resps: []respUnit{
+ {http.StatusOK, controllers.ReviewResponse{}},
+ {http.StatusNotFound, envelope.APIError{}},
+ {http.StatusNotImplemented, envelope.APIError{}},
+ },
+ },
+ }
+}
+
type eventsQuery struct {
After *int64 `query:"after,omitempty" minimum:"0" description:"Replay events with seq greater than this cursor. When omitted, clients may send Last-Event-ID instead."`
}
diff --git a/backend/internal/httpd/controllers/reviews.go b/backend/internal/httpd/controllers/reviews.go
new file mode 100644
index 000000000..5d7df6bd8
--- /dev/null
+++ b/backend/internal/httpd/controllers/reviews.go
@@ -0,0 +1,103 @@
+package controllers
+
+import (
+ "encoding/json"
+ "errors"
+ "net/http"
+
+ "github.com/go-chi/chi/v5"
+
+ "github.com/aoagents/agent-orchestrator/backend/internal/httpd/apispec"
+ "github.com/aoagents/agent-orchestrator/backend/internal/httpd/envelope"
+ reviewsvc "github.com/aoagents/agent-orchestrator/backend/internal/service/review"
+)
+
+// ReviewIDParam is the {id} path parameter on the /reviews/{id} routes.
+type ReviewIDParam struct {
+ ID string `path:"id" description:"Review run id."`
+}
+
+// ListReviewsResponse is the body of GET /api/v1/reviews.
+type ListReviewsResponse struct {
+ Reviews []reviewsvc.Run `json:"reviews"`
+}
+
+// ExecuteReviewInput is the body of POST /api/v1/reviews/execute.
+type ExecuteReviewInput struct {
+ SessionID string `json:"sessionId" description:"Session whose PR to review."`
+}
+
+// ReviewResponse is the { review } body of execute (201) and send (200).
+type ReviewResponse struct {
+ Review reviewsvc.Run `json:"review"`
+}
+
+// ReviewsController owns the /reviews routes. A nil Svc returns 501.
+type ReviewsController struct {
+ Svc reviewsvc.Manager
+}
+
+// Register mounts the review routes on the supplied router.
+func (c *ReviewsController) Register(r chi.Router) {
+ r.Get("/reviews", c.list)
+ r.Post("/reviews/execute", c.execute)
+ r.Post("/reviews/{id}/send", c.send)
+}
+
+func (c *ReviewsController) list(w http.ResponseWriter, r *http.Request) {
+ if c.Svc == nil {
+ apispec.NotImplemented(w, r, "GET", "/api/v1/reviews")
+ return
+ }
+ runs, err := c.Svc.List(r.Context())
+ if err != nil {
+ writeReviewError(w, r, err)
+ return
+ }
+ if runs == nil {
+ runs = []reviewsvc.Run{}
+ }
+ envelope.WriteJSON(w, http.StatusOK, ListReviewsResponse{Reviews: runs})
+}
+
+func (c *ReviewsController) execute(w http.ResponseWriter, r *http.Request) {
+ if c.Svc == nil {
+ apispec.NotImplemented(w, r, "POST", "/api/v1/reviews/execute")
+ return
+ }
+ var in ExecuteReviewInput
+ if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
+ envelope.WriteAPIError(w, r, http.StatusBadRequest, "bad_request", "INVALID_BODY", "Invalid request body", nil)
+ return
+ }
+ run, err := c.Svc.Execute(r.Context(), in.SessionID)
+ if err != nil {
+ writeReviewError(w, r, err)
+ return
+ }
+ envelope.WriteJSON(w, http.StatusCreated, ReviewResponse{Review: run})
+}
+
+func (c *ReviewsController) send(w http.ResponseWriter, r *http.Request) {
+ if c.Svc == nil {
+ apispec.NotImplemented(w, r, "POST", "/api/v1/reviews/{id}/send")
+ return
+ }
+ run, err := c.Svc.Send(r.Context(), chi.URLParam(r, "id"))
+ if err != nil {
+ writeReviewError(w, r, err)
+ return
+ }
+ envelope.WriteJSON(w, http.StatusOK, ReviewResponse{Review: run})
+}
+
+func writeReviewError(w http.ResponseWriter, r *http.Request, err error) {
+ switch {
+ case errors.Is(err, reviewsvc.ErrInvalid):
+ envelope.WriteAPIError(w, r, http.StatusUnprocessableEntity, "unprocessable", "REVIEW_INVALID", err.Error(), nil)
+ case errors.Is(err, reviewsvc.ErrNotFound):
+ envelope.WriteAPIError(w, r, http.StatusNotFound, "not_found", "REVIEW_NOT_FOUND", "Unknown review run", nil)
+ default:
+ envelope.WriteAPIError(w, r, http.StatusInternalServerError, "internal", "REVIEW_OPERATION_FAILED", "Review operation failed", nil)
+ }
+}
diff --git a/backend/internal/service/review/review.go b/backend/internal/service/review/review.go
new file mode 100644
index 000000000..2e23c6b86
--- /dev/null
+++ b/backend/internal/service/review/review.go
@@ -0,0 +1,108 @@
+// Package review is the daemon's code-review surface: review runs against a
+// session's PR and the findings they produce.
+//
+// This is an in-memory implementation. Execution is not yet wired to a real
+// review agent — Execute records a pending run so the HTTP surface is live and
+// the dashboard renders against real endpoints; agent-backed findings and
+// persistence are a follow-up. Mirrors agent-orchestrator's reviews feature
+// (packages/web/src/app/api/reviews) on reverbcode's daemon.
+package review
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "sync"
+ "time"
+)
+
+// ErrInvalid and ErrNotFound let the HTTP layer map service failures to 422/404.
+var (
+ ErrInvalid = errors.New("review: invalid input")
+ ErrNotFound = errors.New("review: not found")
+)
+
+// Severity ranks a finding by how much it should block the human; one of
+// "info" | "warning" | "error". Kept as a plain string on the wire.
+const (
+ SeverityInfo = "info"
+ SeverityWarning = "warning"
+ SeverityError = "error"
+)
+
+// Finding is one review comment produced for a run.
+type Finding struct {
+ ID string `json:"id"`
+ Path string `json:"path"`
+ Line int `json:"line"`
+ Severity string `json:"severity"`
+ Body string `json:"body"`
+}
+
+// Run is one code-review execution against a session's PR.
+type Run struct {
+ ID string `json:"id"`
+ SessionID string `json:"sessionId"`
+ // Status is one of: pending | complete | sent.
+ Status string `json:"status"`
+ CreatedAt time.Time `json:"createdAt"`
+ Findings []Finding `json:"findings"`
+}
+
+// Manager is the reviews surface the HTTP controller depends on.
+type Manager interface {
+ List(ctx context.Context) ([]Run, error)
+ Execute(ctx context.Context, sessionID string) (Run, error)
+ Send(ctx context.Context, id string) (Run, error)
+}
+
+type memStore struct {
+ mu sync.Mutex
+ runs map[string]*Run
+ seq int
+}
+
+// NewInMemory returns an in-memory Manager. Runs do not survive a daemon
+// restart.
+func NewInMemory() Manager {
+ return &memStore{runs: map[string]*Run{}}
+}
+
+func (s *memStore) List(_ context.Context) ([]Run, error) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ out := make([]Run, 0, len(s.runs))
+ for _, run := range s.runs {
+ out = append(out, *run)
+ }
+ return out, nil
+}
+
+func (s *memStore) Execute(_ context.Context, sessionID string) (Run, error) {
+ if sessionID == "" {
+ return Run{}, fmt.Errorf("%w: sessionId is required", ErrInvalid)
+ }
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.seq++
+ run := &Run{
+ ID: fmt.Sprintf("rev-%d", s.seq),
+ SessionID: sessionID,
+ Status: "pending",
+ CreatedAt: time.Now().UTC(),
+ Findings: []Finding{},
+ }
+ s.runs[run.ID] = run
+ return *run, nil
+}
+
+func (s *memStore) Send(_ context.Context, id string) (Run, error) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ run, ok := s.runs[id]
+ if !ok {
+ return Run{}, fmt.Errorf("%w: review %q", ErrNotFound, id)
+ }
+ run.Status = "sent"
+ return *run, nil
+}
diff --git a/frontend/.npmrc b/frontend/.npmrc
new file mode 100644
index 000000000..b05bfeefc
--- /dev/null
+++ b/frontend/.npmrc
@@ -0,0 +1,2 @@
+node-linker=hoisted
+minimum-release-age=0
diff --git a/frontend/components.json b/frontend/components.json
new file mode 100644
index 000000000..febd8071e
--- /dev/null
+++ b/frontend/components.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "src/renderer/styles.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "iconLibrary": "lucide",
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ }
+}
diff --git a/frontend/forge.config.ts b/frontend/forge.config.ts
index c310a7ca4..3cdccc83a 100644
--- a/frontend/forge.config.ts
+++ b/frontend/forge.config.ts
@@ -14,7 +14,6 @@ const config: ForgeConfig = {
osxSign: process.env.CSC_LINK ? {} : undefined,
osxNotarize: process.env.APPLE_ID
? {
- tool: "notarytool",
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD!,
teamId: process.env.APPLE_TEAM_ID!,
@@ -24,7 +23,7 @@ const config: ForgeConfig = {
rebuildConfig: {},
makers: [
{ name: "@electron-forge/maker-squirrel", config: { name: "AgentOrchestrator" } },
- { name: "@electron-forge/maker-zip", platforms: ["darwin"] },
+ { name: "@electron-forge/maker-zip", platforms: ["darwin"], config: {} },
{ name: "@electron-forge/maker-deb", config: {} },
{ name: "@electron-forge/maker-rpm", config: {} },
],
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 26b975e29..63d6ce8ca 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -17,6 +17,7 @@
"@xterm/addon-canvas": "^0.7.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-search": "^0.15.0",
+ "@xterm/addon-unicode11": "^0.9.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/addon-webgl": "^0.19.0",
"@xterm/xterm": "^5.5.0",
@@ -24,6 +25,7 @@
"clsx": "^2.1.1",
"lucide-react": "^1.17.0",
"openapi-fetch": "^0.17.0",
+ "radix-ui": "^1.5.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-resizable-panels": "^4.11.2",
@@ -2083,7 +2085,7 @@
"node_modules/@electron/node-gyp": {
"version": "10.2.0-electron.1",
"resolved": "git+ssh://git@github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2",
- "integrity": "sha512-CrYo6TntjpoMO1SHjl5Pa/JoUsECNqNdB7Kx49WLQpWzPw53eEITJ2Hs9fh/ryUYDn4pxZz11StaBYBrLFJdqg==",
+ "integrity": "sha512-lBSgDMQqt7QWMuIjS8zNAq5FI5o5RVBAcJUGWGI6GgoQITJt3msAkUrHp8YHj3RTVE+h70ndqMGqURjp3IfRyQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3709,12 +3711,100 @@
"node": ">=18"
}
},
+ "node_modules/@radix-ui/number": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.2.tgz",
+ "integrity": "sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==",
+ "license": "MIT"
+ },
"node_modules/@radix-ui/primitive": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.4.tgz",
"integrity": "sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==",
"license": "MIT"
},
+ "node_modules/@radix-ui/react-accessible-icon": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.9.tgz",
+ "integrity": "sha512-5W9KzJz/3DeYbGJHbZv8Q6AkxMOKUmALfc+PRg9dWwJZMk6zD37Sz8sZrF7UD6CBkiJvn7dNeRzn5G7XiCMyig==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-visually-hidden": "1.2.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-accordion": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.13.tgz",
+ "integrity": "sha512-xITxBB2p5m5tAe7M0F95kb4uAh7jSIKGlExMEm93HlW+XxZHV2eXFbPWLktd4JhRiwcnXNbO7iekcrbZy6ZCvA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collapsible": "1.1.13",
+ "@radix-ui/react-collection": "1.1.9",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-controllable-state": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-alert-dialog": {
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.16.tgz",
+ "integrity": "sha512-vPaIgo0mxYlvcFaM9jB2Uot9TjGXMuAPEvrc6BOLeV+I5U8s1dkIoouYaa6lmSfc5SPMo5x5djOTOTvaigdGMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-dialog": "1.1.16",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-slot": "1.2.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-arrow": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.9.tgz",
@@ -3738,6 +3828,116 @@
}
}
},
+ "node_modules/@radix-ui/react-aspect-ratio": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.9.tgz",
+ "integrity": "sha512-Xy+Dpxt/5n9rVTdPrNFmf8GwG1NlT1pzCF/z1MgOGZMLZWdWl+km+ZRWGQAPEhbkzSwYEsfYmTca8NhUtVxqnw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-avatar": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.12.tgz",
+ "integrity": "sha512-NQCQyWC7QrDPhjMn8hUqFeU0lUrprIgm1AyMgLbzuQJibNnatdc3SSMo3/UGFu/eUkJUU1cEcKCnyhXTQzq6tA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-is-hydrated": "0.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-checkbox": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.4.tgz",
+ "integrity": "sha512-m3JmIOAX5ZzZ6VPjxEU2dbTOhoHi0nT5riwcDwe8idocsWf4a5DXJLDtZ6LfJwMBx7W+A2b7kp2TgPEKtaiF6A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-previous": "1.1.2",
+ "@radix-ui/react-use-size": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collapsible": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.13.tgz",
+ "integrity": "sha512-F0s8+p2XNpfc3k02zBfB0jPWbkHVG162+p7BdUMyJ2308QMqZ+oaclX+FAzKFovgL5OqRU+Rvy6f/vbdlJVaqA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-collection": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.9.tgz",
@@ -3794,6 +3994,33 @@
}
}
},
+ "node_modules/@radix-ui/react-context-menu": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.3.0.tgz",
+ "integrity": "sha512-d7CouXhAW+CGmFOqmB+IEvd3E9GcaqfgvfjCc3hfulp2pkaUCEVEGa0SN5nNWYA+IvQ6g1Pt+S5dpNn1AoY9hg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-menu": "2.1.17",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-controllable-state": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-dialog": {
"version": "1.1.16",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.16.tgz",
@@ -3872,6 +4099,35 @@
}
}
},
+ "node_modules/@radix-ui/react-dropdown-menu": {
+ "version": "2.1.17",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.17.tgz",
+ "integrity": "sha512-S6b3Jm57sY5EdDyOMLkacbB0qMnKhy1RCKZCt795ZkmtUOAvojYIZ5p7dXHIh5Cyr3jCLLI5/g64V3FKLudZmw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-menu": "2.1.17",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-controllable-state": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-focus-guards": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.4.tgz",
@@ -3912,6 +4168,65 @@
}
}
},
+ "node_modules/@radix-ui/react-form": {
+ "version": "0.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.9.tgz",
+ "integrity": "sha512-eTPyThIKDacJ3mJDvYwf/PSmsEYlOyA2Qcb+aGyWwYv+P5w57VPUkMVA2XJ9z0Du2KBY1HoHQzhPV9iYL/r4hg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-label": "2.1.9",
+ "@radix-ui/react-primitive": "2.1.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-hover-card": {
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.16.tgz",
+ "integrity": "sha512-hAileDBtd6CX7nlZOarOnISQ6PP4q0e16BX51ulzdZ+7IzjL0sDTVpFdmSYrIjw6zVNsfQBao5gG6AWr3qwfvA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-dismissable-layer": "1.1.12",
+ "@radix-ui/react-popper": "1.3.0",
+ "@radix-ui/react-portal": "1.1.11",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-controllable-state": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-id": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.2.tgz",
@@ -3930,6 +4245,238 @@
}
}
},
+ "node_modules/@radix-ui/react-label": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.9.tgz",
+ "integrity": "sha512-rDoTeMbCwRVcnmo7NGT9IlPo1yXmEI+xc1URP3oeewwZEV4mdTp1dYUhYbQdo4D1q2SjKVvv4N1gNY77QAQtjA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menu": {
+ "version": "2.1.17",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.17.tgz",
+ "integrity": "sha512-fmbNnFyf+JYCN0DhhWnEdUTDnZD1mXaPQWivdsPIb8oOSbARfD3LIQJbLCG8a8QLCwoMxiJ7GVPIFcC8Dw8v2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collection": "1.1.9",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.12",
+ "@radix-ui/react-focus-guards": "1.1.4",
+ "@radix-ui/react-focus-scope": "1.1.9",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-popper": "1.3.0",
+ "@radix-ui/react-portal": "1.1.11",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-roving-focus": "1.1.12",
+ "@radix-ui/react-slot": "1.2.5",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.7.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menubar": {
+ "version": "1.1.17",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.17.tgz",
+ "integrity": "sha512-AKtZ4O782yO7qwIyq73WpulYt1IHhQ0htDb6wNcxzxnSDCcSWMVBiU9ycpcA90XzQO4IVIxIErtak6Kg/Vt0rQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collection": "1.1.9",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-menu": "2.1.17",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-roving-focus": "1.1.12",
+ "@radix-ui/react-use-controllable-state": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-navigation-menu": {
+ "version": "1.2.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.15.tgz",
+ "integrity": "sha512-/fS8hKCcRt4DwCGa5QIB3juRXmfYSOk4a2AEe/BDIyy7Hm+eje2Y13oUx5zejl+wFt1owrM7E8NWlbaEl5EGpg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collection": "1.1.9",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.12",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-layout-effect": "1.1.2",
+ "@radix-ui/react-use-previous": "1.1.2",
+ "@radix-ui/react-visually-hidden": "1.2.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-one-time-password-field": {
+ "version": "0.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.9.tgz",
+ "integrity": "sha512-fvCzA9hm7yN5xxTPJIi4VhSmH5gv+76ILsxguBK3cm3icD5BR4vW7POQmu8Zio0yh91uuouG/Kang40IbMkaSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.2",
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collection": "1.1.9",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-roving-focus": "1.1.12",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-effect-event": "0.0.3",
+ "@radix-ui/react-use-is-hydrated": "0.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-password-toggle-field": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.4.tgz",
+ "integrity": "sha512-qoDSkObZ9faJlsjlwyBH6ia7kq9vaJ2QwWTowT3nQpzPvUTAKesmWuGJYpd91HIoJqS+5ZPXy5uFPp+HlwdaAg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-effect-event": "0.0.3",
+ "@radix-ui/react-use-is-hydrated": "0.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover": {
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.16.tgz",
+ "integrity": "sha512-8brVpAU5Uq7Bh0c8EFc4ZTf2JJTYn0o+1L+CUJB3UYIOkTjKGMgoHvduylrahdmNlr3DfH0rFq2DrbNZXgaspw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-dismissable-layer": "1.1.12",
+ "@radix-ui/react-focus-guards": "1.1.4",
+ "@radix-ui/react-focus-scope": "1.1.9",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-popper": "1.3.0",
+ "@radix-ui/react-portal": "1.1.11",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-slot": "1.2.5",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.7.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-popper": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.0.tgz",
@@ -4032,6 +4579,62 @@
}
}
},
+ "node_modules/@radix-ui/react-progress": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.9.tgz",
+ "integrity": "sha512-+EOkvg1Zn1vI1+fRDfRSAiJ7BWfcDAo5ASMmbqrcLZ4s4USk2FGkoHgeb2X+CkUgo2zJMiyObwf1k44CrRWsyw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-radio-group": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.4.0.tgz",
+ "integrity": "sha512-eHdV5bLx9sH+tBnbDjkIBdvQEH/c6MEtQYhTbxkaDK9qsIFFLtmJYEQFVdwhnruWotLfQmIuWEL/J+L3utE8rQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-roving-focus": "1.1.12",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-previous": "1.1.2",
+ "@radix-ui/react-use-size": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.12.tgz",
@@ -4063,6 +4666,137 @@
}
}
},
+ "node_modules/@radix-ui/react-scroll-area": {
+ "version": "1.2.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.11.tgz",
+ "integrity": "sha512-DS39ziOgea75U/TrXKU2/oKp0be2jrDHnzFLvahg/0iNAT1Zq16e4Uw0WXwyXvsK+mG3BRyMb7A3NRZMDuEXtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.2",
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.3.0.tgz",
+ "integrity": "sha512-mENc7WpJvJcW8hlMpzfFcHcEhTvYS5JMBmi9HVC1Q00uhBwML086MHYUV8QQdQv6lcu0Wg8dzd1RB8AFADcG/g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.2",
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collection": "1.1.9",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.12",
+ "@radix-ui/react-focus-guards": "1.1.4",
+ "@radix-ui/react-focus-scope": "1.1.9",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-popper": "1.3.0",
+ "@radix-ui/react-portal": "1.1.11",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-slot": "1.2.5",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-layout-effect": "1.1.2",
+ "@radix-ui/react-use-previous": "1.1.2",
+ "@radix-ui/react-visually-hidden": "1.2.5",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.7.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-separator": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.9.tgz",
+ "integrity": "sha512-gvgW+JV/Mbjj6darztTetnmElpQEzZrXpJvfj+dOxNAxiyHEAyUvEjjl4zxblvmjmKmi3jfPoy7ZdxzCuUBJSA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slider": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.4.0.tgz",
+ "integrity": "sha512-RHcPlLOThRJM51DSIC33ZnpDEBYhyEFroVWkd2P54PGGjkmAt14RboYUU9E1MFst666zFHM0tGtWvMjSOtU1pw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.2",
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collection": "1.1.9",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-layout-effect": "1.1.2",
+ "@radix-ui/react-use-previous": "1.1.2",
+ "@radix-ui/react-use-size": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-slot": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.5.tgz",
@@ -4081,6 +4815,35 @@
}
}
},
+ "node_modules/@radix-ui/react-switch": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.3.0.tgz",
+ "integrity": "sha512-GP1EZwhoZO/GGnhM1P5/2Vpm8iN8EnngyU0oezn2l78kN8tj25pyrvjIaT7azBhK615KSt+P2w39y57YV5jVkA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-previous": "1.1.2",
+ "@radix-ui/react-use-size": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-tabs": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.14.tgz",
@@ -4111,6 +4874,123 @@
}
}
},
+ "node_modules/@radix-ui/react-toast": {
+ "version": "1.2.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.16.tgz",
+ "integrity": "sha512-WUymDDiN2DpoGudRN1aW4wF5O3BNQjZZO/5nngPoNiEVqjyOzirvZZNO0R6dC1ifucSINVaSv8JX1aq47VGgiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collection": "1.1.9",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-dismissable-layer": "1.1.12",
+ "@radix-ui/react-portal": "1.1.11",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-layout-effect": "1.1.2",
+ "@radix-ui/react-visually-hidden": "1.2.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toggle": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.11.tgz",
+ "integrity": "sha512-FikrKJemoBGZQ6uRID0HJqSPBP6D7OppdD2OhLl0ZYLlAyPXI7MezoYGmumwNkrAoRm35xXkb4C8JPfJZZzcaw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-controllable-state": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toggle-group": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.12.tgz",
+ "integrity": "sha512-TEgECgJaWGAHJJZGzNNEYTNBdIXqX7LchANycpyP7DkfjmuiSN7ISt1k/ZRGVJgVJonsgP4vwaiKMn5utrcwWQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-roving-focus": "1.1.12",
+ "@radix-ui/react-toggle": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toolbar": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.12.tgz",
+ "integrity": "sha512-4wHtJVdIgqMmEwUvxA0BYg/2JMRbt0L3+8UD8Ml/nhKkfXtiZcM8u/S15gQ5xj9YEd/0qlrm5bE805LsjQ+J8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-roving-focus": "1.1.12",
+ "@radix-ui/react-separator": "1.1.9",
+ "@radix-ui/react-toggle-group": "1.1.12"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-tooltip": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.9.tgz",
@@ -4215,6 +5095,21 @@
}
}
},
+ "node_modules/@radix-ui/react-use-is-hydrated": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.1.tgz",
+ "integrity": "sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz",
@@ -4230,6 +5125,21 @@
}
}
},
+ "node_modules/@radix-ui/react-use-previous": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.2.tgz",
+ "integrity": "sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-use-rect": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.2.tgz",
@@ -5777,6 +6687,12 @@
"@xterm/xterm": "^5.0.0"
}
},
+ "node_modules/@xterm/addon-unicode11": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0.tgz",
+ "integrity": "sha512-FxDnYcyuXhNl+XSqGZL/t0U9eiNb/q3EWT5rYkQT/zuig8Gz/VagnQANKHdDWFM2lTMk9ly0EFQxxxtZUoRetw==",
+ "license": "MIT"
+ },
"node_modules/@xterm/addon-web-links": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.11.0.tgz",
@@ -6523,9 +7439,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001797",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz",
- "integrity": "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==",
+ "version": "1.0.30001799",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
+ "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==",
"dev": true,
"funding": [
{
@@ -11111,6 +12027,83 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/radix-ui": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.5.0.tgz",
+ "integrity": "sha512-Nzh2HNpClgB31FBHRqt2xG8XNUfVfQRpf34hACC5PNrXTd5JdXdqOXwLs3BL+D8CNYiNQiJiT8QGr5Q4vq+00w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-accessible-icon": "1.1.9",
+ "@radix-ui/react-accordion": "1.2.13",
+ "@radix-ui/react-alert-dialog": "1.1.16",
+ "@radix-ui/react-arrow": "1.1.9",
+ "@radix-ui/react-aspect-ratio": "1.1.9",
+ "@radix-ui/react-avatar": "1.1.12",
+ "@radix-ui/react-checkbox": "1.3.4",
+ "@radix-ui/react-collapsible": "1.1.13",
+ "@radix-ui/react-collection": "1.1.9",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-context-menu": "2.3.0",
+ "@radix-ui/react-dialog": "1.1.16",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.12",
+ "@radix-ui/react-dropdown-menu": "2.1.17",
+ "@radix-ui/react-focus-guards": "1.1.4",
+ "@radix-ui/react-focus-scope": "1.1.9",
+ "@radix-ui/react-form": "0.1.9",
+ "@radix-ui/react-hover-card": "1.1.16",
+ "@radix-ui/react-label": "2.1.9",
+ "@radix-ui/react-menu": "2.1.17",
+ "@radix-ui/react-menubar": "1.1.17",
+ "@radix-ui/react-navigation-menu": "1.2.15",
+ "@radix-ui/react-one-time-password-field": "0.1.9",
+ "@radix-ui/react-password-toggle-field": "0.1.4",
+ "@radix-ui/react-popover": "1.1.16",
+ "@radix-ui/react-popper": "1.3.0",
+ "@radix-ui/react-portal": "1.1.11",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-progress": "1.1.9",
+ "@radix-ui/react-radio-group": "1.4.0",
+ "@radix-ui/react-roving-focus": "1.1.12",
+ "@radix-ui/react-scroll-area": "1.2.11",
+ "@radix-ui/react-select": "2.3.0",
+ "@radix-ui/react-separator": "1.1.9",
+ "@radix-ui/react-slider": "1.4.0",
+ "@radix-ui/react-slot": "1.2.5",
+ "@radix-ui/react-switch": "1.3.0",
+ "@radix-ui/react-tabs": "1.1.14",
+ "@radix-ui/react-toast": "1.2.16",
+ "@radix-ui/react-toggle": "1.1.11",
+ "@radix-ui/react-toggle-group": "1.1.12",
+ "@radix-ui/react-toolbar": "1.1.12",
+ "@radix-ui/react-tooltip": "1.2.9",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-effect-event": "0.0.3",
+ "@radix-ui/react-use-escape-keydown": "1.1.2",
+ "@radix-ui/react-use-is-hydrated": "0.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.2",
+ "@radix-ui/react-use-size": "1.1.2",
+ "@radix-ui/react-visually-hidden": "1.2.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react": {
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
@@ -12789,13 +13782,12 @@
}
},
"node_modules/watchpack": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz",
- "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==",
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz",
+ "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.1.2"
},
"engines": {
diff --git a/frontend/package.json b/frontend/package.json
index 35ce2bfab..545e4276c 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -16,7 +16,7 @@
"make": "electron-forge make",
"publish": "electron-forge publish",
"typecheck": "tsc --noEmit",
- "test": "vitest run",
+ "test": "vitest run --config vite.renderer.config.ts",
"test:e2e": "playwright test",
"api:ts": "openapi-typescript ../backend/internal/httpd/apispec/openapi.yaml -o src/api/schema.ts"
},
@@ -56,6 +56,7 @@
"@xterm/addon-canvas": "^0.7.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-search": "^0.15.0",
+ "@xterm/addon-unicode11": "^0.9.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/addon-webgl": "^0.19.0",
"@xterm/xterm": "^5.5.0",
@@ -63,6 +64,7 @@
"clsx": "^2.1.1",
"lucide-react": "^1.17.0",
"openapi-fetch": "^0.17.0",
+ "radix-ui": "^1.5.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-resizable-panels": "^4.11.2",
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
index 047e17e2a..348392a28 100644
--- a/frontend/pnpm-lock.yaml
+++ b/frontend/pnpm-lock.yaml
@@ -34,6 +34,9 @@ importers:
"@xterm/addon-search":
specifier: ^0.15.0
version: 0.15.0(@xterm/xterm@5.5.0)
+ "@xterm/addon-unicode11":
+ specifier: ^0.9.0
+ version: 0.9.0
"@xterm/addon-web-links":
specifier: ^0.11.0
version: 0.11.0(@xterm/xterm@5.5.0)
@@ -55,6 +58,9 @@ importers:
openapi-fetch:
specifier: ^0.17.0
version: 0.17.0
+ radix-ui:
+ specifier: ^1.5.0
+ version: 1.5.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
react:
specifier: ^19.2.7
version: 19.2.7
@@ -422,7 +428,6 @@ packages:
resolution:
{
gitHosted: true,
- integrity: sha512-MXgzlTDEEndJB3TBbvd5uFQO/8gaINo1Hfen8vef5rq/VHVPeB63uuv/uO5+8GFsAJ/rauu6XB79S6K4+aXc+w==,
tarball: https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2,
}
version: 10.2.0-electron.1
@@ -747,10 +752,56 @@ packages:
engines: { node: ">=18" }
hasBin: true
+ "@radix-ui/number@1.1.2":
+ resolution:
+ { integrity: sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig== }
+
"@radix-ui/primitive@1.1.4":
resolution:
{ integrity: sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ== }
+ "@radix-ui/react-accessible-icon@1.1.9":
+ resolution:
+ { integrity: sha512-5W9KzJz/3DeYbGJHbZv8Q6AkxMOKUmALfc+PRg9dWwJZMk6zD37Sz8sZrF7UD6CBkiJvn7dNeRzn5G7XiCMyig== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-accordion@1.2.13":
+ resolution:
+ { integrity: sha512-xITxBB2p5m5tAe7M0F95kb4uAh7jSIKGlExMEm93HlW+XxZHV2eXFbPWLktd4JhRiwcnXNbO7iekcrbZy6ZCvA== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-alert-dialog@1.1.16":
+ resolution:
+ { integrity: sha512-vPaIgo0mxYlvcFaM9jB2Uot9TjGXMuAPEvrc6BOLeV+I5U8s1dkIoouYaa6lmSfc5SPMo5x5djOTOTvaigdGMQ== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
"@radix-ui/react-arrow@1.1.9":
resolution:
{ integrity: sha512-yqHW5WQ/cTpU/un7dqqIKNy2iRU8BC0JB78PEzTfCCYvZu1U6W9KwObAniMk9nhSfyotKPQTYaUD/HB0f5muig== }
@@ -765,6 +816,62 @@ packages:
"@types/react-dom":
optional: true
+ "@radix-ui/react-aspect-ratio@1.1.9":
+ resolution:
+ { integrity: sha512-Xy+Dpxt/5n9rVTdPrNFmf8GwG1NlT1pzCF/z1MgOGZMLZWdWl+km+ZRWGQAPEhbkzSwYEsfYmTca8NhUtVxqnw== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-avatar@1.1.12":
+ resolution:
+ { integrity: sha512-NQCQyWC7QrDPhjMn8hUqFeU0lUrprIgm1AyMgLbzuQJibNnatdc3SSMo3/UGFu/eUkJUU1cEcKCnyhXTQzq6tA== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-checkbox@1.3.4":
+ resolution:
+ { integrity: sha512-m3JmIOAX5ZzZ6VPjxEU2dbTOhoHi0nT5riwcDwe8idocsWf4a5DXJLDtZ6LfJwMBx7W+A2b7kp2TgPEKtaiF6A== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-collapsible@1.1.13":
+ resolution:
+ { integrity: sha512-F0s8+p2XNpfc3k02zBfB0jPWbkHVG162+p7BdUMyJ2308QMqZ+oaclX+FAzKFovgL5OqRU+Rvy6f/vbdlJVaqA== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
"@radix-ui/react-collection@1.1.9":
resolution:
{ integrity: sha512-zuSVi7ziP7uQRqc+yGxsKJfNkdyHv3ZKDaHe0gzg4dRgws96TPKWIiz84tVHP4GEcEl8bC0mdt17NkcxaJHmaQ== }
@@ -789,6 +896,20 @@ packages:
"@types/react":
optional: true
+ "@radix-ui/react-context-menu@2.3.0":
+ resolution:
+ { integrity: sha512-d7CouXhAW+CGmFOqmB+IEvd3E9GcaqfgvfjCc3hfulp2pkaUCEVEGa0SN5nNWYA+IvQ6g1Pt+S5dpNn1AoY9hg== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
"@radix-ui/react-context@1.1.4":
resolution:
{ integrity: sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg== }
@@ -837,6 +958,20 @@ packages:
"@types/react-dom":
optional: true
+ "@radix-ui/react-dropdown-menu@2.1.17":
+ resolution:
+ { integrity: sha512-S6b3Jm57sY5EdDyOMLkacbB0qMnKhy1RCKZCt795ZkmtUOAvojYIZ5p7dXHIh5Cyr3jCLLI5/g64V3FKLudZmw== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
"@radix-ui/react-focus-guards@1.1.4":
resolution:
{ integrity: sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q== }
@@ -861,6 +996,34 @@ packages:
"@types/react-dom":
optional: true
+ "@radix-ui/react-form@0.1.9":
+ resolution:
+ { integrity: sha512-eTPyThIKDacJ3mJDvYwf/PSmsEYlOyA2Qcb+aGyWwYv+P5w57VPUkMVA2XJ9z0Du2KBY1HoHQzhPV9iYL/r4hg== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-hover-card@1.1.16":
+ resolution:
+ { integrity: sha512-hAileDBtd6CX7nlZOarOnISQ6PP4q0e16BX51ulzdZ+7IzjL0sDTVpFdmSYrIjw6zVNsfQBao5gG6AWr3qwfvA== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
"@radix-ui/react-id@1.1.2":
resolution:
{ integrity: sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA== }
@@ -871,6 +1034,104 @@ packages:
"@types/react":
optional: true
+ "@radix-ui/react-label@2.1.9":
+ resolution:
+ { integrity: sha512-rDoTeMbCwRVcnmo7NGT9IlPo1yXmEI+xc1URP3oeewwZEV4mdTp1dYUhYbQdo4D1q2SjKVvv4N1gNY77QAQtjA== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-menu@2.1.17":
+ resolution:
+ { integrity: sha512-fmbNnFyf+JYCN0DhhWnEdUTDnZD1mXaPQWivdsPIb8oOSbARfD3LIQJbLCG8a8QLCwoMxiJ7GVPIFcC8Dw8v2Q== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-menubar@1.1.17":
+ resolution:
+ { integrity: sha512-AKtZ4O782yO7qwIyq73WpulYt1IHhQ0htDb6wNcxzxnSDCcSWMVBiU9ycpcA90XzQO4IVIxIErtak6Kg/Vt0rQ== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-navigation-menu@1.2.15":
+ resolution:
+ { integrity: sha512-/fS8hKCcRt4DwCGa5QIB3juRXmfYSOk4a2AEe/BDIyy7Hm+eje2Y13oUx5zejl+wFt1owrM7E8NWlbaEl5EGpg== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-one-time-password-field@0.1.9":
+ resolution:
+ { integrity: sha512-fvCzA9hm7yN5xxTPJIi4VhSmH5gv+76ILsxguBK3cm3icD5BR4vW7POQmu8Zio0yh91uuouG/Kang40IbMkaSQ== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-password-toggle-field@0.1.4":
+ resolution:
+ { integrity: sha512-qoDSkObZ9faJlsjlwyBH6ia7kq9vaJ2QwWTowT3nQpzPvUTAKesmWuGJYpd91HIoJqS+5ZPXy5uFPp+HlwdaAg== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-popover@1.1.16":
+ resolution:
+ { integrity: sha512-8brVpAU5Uq7Bh0c8EFc4ZTf2JJTYn0o+1L+CUJB3UYIOkTjKGMgoHvduylrahdmNlr3DfH0rFq2DrbNZXgaspw== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
"@radix-ui/react-popper@1.3.0":
resolution:
{ integrity: sha512-9PB589e1aWZbrlFUHdz6WiPCL+xLZHQFX7oibqG/6Q0SwOkxDyQX9W/cyPa+sAPPKuC8cpLCpRczE5a/1DiwVQ== }
@@ -927,6 +1188,34 @@ packages:
"@types/react-dom":
optional: true
+ "@radix-ui/react-progress@1.1.9":
+ resolution:
+ { integrity: sha512-+EOkvg1Zn1vI1+fRDfRSAiJ7BWfcDAo5ASMmbqrcLZ4s4USk2FGkoHgeb2X+CkUgo2zJMiyObwf1k44CrRWsyw== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-radio-group@1.4.0":
+ resolution:
+ { integrity: sha512-eHdV5bLx9sH+tBnbDjkIBdvQEH/c6MEtQYhTbxkaDK9qsIFFLtmJYEQFVdwhnruWotLfQmIuWEL/J+L3utE8rQ== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
"@radix-ui/react-roving-focus@1.1.12":
resolution:
{ integrity: sha512-FvgPt1bRmg8Xt2QpF7NUZW3dE0ZQHGm41dAdgT2J2GJPoIXz+9Em3NobAxf4fupcxhgHu03E5CRiU2MWvObXyg== }
@@ -941,6 +1230,62 @@ packages:
"@types/react-dom":
optional: true
+ "@radix-ui/react-scroll-area@1.2.11":
+ resolution:
+ { integrity: sha512-DS39ziOgea75U/TrXKU2/oKp0be2jrDHnzFLvahg/0iNAT1Zq16e4Uw0WXwyXvsK+mG3BRyMb7A3NRZMDuEXtQ== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-select@2.3.0":
+ resolution:
+ { integrity: sha512-mENc7WpJvJcW8hlMpzfFcHcEhTvYS5JMBmi9HVC1Q00uhBwML086MHYUV8QQdQv6lcu0Wg8dzd1RB8AFADcG/g== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-separator@1.1.9":
+ resolution:
+ { integrity: sha512-gvgW+JV/Mbjj6darztTetnmElpQEzZrXpJvfj+dOxNAxiyHEAyUvEjjl4zxblvmjmKmi3jfPoy7ZdxzCuUBJSA== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-slider@1.4.0":
+ resolution:
+ { integrity: sha512-RHcPlLOThRJM51DSIC33ZnpDEBYhyEFroVWkd2P54PGGjkmAt14RboYUU9E1MFst666zFHM0tGtWvMjSOtU1pw== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
"@radix-ui/react-slot@1.2.5":
resolution:
{ integrity: sha512-rCMO3QsIVKv5JTY5CVbo2MvO77SpEqqYc8AvRE7OWqRDOIqAKjsp+DrmnY9uc8NPdxB5E2z47HTYGeE2+NTptg== }
@@ -951,6 +1296,20 @@ packages:
"@types/react":
optional: true
+ "@radix-ui/react-switch@1.3.0":
+ resolution:
+ { integrity: sha512-GP1EZwhoZO/GGnhM1P5/2Vpm8iN8EnngyU0oezn2l78kN8tj25pyrvjIaT7azBhK615KSt+P2w39y57YV5jVkA== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
"@radix-ui/react-tabs@1.1.14":
resolution:
{ integrity: sha512-D5jwp9JNuwDeCw3CYD2Fz+sSHo0droQjC8u75dJHe4aWr5q6yBiXZU+hurXnKudRgEpUkD5TsI6bjHPo5ThUxA== }
@@ -965,6 +1324,62 @@ packages:
"@types/react-dom":
optional: true
+ "@radix-ui/react-toast@1.2.16":
+ resolution:
+ { integrity: sha512-WUymDDiN2DpoGudRN1aW4wF5O3BNQjZZO/5nngPoNiEVqjyOzirvZZNO0R6dC1ifucSINVaSv8JX1aq47VGgiA== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-toggle-group@1.1.12":
+ resolution:
+ { integrity: sha512-TEgECgJaWGAHJJZGzNNEYTNBdIXqX7LchANycpyP7DkfjmuiSN7ISt1k/ZRGVJgVJonsgP4vwaiKMn5utrcwWQ== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-toggle@1.1.11":
+ resolution:
+ { integrity: sha512-FikrKJemoBGZQ6uRID0HJqSPBP6D7OppdD2OhLl0ZYLlAyPXI7MezoYGmumwNkrAoRm35xXkb4C8JPfJZZzcaw== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
+ "@radix-ui/react-toolbar@1.1.12":
+ resolution:
+ { integrity: sha512-4wHtJVdIgqMmEwUvxA0BYg/2JMRbt0L3+8UD8Ml/nhKkfXtiZcM8u/S15gQ5xj9YEd/0qlrm5bE805LsjQ+J8A== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
"@radix-ui/react-tooltip@1.2.9":
resolution:
{ integrity: sha512-u6F9MmTtBSLkiXNVDrtB/yPCZarM9smNswC24YYLV/M+bth6J3Gs3vlJezEoFwKZvPvxhCpUYdUnOsNG/0XOlA== }
@@ -1019,6 +1434,16 @@ packages:
"@types/react":
optional: true
+ "@radix-ui/react-use-is-hydrated@0.1.1":
+ resolution:
+ { integrity: sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A== }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
"@radix-ui/react-use-layout-effect@1.1.2":
resolution:
{ integrity: sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA== }
@@ -1029,6 +1454,16 @@ packages:
"@types/react":
optional: true
+ "@radix-ui/react-use-previous@1.1.2":
+ resolution:
+ { integrity: sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw== }
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+
"@radix-ui/react-use-rect@1.1.2":
resolution:
{ integrity: sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw== }
@@ -1646,6 +2081,10 @@ packages:
peerDependencies:
"@xterm/xterm": ^5.0.0
+ "@xterm/addon-unicode11@0.9.0":
+ resolution:
+ { integrity: sha512-FxDnYcyuXhNl+XSqGZL/t0U9eiNb/q3EWT5rYkQT/zuig8Gz/VagnQANKHdDWFM2lTMk9ly0EFQxxxtZUoRetw== }
+
"@xterm/addon-web-links@0.11.0":
resolution:
{ integrity: sha512-nIHQ38pQI+a5kXnRaTgwqSHnX7KE6+4SVoceompgHL26unAxdfP6IPqUTSYPQgSwM56hsElfoNrrW5V7BUED/Q== }
@@ -3448,6 +3887,20 @@ packages:
{ integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== }
engines: { node: ">=10" }
+ radix-ui@1.5.0:
+ resolution:
+ { integrity: sha512-Nzh2HNpClgB31FBHRqt2xG8XNUfVfQRpf34hACC5PNrXTd5JdXdqOXwLs3BL+D8CNYiNQiJiT8QGr5Q4vq+00w== }
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+
react-dom@19.2.7:
resolution:
{ integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ== }
@@ -5252,8 +5705,50 @@ snapshots:
dependencies:
playwright: 1.60.0
+ "@radix-ui/number@1.1.2": {}
+
"@radix-ui/primitive@1.1.4": {}
+ "@radix-ui/react-accessible-icon@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
+ dependencies:
+ "@radix-ui/react-visually-hidden": 1.2.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
+ "@radix-ui/react-accordion@1.2.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
+ dependencies:
+ "@radix-ui/primitive": 1.1.4
+ "@radix-ui/react-collapsible": 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-collection": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-direction": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-id": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
+ "@radix-ui/react-alert-dialog@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
+ dependencies:
+ "@radix-ui/primitive": 1.1.4
+ "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-dialog": 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-slot": 1.2.5(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
"@radix-ui/react-arrow@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
dependencies:
"@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
@@ -5263,6 +5758,60 @@ snapshots:
"@types/react": 19.2.17
"@types/react-dom": 19.2.3(@types/react@19.2.17)
+ "@radix-ui/react-aspect-ratio@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
+ dependencies:
+ "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
+ "@radix-ui/react-avatar@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
+ dependencies:
+ "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-use-callback-ref": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-is-hydrated": 0.1.1(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-layout-effect": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
+ "@radix-ui/react-checkbox@1.3.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
+ dependencies:
+ "@radix-ui/primitive": 1.1.4
+ "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-presence": 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-previous": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-size": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
+ "@radix-ui/react-collapsible@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
+ dependencies:
+ "@radix-ui/primitive": 1.1.4
+ "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-id": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-presence": 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-layout-effect": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
"@radix-ui/react-collection@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
dependencies:
"@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7)
@@ -5281,6 +5830,19 @@ snapshots:
optionalDependencies:
"@types/react": 19.2.17
+ "@radix-ui/react-context-menu@2.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
+ dependencies:
+ "@radix-ui/primitive": 1.1.4
+ "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-menu": 2.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
"@radix-ui/react-context@1.1.4(@types/react@19.2.17)(react@19.2.7)":
dependencies:
react: 19.2.7
@@ -5328,6 +5890,21 @@ snapshots:
"@types/react": 19.2.17
"@types/react-dom": 19.2.3(@types/react@19.2.17)
+ "@radix-ui/react-dropdown-menu@2.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
+ dependencies:
+ "@radix-ui/primitive": 1.1.4
+ "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-id": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-menu": 2.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
"@radix-ui/react-focus-guards@1.1.4(@types/react@19.2.17)(react@19.2.7)":
dependencies:
react: 19.2.7
@@ -5345,6 +5922,37 @@ snapshots:
"@types/react": 19.2.17
"@types/react-dom": 19.2.3(@types/react@19.2.17)
+ "@radix-ui/react-form@0.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
+ dependencies:
+ "@radix-ui/primitive": 1.1.4
+ "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-id": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-label": 2.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
+ "@radix-ui/react-hover-card@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
+ dependencies:
+ "@radix-ui/primitive": 1.1.4
+ "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-dismissable-layer": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-popper": 1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-portal": 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-presence": 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
"@radix-ui/react-id@1.1.2(@types/react@19.2.17)(react@19.2.7)":
dependencies:
"@radix-ui/react-use-layout-effect": 1.1.2(@types/react@19.2.17)(react@19.2.7)
@@ -5352,6 +5960,140 @@ snapshots:
optionalDependencies:
"@types/react": 19.2.17
+ "@radix-ui/react-label@2.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
+ dependencies:
+ "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
+ "@radix-ui/react-menu@2.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
+ dependencies:
+ "@radix-ui/primitive": 1.1.4
+ "@radix-ui/react-collection": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-direction": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-dismissable-layer": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-focus-guards": 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-focus-scope": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-id": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-popper": 1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-portal": 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-presence": 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-roving-focus": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-slot": 1.2.5(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-callback-ref": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ aria-hidden: 1.2.6
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
+ "@radix-ui/react-menubar@1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
+ dependencies:
+ "@radix-ui/primitive": 1.1.4
+ "@radix-ui/react-collection": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-direction": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-id": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-menu": 2.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-roving-focus": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
+ "@radix-ui/react-navigation-menu@1.2.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
+ dependencies:
+ "@radix-ui/primitive": 1.1.4
+ "@radix-ui/react-collection": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-direction": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-dismissable-layer": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-id": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-presence": 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-use-callback-ref": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-layout-effect": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-previous": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-visually-hidden": 1.2.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
+ "@radix-ui/react-one-time-password-field@0.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
+ dependencies:
+ "@radix-ui/number": 1.1.2
+ "@radix-ui/primitive": 1.1.4
+ "@radix-ui/react-collection": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-direction": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-roving-focus": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-effect-event": 0.0.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-is-hydrated": 0.1.1(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-layout-effect": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
+ "@radix-ui/react-password-toggle-field@0.1.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
+ dependencies:
+ "@radix-ui/primitive": 1.1.4
+ "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-id": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-effect-event": 0.0.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-is-hydrated": 0.1.1(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
+ "@radix-ui/react-popover@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
+ dependencies:
+ "@radix-ui/primitive": 1.1.4
+ "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-dismissable-layer": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-focus-guards": 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-focus-scope": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-id": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-popper": 1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-portal": 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-presence": 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-slot": 1.2.5(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7)
+ aria-hidden: 1.2.6
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
"@radix-ui/react-popper@1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
dependencies:
"@floating-ui/react-dom": 2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
@@ -5398,6 +6140,34 @@ snapshots:
"@types/react": 19.2.17
"@types/react-dom": 19.2.3(@types/react@19.2.17)
+ "@radix-ui/react-progress@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
+ dependencies:
+ "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
+ "@radix-ui/react-radio-group@1.4.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
+ dependencies:
+ "@radix-ui/primitive": 1.1.4
+ "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-direction": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-presence": 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-roving-focus": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-previous": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-size": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
"@radix-ui/react-roving-focus@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
dependencies:
"@radix-ui/primitive": 1.1.4
@@ -5415,6 +6185,81 @@ snapshots:
"@types/react": 19.2.17
"@types/react-dom": 19.2.3(@types/react@19.2.17)
+ "@radix-ui/react-scroll-area@1.2.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
+ dependencies:
+ "@radix-ui/number": 1.1.2
+ "@radix-ui/primitive": 1.1.4
+ "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-direction": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-presence": 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-use-callback-ref": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-layout-effect": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
+ "@radix-ui/react-select@2.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
+ dependencies:
+ "@radix-ui/number": 1.1.2
+ "@radix-ui/primitive": 1.1.4
+ "@radix-ui/react-collection": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-direction": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-dismissable-layer": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-focus-guards": 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-focus-scope": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-id": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-popper": 1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-portal": 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-presence": 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-slot": 1.2.5(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-callback-ref": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-layout-effect": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-previous": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-visually-hidden": 1.2.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ aria-hidden: 1.2.6
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
+ "@radix-ui/react-separator@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
+ dependencies:
+ "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
+ "@radix-ui/react-slider@1.4.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
+ dependencies:
+ "@radix-ui/number": 1.1.2
+ "@radix-ui/primitive": 1.1.4
+ "@radix-ui/react-collection": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-direction": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-layout-effect": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-previous": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-size": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
"@radix-ui/react-slot@1.2.5(@types/react@19.2.17)(react@19.2.7)":
dependencies:
"@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7)
@@ -5422,6 +6267,21 @@ snapshots:
optionalDependencies:
"@types/react": 19.2.17
+ "@radix-ui/react-switch@1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
+ dependencies:
+ "@radix-ui/primitive": 1.1.4
+ "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-previous": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-size": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
"@radix-ui/react-tabs@1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
dependencies:
"@radix-ui/primitive": 1.1.4
@@ -5438,6 +6298,67 @@ snapshots:
"@types/react": 19.2.17
"@types/react-dom": 19.2.3(@types/react@19.2.17)
+ "@radix-ui/react-toast@1.2.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
+ dependencies:
+ "@radix-ui/primitive": 1.1.4
+ "@radix-ui/react-collection": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-dismissable-layer": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-portal": 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-presence": 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-use-callback-ref": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-layout-effect": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-visually-hidden": 1.2.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
+ "@radix-ui/react-toggle-group@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
+ dependencies:
+ "@radix-ui/primitive": 1.1.4
+ "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-direction": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-roving-focus": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-toggle": 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
+ "@radix-ui/react-toggle@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
+ dependencies:
+ "@radix-ui/primitive": 1.1.4
+ "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
+ "@radix-ui/react-toolbar@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
+ dependencies:
+ "@radix-ui/primitive": 1.1.4
+ "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-direction": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-roving-focus": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-separator": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-toggle-group": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
"@radix-ui/react-tooltip@1.2.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)":
dependencies:
"@radix-ui/primitive": 1.1.4
@@ -5486,12 +6407,24 @@ snapshots:
optionalDependencies:
"@types/react": 19.2.17
+ "@radix-ui/react-use-is-hydrated@0.1.1(@types/react@19.2.17)(react@19.2.7)":
+ dependencies:
+ react: 19.2.7
+ optionalDependencies:
+ "@types/react": 19.2.17
+
"@radix-ui/react-use-layout-effect@1.1.2(@types/react@19.2.17)(react@19.2.7)":
dependencies:
react: 19.2.7
optionalDependencies:
"@types/react": 19.2.17
+ "@radix-ui/react-use-previous@1.1.2(@types/react@19.2.17)(react@19.2.7)":
+ dependencies:
+ react: 19.2.7
+ optionalDependencies:
+ "@types/react": 19.2.17
+
"@radix-ui/react-use-rect@1.1.2(@types/react@19.2.17)(react@19.2.7)":
dependencies:
"@radix-ui/rect": 1.1.2
@@ -5992,6 +6925,8 @@ snapshots:
dependencies:
"@xterm/xterm": 5.5.0
+ "@xterm/addon-unicode11@0.9.0": {}
+
"@xterm/addon-web-links@0.11.0(@xterm/xterm@5.5.0)":
dependencies:
"@xterm/xterm": 5.5.0
@@ -7387,6 +8322,69 @@ snapshots:
quick-lru@5.1.1: {}
+ radix-ui@1.5.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
+ dependencies:
+ "@radix-ui/primitive": 1.1.4
+ "@radix-ui/react-accessible-icon": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-accordion": 1.2.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-alert-dialog": 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-arrow": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-aspect-ratio": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-avatar": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-checkbox": 1.3.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-collapsible": 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-collection": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-context-menu": 2.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-dialog": 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-direction": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-dismissable-layer": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-dropdown-menu": 2.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-focus-guards": 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-focus-scope": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-form": 0.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-hover-card": 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-label": 2.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-menu": 2.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-menubar": 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-navigation-menu": 1.2.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-one-time-password-field": 0.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-password-toggle-field": 0.1.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-popover": 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-popper": 1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-portal": 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-presence": 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-progress": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-radio-group": 1.4.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-roving-focus": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-scroll-area": 1.2.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-select": 2.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-separator": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-slider": 1.4.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-slot": 1.2.5(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-switch": 1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-tabs": 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-toast": 1.2.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-toggle": 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-toggle-group": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-toolbar": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-tooltip": 1.2.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ "@radix-ui/react-use-callback-ref": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-effect-event": 0.0.3(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-escape-keydown": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-is-hydrated": 0.1.1(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-layout-effect": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-use-size": 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ "@radix-ui/react-visually-hidden": 1.2.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ "@types/react": 19.2.17
+ "@types/react-dom": 19.2.3(@types/react@19.2.17)
+
react-dom@19.2.7(react@19.2.7):
dependencies:
react: 19.2.7
diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml
new file mode 100644
index 000000000..8530a6136
--- /dev/null
+++ b/frontend/pnpm-workspace.yaml
@@ -0,0 +1,20 @@
+# pnpm settings (pnpm ≥10 reads these from pnpm-workspace.yaml, not package.json).
+# Flat npm-style node_modules: electron-forge's Vite dep-optimizer emits chunks
+# that re-import sub-deps (@tanstack/query-core, @radix-ui/primitive, …) by bare
+# specifier, which only resolve in a hoisted layout. .npmrc's node-linker=hoisted
+# is ignored by pnpm ≥10, so it must live here.
+nodeLinker: hoisted
+# Electron's postinstall downloads the runtime binary into node_modules/electron/dist;
+# without this approval pnpm skips it and `electron-forge start` cannot launch.
+allowBuilds:
+ electron: true
+ electron-winstaller: true
+# @electron/rebuild (via electron-forge) depends on @electron/node-gyp as a git
+# URL; pnpm 11's blockExoticSubdeps default rejects it on every re-resolution,
+# which would make any `pnpm add` fail in this project.
+blockExoticSubdeps: false
+# pnpm 11 defaults to a 24h supply-chain quarantine for fresh releases, which
+# kept rejecting routine transitive bumps (electron-to-chromium, semver, …) and
+# blocked every install. .npmrc's minimum-release-age=0 is ignored by pnpm ≥10,
+# so the opt-out has to live here.
+minimumReleaseAge: 0
diff --git a/frontend/src/api/schema.ts b/frontend/src/api/schema.ts
index 34a782c74..93d306ad7 100644
--- a/frontend/src/api/schema.ts
+++ b/frontend/src/api/schema.ts
@@ -143,6 +143,57 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/api/v1/reviews": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** List code-review runs */
+ get: operations["listReviews"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/reviews/{id}/send": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Send a review run's findings to its PR */
+ post: operations["sendReview"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/reviews/execute": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Start a code-review run for a session's PR */
+ post: operations["executeReview"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/api/v1/sessions": {
parameters: {
query?: never;
@@ -371,6 +422,10 @@ export interface components {
lastActivityAt: string;
state: string;
};
+ ExecuteReviewInput: {
+ /** @description Session whose PR to review. */
+ sessionId: string;
+ };
KillSessionResponse: {
freed?: boolean;
ok: boolean;
@@ -379,6 +434,9 @@ export interface components {
ListProjectsResponse: {
projects: components["schemas"]["ProjectSummary"][];
};
+ ListReviewsResponse: {
+ reviews: components["schemas"]["ReviewRun"][];
+ };
ListSessionPRsResponse: {
prs: components["schemas"]["SessionPRFacts"][];
sessionId: string;
@@ -457,6 +515,24 @@ export interface components {
session: components["schemas"]["Session"];
sessionId: string;
};
+ ReviewFinding: {
+ body: string;
+ id: string;
+ line: number;
+ path: string;
+ severity: string;
+ };
+ ReviewResponse: {
+ review: components["schemas"]["ReviewRun"];
+ };
+ ReviewRun: {
+ /** Format: date-time */
+ createdAt: string;
+ findings: components["schemas"]["ReviewFinding"][];
+ id: string;
+ sessionId: string;
+ status: string;
+ };
RoleOverride: {
agent?: string;
agentConfig?: components["schemas"]["AgentConfig"];
@@ -1084,6 +1160,127 @@ export interface operations {
};
};
};
+ listReviews: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ListReviewsResponse"];
+ };
+ };
+ /** @description Not Implemented */
+ 501: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["APIError"];
+ };
+ };
+ };
+ };
+ sendReview: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Review run id. */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ReviewResponse"];
+ };
+ };
+ /** @description Not Found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["APIError"];
+ };
+ };
+ /** @description Not Implemented */
+ 501: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["APIError"];
+ };
+ };
+ };
+ };
+ executeReview: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["ExecuteReviewInput"];
+ };
+ };
+ responses: {
+ /** @description Created */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ReviewResponse"];
+ };
+ };
+ /** @description Bad Request */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["APIError"];
+ };
+ };
+ /** @description Unprocessable Entity */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["APIError"];
+ };
+ };
+ /** @description Not Implemented */
+ 501: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["APIError"];
+ };
+ };
+ };
+ };
listSessions: {
parameters: {
query?: {
diff --git a/frontend/src/main.ts b/frontend/src/main.ts
index 7521cde4d..2d0d54179 100644
--- a/frontend/src/main.ts
+++ b/frontend/src/main.ts
@@ -1,5 +1,5 @@
import { app, BrowserWindow, dialog, ipcMain, net, protocol, shell, type OpenDialogOptions } from "electron";
-import updateElectronApp from "update-electron-app";
+import { updateElectronApp } from "update-electron-app";
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
import { readFile } from "node:fs/promises";
import os from "node:os";
diff --git a/frontend/src/renderer/App.test.tsx b/frontend/src/renderer/App.test.tsx
deleted file mode 100644
index 1d3b9e397..000000000
--- a/frontend/src/renderer/App.test.tsx
+++ /dev/null
@@ -1,263 +0,0 @@
-import { render, screen } from "@testing-library/react";
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import userEvent from "@testing-library/user-event";
-import { beforeEach, expect, test, vi } from "vitest";
-import { App } from "./App";
-import { TooltipProvider } from "./components/ui/tooltip";
-import { useUiStore } from "./stores/ui-store";
-
-const { postMock, patchMock, mockData } = vi.hoisted(() => ({
- postMock: vi.fn(),
- patchMock: vi.fn(),
- mockData: {
- projectsError: undefined as Error | undefined,
- projects: [] as { id: string; name: string; path: string; sessionPrefix: string }[],
- sessions: [] as {
- id: string;
- projectId: string;
- displayName?: string;
- harness?: string;
- status: string;
- isTerminated: boolean;
- updatedAt: string;
- }[],
- },
-}));
-
-vi.mock("./lib/api-client", () => ({
- getApiBaseUrl: () => "http://127.0.0.1:3001",
- setApiBaseUrl: () => undefined,
- subscribeApiBaseUrl: () => () => undefined,
- apiClient: {
- GET: vi.fn(async (url: string) => {
- if (url === "/api/v1/projects") {
- if (mockData.projectsError) return { data: undefined, error: mockData.projectsError };
- return { data: { projects: mockData.projects }, error: undefined };
- }
- if (url === "/api/v1/sessions") {
- return { data: { sessions: mockData.sessions }, error: undefined };
- }
- return { data: undefined, error: new Error(`unexpected GET ${url}`) };
- }),
- POST: postMock,
- PATCH: patchMock,
- },
-}));
-
-vi.mock("./components/TerminalPane", () => ({
- TerminalPane: () =>
Terminal scaffold
,
-}));
-
-function renderApp() {
- const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
- // TooltipProvider mirrors routes/__root.tsx, which wraps App in production.
- return render(
-
-
-
-
- ,
- );
-}
-
-beforeEach(() => {
- postMock.mockReset();
- patchMock.mockReset();
- mockData.projectsError = undefined;
- mockData.projects = [];
- mockData.sessions = [];
- window.localStorage.clear();
- useUiStore.setState({
- view: "orchestrator",
- workbenchTab: "changes",
- isSidebarOpen: true,
- selectedSessionId: null,
- selectedWorkspaceId: null,
- theme: "dark",
- });
-});
-
-test("renders the orchestrator anchor and empty state", async () => {
- renderApp();
-
- expect(await screen.findByRole("button", { name: "Orchestrator" })).toBeInTheDocument();
- expect(await screen.findByText("No projects yet.")).toBeInTheDocument();
-});
-
-test("surfaces project load failures instead of the empty state", async () => {
- mockData.projectsError = new TypeError("Failed to fetch");
-
- renderApp();
-
- expect(await screen.findByText("Could not load projects.", undefined, { timeout: 3_000 })).toBeInTheDocument();
- expect(await screen.findByText("Failed to fetch")).toBeInTheDocument();
- expect(screen.queryByText("No projects yet.")).not.toBeInTheDocument();
-});
-
-test("renders projects and sessions from the API", async () => {
- mockData.projects = [{ id: "proj-1", name: "my-app", path: "/home/me/my-app", sessionPrefix: "" }];
- mockData.sessions = [
- {
- id: "sess-1",
- projectId: "proj-1",
- displayName: "fix-bug",
- harness: "claude-code",
- status: "working",
- isTerminated: false,
- updatedAt: new Date().toISOString(),
- },
- ];
-
- renderApp();
-
- expect(await screen.findByRole("button", { name: "Select my-app" })).toBeInTheDocument();
- expect(await screen.findByRole("button", { name: "fix-bug" })).toBeInTheDocument();
-});
-
-test("adds a project from the rail", async () => {
- const user = userEvent.setup();
- const bridge = window.ao;
- if (!bridge) throw new Error("test preload bridge is not installed");
- bridge.app.chooseDirectory = vi.fn(async () => "/Users/me/new-project");
- postMock.mockResolvedValueOnce({
- data: {
- project: {
- id: "new-project",
- name: "New Project",
- path: "/Users/me/new-project",
- repo: "git@example.com:new-project.git",
- defaultBranch: "main",
- },
- },
- });
-
- renderApp();
-
- await user.click(await screen.findByRole("button", { name: "New project" }));
-
- expect(bridge.app.chooseDirectory).toHaveBeenCalled();
- expect(postMock).toHaveBeenCalledWith("/api/v1/projects", { body: { path: "/Users/me/new-project" } });
- expect(await screen.findByText("New Project")).toBeInTheDocument();
-});
-
-test("spawns a worker from the New worker modal", async () => {
- const user = userEvent.setup();
- mockData.projects = [{ id: "proj-1", name: "my-app", path: "/home/me/my-app", sessionPrefix: "" }];
- postMock.mockResolvedValueOnce({
- data: {
- session: {
- id: "new-task",
- projectId: "proj-1",
- harness: "claude-code",
- branch: "main",
- isTerminated: false,
- },
- },
- });
-
- renderApp();
-
- // Wait for projects to load.
- await screen.findByRole("button", { name: "Select my-app" });
-
- // Open spawn modal from the orchestrator topbar.
- await user.click(screen.getByRole("button", { name: "New worker" }));
- await user.type(await screen.findByLabelText("Prompt"), "Make task creation work");
- await user.click(screen.getByRole("button", { name: /Spawn worker/ }));
-
- // No `branch` field: it names the worktree branch, and sending the default
- // base ("main") makes the daemon 409 with BRANCH_CHECKED_OUT_ELSEWHERE.
- expect(postMock).toHaveBeenCalledWith("/api/v1/sessions", {
- body: {
- projectId: "proj-1",
- kind: "worker",
- harness: "claude-code",
- prompt: "Make task creation work",
- },
- });
- // No worker name given, so no rename round-trip.
- expect(patchMock).not.toHaveBeenCalled();
-});
-
-test("renames the spawned worker when a name is given", async () => {
- const user = userEvent.setup();
- mockData.projects = [{ id: "proj-1", name: "my-app", path: "/home/me/my-app", sessionPrefix: "" }];
- postMock.mockResolvedValueOnce({
- data: {
- session: {
- id: "new-task",
- projectId: "proj-1",
- harness: "claude-code",
- isTerminated: false,
- },
- },
- });
- patchMock.mockResolvedValueOnce({
- data: { ok: true, sessionId: "new-task", displayName: "fix-login" },
- });
-
- renderApp();
-
- await screen.findByRole("button", { name: "Select my-app" });
-
- await user.click(screen.getByRole("button", { name: "New worker" }));
- await user.type(await screen.findByLabelText("Worker name"), "fix-login");
- await user.type(screen.getByLabelText("Prompt"), "Fix the login bug");
- await user.click(screen.getByRole("button", { name: /Spawn worker/ }));
-
- expect(patchMock).toHaveBeenCalledWith("/api/v1/sessions/{sessionId}", {
- params: { path: { sessionId: "new-task" } },
- body: { displayName: "fix-login" },
- });
- expect(await screen.findByRole("button", { name: "fix-login" })).toBeInTheDocument();
-});
-
-test("surfaces an error when spawning fails", async () => {
- const user = userEvent.setup();
- mockData.projects = [{ id: "proj-1", name: "my-app", path: "/home/me/my-app", sessionPrefix: "" }];
- postMock.mockResolvedValueOnce({ error: new TypeError("Failed to fetch") });
-
- renderApp();
-
- await screen.findByRole("button", { name: "Select my-app" });
-
- await user.click(screen.getByRole("button", { name: "New worker" }));
- await user.type(await screen.findByLabelText("Prompt"), "Failing task");
- await user.click(screen.getByRole("button", { name: /Spawn worker/ }));
-
- expect(postMock).toHaveBeenCalledWith("/api/v1/sessions", {
- body: {
- projectId: "proj-1",
- kind: "worker",
- harness: "claude-code",
- prompt: "Failing task",
- },
- });
- expect(await screen.findByText("Failed to fetch")).toBeInTheDocument();
-});
-
-test("surfaces the daemon error envelope message, not [object Object]", async () => {
- const user = userEvent.setup();
- mockData.projects = [{ id: "proj-1", name: "my-app", path: "/home/me/my-app", sessionPrefix: "" }];
- // openapi-fetch resolves non-2xx bodies as a plain APIError envelope.
- postMock.mockResolvedValueOnce({
- error: {
- code: "BRANCH_CHECKED_OUT_ELSEWHERE",
- error: "Conflict",
- message: "main is checked out at /home/me/my-app",
- },
- });
-
- renderApp();
-
- await screen.findByRole("button", { name: "Select my-app" });
-
- await user.click(screen.getByRole("button", { name: "New worker" }));
- await user.type(await screen.findByLabelText("Prompt"), "Failing task");
- await user.click(screen.getByRole("button", { name: /Spawn worker/ }));
-
- expect(
- await screen.findByText("main is checked out at /home/me/my-app (BRANCH_CHECKED_OUT_ELSEWHERE)"),
- ).toBeInTheDocument();
- expect(screen.queryByText("[object Object]")).not.toBeInTheDocument();
-});
diff --git a/frontend/src/renderer/App.tsx b/frontend/src/renderer/App.tsx
deleted file mode 100644
index 4ea22e084..000000000
--- a/frontend/src/renderer/App.tsx
+++ /dev/null
@@ -1,241 +0,0 @@
-import { useQueryClient } from "@tanstack/react-query";
-import { useEffect, useState } from "react";
-import { CenterPane } from "./components/CenterPane";
-import { SideRail } from "./components/SideRail";
-import { Sidebar } from "./components/Sidebar";
-import { SpawnWorkerModal } from "./components/SpawnWorkerModal";
-import { Topbar } from "./components/Topbar";
-import { useDaemonStatus } from "./hooks/useDaemonStatus";
-import { useWorkspaceQuery, workspaceQueryKey } from "./hooks/useWorkspaceQuery";
-import { apiClient } from "./lib/api-client";
-import { Theme, useUiStore } from "./stores/ui-store";
-import { toAgentProvider, toSessionStatus, type AgentProvider, type WorkspaceSummary } from "./types/workspace";
-
-type AppProps = {
- routeSessionId?: string;
- routeWorkspaceId?: string;
-};
-
-function systemTheme(): Theme {
- return window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
-}
-
-function errorMessage(error: unknown) {
- return error instanceof Error ? error.message : "Could not load projects";
-}
-
-/**
- * openapi-fetch resolves non-2xx responses to a plain APIError envelope
- * ({ code, error, message, ... }), not an Error — String() on it renders
- * "[object Object]".
- */
-function apiErrorMessage(error: unknown, fallback: string): string {
- if (error instanceof Error) return error.message;
- if (typeof error === "string" && error) return error;
- if (error && typeof error === "object") {
- const envelope = error as { message?: unknown; code?: unknown };
- if (typeof envelope.message === "string" && envelope.message) {
- return typeof envelope.code === "string" && envelope.code
- ? `${envelope.message} (${envelope.code})`
- : envelope.message;
- }
- }
- return fallback;
-}
-
-export function App({ routeSessionId, routeWorkspaceId }: AppProps) {
- const queryClient = useQueryClient();
- const {
- view,
- workbenchTab,
- selectedSessionId,
- selectedWorkspaceId,
- theme,
- setSystemTheme,
- setWorkbenchTab,
- toggleSidebar,
- selectWorkspace,
- selectSession,
- } = useUiStore();
- const workspaceQuery = useWorkspaceQuery();
- const workspaces = workspaceQuery.data ?? [];
- const daemonStatus = useDaemonStatus(queryClient);
- const [spawnOpen, setSpawnOpen] = useState(false);
- const [spawnProjectId, setSpawnProjectId] = useState(undefined);
-
- const openSpawn = (projectId?: string) => {
- setSpawnProjectId(projectId);
- setSpawnOpen(true);
- };
-
- const selectedWorkspace =
- workspaces.find((workspace) => workspace.id === selectedWorkspaceId) ?? workspaces[0] ?? null;
- const selectedSession =
- view === "session"
- ? workspaces.flatMap((workspace) => workspace.sessions).find((session) => session.id === selectedSessionId)
- : undefined;
- const sessionWorkspace = selectedSession
- ? (workspaces.find((workspace) => workspace.id === selectedSession.workspaceId) ?? selectedWorkspace)
- : selectedWorkspace;
-
- useEffect(() => {
- if (routeWorkspaceId) selectWorkspace(routeWorkspaceId);
- if (routeSessionId) selectSession(routeSessionId, routeWorkspaceId);
- }, [routeSessionId, routeWorkspaceId, selectWorkspace, selectSession]);
-
- useEffect(() => {
- document.documentElement.dataset.theme = theme;
- document.documentElement.style.colorScheme = theme;
- }, [theme]);
-
- useEffect(() => {
- const mediaQuery = window.matchMedia("(prefers-color-scheme: light)");
- const handleChange = () => setSystemTheme(systemTheme());
-
- handleChange();
- mediaQuery.addEventListener("change", handleChange);
- return () => mediaQuery.removeEventListener("change", handleChange);
- }, [setSystemTheme]);
-
- useEffect(() => {
- const handleKeyDown = (event: KeyboardEvent) => {
- if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "b") {
- event.preventDefault();
- toggleSidebar();
- return;
- }
-
- if ((event.metaKey || event.ctrlKey) && /^[1-9]$/.test(event.key)) {
- const workspace = workspaces[Number(event.key) - 1];
- if (workspace) {
- event.preventDefault();
- selectWorkspace(workspace.id);
- }
- }
- };
-
- window.addEventListener("keydown", handleKeyDown);
- return () => window.removeEventListener("keydown", handleKeyDown);
- }, [selectWorkspace, toggleSidebar, workspaces]);
-
- const updateWorkspaces = (updater: (workspaces: WorkspaceSummary[]) => WorkspaceSummary[]) => {
- queryClient.setQueryData(workspaceQueryKey, (current = workspaces) => updater(current));
- };
-
- const createProject = async (input: { path: string }) => {
- const { data, error } = await apiClient.POST("/api/v1/projects", { body: { path: input.path } });
-
- if (error) throw new Error(apiErrorMessage(error, "Could not add project"));
- if (!data?.project) throw new Error("Project creation returned no project");
-
- const workspace: WorkspaceSummary = {
- id: data.project.id,
- name: data.project.name,
- path: data.project.path,
- type: "main",
- sessions: [],
- };
-
- updateWorkspaces((current) => [workspace, ...current.filter((item) => item.id !== workspace.id)]);
- selectWorkspace(workspace.id);
- };
-
- const createTask = async (input: { projectId: string; prompt: string; name?: string; harness?: AgentProvider }) => {
- // No `branch` here: the API's branch field names the session's worktree
- // branch (sending a checked-out branch like "main" 409s); the base branch
- // comes from the project config.
- const { data, error } = await apiClient.POST("/api/v1/sessions", {
- body: {
- projectId: input.projectId,
- kind: "worker",
- harness: input.harness,
- prompt: input.prompt,
- },
- });
-
- if (error || !data?.session) {
- throw new Error(apiErrorMessage(error, "No session returned"));
- }
-
- const session = data.session;
-
- // Best-effort: the session is already running, so a failed rename should
- // not surface as a failed spawn (retrying would create a duplicate).
- let displayName: string | undefined;
- if (input.name) {
- const renamed = await apiClient.PATCH("/api/v1/sessions/{sessionId}", {
- params: { path: { sessionId: session.id } },
- body: { displayName: input.name },
- });
- displayName = renamed.data?.displayName;
- }
-
- updateWorkspaces((current) =>
- current.map((item) =>
- item.id === input.projectId
- ? {
- ...item,
- sessions: [
- {
- id: session.id,
- terminalHandleId: session.terminalHandleId,
- workspaceId: item.id,
- workspaceName: item.name,
- title: displayName ?? input.prompt,
- provider: toAgentProvider(session.harness),
- branch: "",
- status: toSessionStatus(session.status, session.isTerminated),
- updatedAt: "now",
- },
- ...item.sessions.filter((existing) => existing.id !== session.id),
- ],
- }
- : item,
- ),
- );
- selectSession(session.id, input.projectId);
- };
-
- const showSideRail = !(view === "session" && workbenchTab === "terminal");
-
- return (
- <>
-
-
openSpawn()}
- onSetWorkbenchTab={setWorkbenchTab}
- onToggleSidebar={toggleSidebar}
- session={selectedSession}
- view={view}
- workbenchTab={workbenchTab}
- workspace={sessionWorkspace}
- />
-
-
-
- {showSideRail && (
-
- )}
-
-
-
- >
- );
-}
diff --git a/frontend/src/renderer/components/CenterPane.tsx b/frontend/src/renderer/components/CenterPane.tsx
index fdf8d55f3..96c058e48 100644
--- a/frontend/src/renderer/components/CenterPane.tsx
+++ b/frontend/src/renderer/components/CenterPane.tsx
@@ -1,45 +1,16 @@
-import { Columns2 } from "lucide-react";
-import type { Theme, WorkbenchView } from "../stores/ui-store";
+import type { Theme } from "../stores/ui-store";
import type { WorkspaceSession } from "../types/workspace";
import { TerminalPane } from "./TerminalPane";
type CenterPaneProps = {
- view: WorkbenchView;
session?: WorkspaceSession;
theme: Theme;
daemonReady: boolean;
};
-export function CenterPane({ view, session, theme, daemonReady }: CenterPaneProps) {
- const isOrchestrator = view === "orchestrator";
- const agentLabel = session?.provider ?? "claude-code";
-
+export function CenterPane({ session, theme, daemonReady }: CenterPaneProps) {
return (
-
-
-
-
- {isOrchestrator ? (
- <>
- orchestrator {agentLabel}
- >
- ) : (
- <>
- {agentLabel} (1)
- >
- )}
-
- {!isOrchestrator && (
-
-
-
- )}
-
-
+
diff --git a/frontend/src/renderer/components/DashboardTopbar.tsx b/frontend/src/renderer/components/DashboardTopbar.tsx
new file mode 100644
index 000000000..029565dfb
--- /dev/null
+++ b/frontend/src/renderer/components/DashboardTopbar.tsx
@@ -0,0 +1,142 @@
+import { useQueryClient } from "@tanstack/react-query";
+import { useNavigate } from "@tanstack/react-router";
+import { Bell, Waypoints } from "lucide-react";
+import { useState } from "react";
+import { findProjectOrchestrator } from "../types/workspace";
+import { useWorkspaceQuery, workspaceQueryKey } from "../hooks/useWorkspaceQuery";
+import { spawnOrchestrator } from "../lib/spawn-orchestrator";
+import { useUiStore } from "../stores/ui-store";
+import { cn } from "../lib/utils";
+
+const isMac = typeof navigator !== "undefined" && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent);
+const dragStyle = isMac ? ({ WebkitAppRegion: "drag" } as React.CSSProperties) : undefined;
+const noDragStyle = isMac ? ({ WebkitAppRegion: "no-drag" } as React.CSSProperties) : undefined;
+
+type DashboardTab = "coding" | "reviews";
+
+type DashboardTopbarProps = {
+ /** Which top-nav tab reads as active (omit on the PR board, which is neither). */
+ activeTab?: DashboardTab;
+ /** When set, the project crumb scopes to one project. */
+ projectId?: string;
+ projectLabel?: string;
+};
+
+// The dashboard header (mc-board .dashboard-app-header): project crumb · Coding/
+// Reviews tabs | bell · Orchestrator (board ↔ terminal).
+// Shared verbatim across the board, review, and PR screens so navigating between
+// them keeps one stable top strip (agent-orchestrator surfaces them as tabs).
+export function DashboardTopbar({ activeTab, projectId, projectLabel = "agent-orchestrator" }: DashboardTopbarProps) {
+ const navigate = useNavigate();
+ const queryClient = useQueryClient();
+ const [isSpawning, setIsSpawning] = useState(false);
+ const isSidebarOpen = useUiStore((state) => state.isSidebarOpen);
+ const all = useWorkspaceQuery().data ?? [];
+ const orchestrator = projectId ? findProjectOrchestrator(all, projectId) : undefined;
+
+ const openOrchestrator = async () => {
+ if (!projectId) return;
+ if (orchestrator) {
+ void navigate({
+ to: "/projects/$projectId/sessions/$sessionId",
+ params: { projectId, sessionId: orchestrator.id },
+ });
+ return;
+ }
+ setIsSpawning(true);
+ try {
+ const sessionId = await spawnOrchestrator(projectId);
+ await queryClient.invalidateQueries({ queryKey: workspaceQueryKey });
+ void navigate({
+ to: "/projects/$projectId/sessions/$sessionId",
+ params: { projectId, sessionId },
+ });
+ } catch (error) {
+ console.error("Failed to spawn orchestrator:", error);
+ } finally {
+ setIsSpawning(false);
+ }
+ };
+
+ return (
+
+
+
+ {projectLabel}
+
+
+ void navigate(projectId ? { to: "/projects/$projectId", params: { projectId } } : { to: "/" })
+ }
+ style={noDragStyle}
+ type="button"
+ >
+ Coding
+
+ void navigate({ to: "/review" })}
+ style={noDragStyle}
+ type="button"
+ >
+ Reviews
+
+
+
+
+
+
+
+
+
+ {projectId ? (
+ orchestrator ? (
+
+ void navigate({
+ to: "/projects/$projectId/sessions/$sessionId",
+ params: { projectId, sessionId: orchestrator.id },
+ })
+ }
+ style={noDragStyle}
+ type="button"
+ >
+
+ Orchestrator
+
+ ) : (
+ void openOrchestrator()}
+ style={noDragStyle}
+ type="button"
+ >
+
+ {isSpawning ? "Spawning…" : "Spawn Orchestrator"}
+
+ )
+ ) : null}
+
+
+ );
+}
+
+// The board subhead (mc-board .dashboard-main__subhead): a 21px bold title with
+// a muted one-line subtitle, optionally a trailing count.
+export function DashboardSubhead({ title, subtitle, count }: { title: string; subtitle: string; count?: number }) {
+ return (
+
+
{title}
+ {typeof count === "number" && {count} }
+ {subtitle}
+
+ );
+}
diff --git a/frontend/src/renderer/components/ProjectSettingsForm.tsx b/frontend/src/renderer/components/ProjectSettingsForm.tsx
new file mode 100644
index 000000000..ecfa5e71c
--- /dev/null
+++ b/frontend/src/renderer/components/ProjectSettingsForm.tsx
@@ -0,0 +1,237 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { useState } from "react";
+import type { components } from "../../api/schema";
+import { apiClient, apiErrorMessage } from "../lib/api-client";
+import { workspaceQueryKey } from "../hooks/useWorkspaceQuery";
+import { DashboardSubhead, DashboardTopbar } from "./DashboardTopbar";
+import { Button } from "./ui/button";
+import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
+import { Label } from "./ui/label";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
+
+type Project = components["schemas"]["Project"];
+type ProjectConfig = components["schemas"]["ProjectConfig"];
+
+// Agents the daemon registers (see SpawnWorkerModal). Empty = "use the daemon
+// default". Kept short — the spawn modal owns the full list.
+const AGENT_OPTIONS = ["claude-code", "codex", "opencode", "amp", "goose", "kiro"] as const;
+
+const projectQueryKey = (id: string) => ["project", id] as const;
+
+export function ProjectSettingsForm({ projectId }: { projectId: string }) {
+ const queryClient = useQueryClient();
+
+ const query = useQuery({
+ queryKey: projectQueryKey(projectId),
+ queryFn: async () => {
+ const { data, error } = await apiClient.GET("/api/v1/projects/{id}", {
+ params: { path: { id: projectId } },
+ });
+ if (error) throw new Error(apiErrorMessage(error));
+ if (data?.status !== "ok") throw new Error("Project config is unavailable (degraded).");
+ return data.project as Project;
+ },
+ });
+
+ if (query.isLoading) {
+ return
Loading project settings… ;
+ }
+ if (query.isError || !query.data) {
+ return (
+
{query.error instanceof Error ? query.error.message : "Could not load project."}
+ );
+ }
+
+ return (
+
+
+
+
+ queryClient.invalidateQueries({ queryKey: workspaceQueryKey })}
+ projectId={projectId}
+ />
+
+
+ );
+}
+
+function SettingsBody({ project, projectId, onSaved }: { project: Project; projectId: string; onSaved: () => void }) {
+ const queryClient = useQueryClient();
+ const config = project.config ?? {};
+ const [form, setForm] = useState({
+ defaultBranch: config.defaultBranch ?? project.defaultBranch ?? "",
+ sessionPrefix: config.sessionPrefix ?? "",
+ workerAgent: config.worker?.agent ?? "",
+ orchestratorAgent: config.orchestrator?.agent ?? "",
+ model: config.agentConfig?.model ?? "",
+ });
+ const [savedAt, setSavedAt] = useState
(null);
+
+ const mutation = useMutation({
+ mutationFn: async () => {
+ // PUT replaces the whole config; merge the edited fields over what loaded
+ // so we don't drop env/symlinks/postCreate the form doesn't expose.
+ const next: ProjectConfig = {
+ ...config,
+ defaultBranch: form.defaultBranch || undefined,
+ sessionPrefix: form.sessionPrefix || undefined,
+ worker: blankToUndefined({ ...config.worker, agent: form.workerAgent || undefined }),
+ orchestrator: blankToUndefined({ ...config.orchestrator, agent: form.orchestratorAgent || undefined }),
+ agentConfig: blankToUndefined({ ...config.agentConfig, model: form.model || undefined }),
+ };
+ const { error } = await apiClient.PUT("/api/v1/projects/{id}/config", {
+ params: { path: { id: projectId } },
+ body: { config: next },
+ });
+ if (error) throw new Error(apiErrorMessage(error));
+ },
+ onSuccess: () => {
+ setSavedAt(Date.now());
+ void queryClient.invalidateQueries({ queryKey: ["project", projectId] });
+ onSaved();
+ },
+ });
+
+ return (
+
+ );
+}
+
+function AgentSelect({ value, onChange }: { value: string; onChange: (value: string) => void }) {
+ // "" sentinel → daemon default; Select can't hold an empty value, so map it.
+ return (
+ onChange(v === "__default__" ? "" : v)}>
+
+
+
+
+ Daemon default
+ {AGENT_OPTIONS.map((agent) => (
+
+ {agent}
+
+ ))}
+
+
+ );
+}
+
+function Field({ label, htmlFor, children }: { label: string; htmlFor?: string; children: React.ReactNode }) {
+ return (
+
+
+ {label}
+
+ {children}
+
+ );
+}
+
+function ReadonlyRow({ label, value }: { label: string; value: string }) {
+ return (
+
+ {label}
+ {value}
+
+ );
+}
+
+function CenteredNote({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+
+// Drop an object whose every value is undefined so we send `undefined` (omit)
+// rather than an empty {} the daemon would persist.
+function blankToUndefined(obj: T): T | undefined {
+ return Object.values(obj).some((v) => v !== undefined) ? obj : undefined;
+}
diff --git a/frontend/src/renderer/components/PullRequestsPage.tsx b/frontend/src/renderer/components/PullRequestsPage.tsx
new file mode 100644
index 000000000..cc8194ec8
--- /dev/null
+++ b/frontend/src/renderer/components/PullRequestsPage.tsx
@@ -0,0 +1,170 @@
+import { useNavigate } from "@tanstack/react-router";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { useState } from "react";
+import { apiClient, apiErrorMessage } from "../lib/api-client";
+import { useWorkspaceQuery, workspaceQueryKey } from "../hooks/useWorkspaceQuery";
+import type { WorkspaceSession } from "../types/workspace";
+import { DashboardSubhead, DashboardTopbar } from "./DashboardTopbar";
+import { Badge } from "./ui/badge";
+import { Button } from "./ui/button";
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./ui/table";
+import { cn } from "../lib/utils";
+
+type PRState = NonNullable["state"];
+
+const stateTone: Record = {
+ open: "border-success/40 bg-success/10 text-success",
+ draft: "border-border bg-raised text-muted-foreground",
+ merged: "border-accent/40 bg-accent-weak text-accent",
+ closed: "border-error/40 bg-error/10 text-error",
+};
+
+// Order open PRs (actionable) above merged/closed.
+const stateRank: Record = { open: 0, draft: 1, merged: 2, closed: 3 };
+
+type PRRow = {
+ number: number;
+ state: PRState;
+ session: WorkspaceSession;
+};
+
+// The PR board, ported from agent-orchestrator's PullRequestsPage. The Go
+// daemon has no PR-list endpoint, so the board is derived from session PR
+// fields (every session carries pullRequest); actions hit /prs/{number}/merge
+// and /resolve-comments. Per-PR CI/review facts live on the session route's
+// inspector.
+export function PullRequestsPage() {
+ const navigate = useNavigate();
+ const workspaceQuery = useWorkspaceQuery();
+ const sessions = (workspaceQuery.data ?? []).flatMap((w) => w.sessions);
+ const rows: PRRow[] = sessions
+ .filter((s): s is WorkspaceSession & { pullRequest: NonNullable } =>
+ Boolean(s.pullRequest),
+ )
+ .map((s) => ({ number: s.pullRequest.number, state: s.pullRequest.state, session: s }))
+ .sort((a, b) => stateRank[a.state] - stateRank[b.state] || a.number - b.number);
+
+ return (
+
+
+
+
+
+ {rows.length === 0 ? (
+
No open pull requests.
+ ) : (
+
+
+
+ PR
+ Worker
+ State
+ Actions
+
+
+
+ {rows.map((row) => (
+
+ void navigate({
+ to: "/projects/$projectId/sessions/$sessionId",
+ params: { projectId: row.session.workspaceId, sessionId: row.session.id },
+ })
+ }
+ />
+ ))}
+
+
+ )}
+
+
+ );
+}
+
+function PRRowView({ row, onOpen }: { row: PRRow; onOpen: () => void }) {
+ const queryClient = useQueryClient();
+ const [note, setNote] = useState<{ ok: boolean; text: string } | null>(null);
+ const refresh = () => void queryClient.invalidateQueries({ queryKey: workspaceQueryKey });
+
+ const merge = useMutation({
+ mutationFn: async () => {
+ const { data, error } = await apiClient.POST("/api/v1/prs/{id}/merge", {
+ params: { path: { id: String(row.number) } },
+ });
+ if (error) throw new Error(apiErrorMessage(error));
+ return data;
+ },
+ onSuccess: (data) => {
+ setNote({ ok: true, text: `merged (${data?.method ?? "squash"})` });
+ refresh();
+ },
+ onError: (e) => setNote({ ok: false, text: e instanceof Error ? e.message : "merge failed" }),
+ });
+
+ const resolve = useMutation({
+ mutationFn: async () => {
+ const { error } = await apiClient.POST("/api/v1/prs/{id}/resolve-comments", {
+ params: { path: { id: String(row.number) } },
+ });
+ if (error) throw new Error(apiErrorMessage(error));
+ },
+ onSuccess: () => {
+ setNote({ ok: true, text: "comments resolved" });
+ refresh();
+ },
+ onError: (e) => setNote({ ok: false, text: e instanceof Error ? e.message : "resolve failed" }),
+ });
+
+ const actionable = row.state === "open" || row.state === "draft";
+
+ return (
+
+ #{row.number}
+
+ {row.session.title}
+
+ {[row.session.workspaceName, row.session.branch].filter(Boolean).join(" · ")}
+
+
+
+
+ {row.state}
+
+
+ e.stopPropagation()}>
+ {note ? (
+ {note.text}
+ ) : actionable ? (
+
+ resolve.mutate()}
+ >
+ {resolve.isPending ? "…" : "Resolve"}
+
+ merge.mutate()}
+ >
+ {merge.isPending ? "Merging…" : "Merge"}
+
+
+ ) : (
+ —
+ )}
+
+
+ );
+}
diff --git a/frontend/src/renderer/components/ReviewDashboard.tsx b/frontend/src/renderer/components/ReviewDashboard.tsx
new file mode 100644
index 000000000..972e897e7
--- /dev/null
+++ b/frontend/src/renderer/components/ReviewDashboard.tsx
@@ -0,0 +1,171 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { Play } from "lucide-react";
+import { useState } from "react";
+import type { components } from "../../api/schema";
+import { apiClient, apiErrorMessage } from "../lib/api-client";
+import { useWorkspaceQuery } from "../hooks/useWorkspaceQuery";
+import { DashboardSubhead, DashboardTopbar } from "./DashboardTopbar";
+import { Badge } from "./ui/badge";
+import { Button } from "./ui/button";
+import { Card, CardContent } from "./ui/card";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
+import { cn } from "../lib/utils";
+
+type ReviewRun = components["schemas"]["ReviewRun"];
+
+const reviewsKey = ["reviews"] as const;
+
+const statusTone: Record = {
+ pending: "border-warning/40 bg-warning/10 text-warning",
+ complete: "border-success/40 bg-success/10 text-success",
+ sent: "border-accent/40 bg-accent-weak text-accent",
+};
+
+// The code-review board, ported from agent-orchestrator's ReviewDashboard onto
+// the daemon's reviews API (GET/POST /api/v1/reviews). Lists review runs and
+// their findings; lets you start a run for a session and send its findings.
+export function ReviewDashboard() {
+ const queryClient = useQueryClient();
+ const sessions = (useWorkspaceQuery().data ?? []).flatMap((w) => w.sessions);
+ const [target, setTarget] = useState("");
+
+ const reviews = useQuery({
+ queryKey: reviewsKey,
+ queryFn: async () => {
+ const { data, error } = await apiClient.GET("/api/v1/reviews");
+ if (error) throw new Error(apiErrorMessage(error));
+ return data?.reviews ?? [];
+ },
+ });
+
+ const execute = useMutation({
+ mutationFn: async (sessionId: string) => {
+ const { error } = await apiClient.POST("/api/v1/reviews/execute", { body: { sessionId } });
+ if (error) throw new Error(apiErrorMessage(error));
+ },
+ onSuccess: () => void queryClient.invalidateQueries({ queryKey: reviewsKey }),
+ });
+
+ const runs = (reviews.data ?? []).slice().sort((a, b) => b.createdAt.localeCompare(a.createdAt));
+
+ return (
+
+
+
+
+
+
+
+
+
+ {sessions.length === 0 ? (
+
+ No workers
+
+ ) : (
+ sessions.map((s) => (
+
+ {s.title}
+
+ ))
+ )}
+
+
+
execute.mutate(target)}
+ >
+
+ {execute.isPending ? "Starting…" : "Run review"}
+
+
+
+
+ {reviews.isError ? (
+
Could not load reviews.
+ ) : runs.length === 0 ? (
+
+ No review runs yet. Pick a worker and Run review .
+
+ ) : (
+
+ {runs.map((run) => (
+ s.id === run.sessionId)?.title}
+ />
+ ))}
+
+ )}
+
+
+ );
+}
+
+function ReviewRunCard({ run, sessionTitle }: { run: ReviewRun; sessionTitle?: string }) {
+ const queryClient = useQueryClient();
+ const send = useMutation({
+ mutationFn: async () => {
+ const { error } = await apiClient.POST("/api/v1/reviews/{id}/send", { params: { path: { id: run.id } } });
+ if (error) throw new Error(apiErrorMessage(error));
+ },
+ onSuccess: () => void queryClient.invalidateQueries({ queryKey: reviewsKey }),
+ });
+
+ return (
+
+
+
+ {run.id}
+ {sessionTitle ?? run.sessionId}
+
+ {run.status}
+
+ {run.status !== "sent" && (
+ send.mutate()}
+ >
+ {send.isPending ? "Sending…" : "Send"}
+
+ )}
+
+ {run.findings.length === 0 ? (
+ no findings
+ ) : (
+
+ {run.findings.map((f) => (
+
+
+ {f.severity}
+
+
+
+ {f.path}:{f.line}
+ {" "}
+ {f.body}
+
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/frontend/src/renderer/components/SessionInspector.tsx b/frontend/src/renderer/components/SessionInspector.tsx
new file mode 100644
index 000000000..dee4cf696
--- /dev/null
+++ b/frontend/src/renderer/components/SessionInspector.tsx
@@ -0,0 +1,390 @@
+import { useQuery } from "@tanstack/react-query";
+import { useState, type ReactNode } from "react";
+import { GitBranch, GitCommitHorizontal, GitPullRequest, Plus, Square, Trash2 } from "lucide-react";
+import type { components } from "../../api/schema";
+import { apiClient } from "../lib/api-client";
+import { formatTimeCompact } from "../lib/format-time";
+import type { SessionStatus, WorkspaceSession } from "../types/workspace";
+import { workerDisplayStatus } from "../types/workspace";
+import { Badge } from "./ui/badge";
+import { Button } from "./ui/button";
+import { cn } from "../lib/utils";
+
+type PRFacts = components["schemas"]["SessionPRFacts"];
+type InspectorView = "summary" | "changes" | "browser";
+
+const VIEWS: { id: InspectorView; label: string; icon: ReactNode }[] = [
+ {
+ id: "summary",
+ label: "Summary",
+ icon: (
+
+
+
+
+
+
+
+
+ ),
+ },
+ {
+ id: "changes",
+ label: "Changes",
+ icon: (
+
+
+
+
+
+
+
+ ),
+ },
+ {
+ id: "browser",
+ label: "Browser",
+ icon: (
+
+
+
+
+
+ ),
+ },
+];
+
+const prStateTone: Record = {
+ open: "border-success/40 bg-success/10 text-success",
+ draft: "border-border bg-raised text-muted-foreground",
+ merged: "border-accent/40 bg-accent-weak text-accent",
+ closed: "border-error/40 bg-error/10 text-error",
+};
+
+/**
+ * Tabbed inspector rail beside the terminal — cloned from agent-orchestrator
+ * SessionInspector (Summary · Changes · Browser).
+ */
+export function SessionInspector({ session }: { session?: WorkspaceSession }) {
+ const [view, setView] = useState("summary");
+
+ if (!session) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+ {VIEWS.map((entry) => (
+ setView(entry.id)}
+ >
+ {entry.icon}
+ {entry.label}
+
+ ))}
+
+
+
+ {view === "summary" ? : null}
+ {view === "changes" ? : null}
+ {view === "browser" ? : null}
+
+
+ );
+}
+
+function Section({ title, action, children }: { title: string; action?: ReactNode; children: ReactNode }) {
+ return (
+
+
+ {title}
+ {action ?? null}
+
+ {children}
+
+ );
+}
+
+function SummaryView({ session }: { session: WorkspaceSession }) {
+ const hasPr = Boolean(session.pullRequest);
+ const query = useQuery({
+ queryKey: ["session-pr", session.id],
+ enabled: hasPr,
+ queryFn: async () => {
+ const { data, error } = await apiClient.GET("/api/v1/sessions/{sessionId}/pr", {
+ params: { path: { sessionId: session.id } },
+ });
+ if (error) return [] as PRFacts[];
+ return data?.prs ?? [];
+ },
+ });
+ const prFacts = query.data?.[0];
+ const branchLabel = session.branch || `session/${session.id}`;
+
+ return (
+
+
+ Open ↗
+
+ ) : undefined
+ }
+ >
+ {!hasPr ? (
+ No pull request opened yet.
+ ) : query.isLoading ? (
+ Loading pull request…
+ ) : (
+
+
+
+
+ PR #{prFacts?.number ?? session.pullRequest?.number}
+
+ {prFacts ? (
+
+ {prFacts.state}
+
+ ) : null}
+
+ {prFacts ? (
+
+
+
+
+
+ ) : (
+
No enriched PR facts yet.
+ )}
+
+ )}
+
+
+
+
+
+
+ );
+}
+
+type TimelineTone = "now" | "good" | "warn" | "neutral";
+
+function ActivityTimeline({ session }: { session: WorkspaceSession }) {
+ const events: { tone: TimelineTone; node: ReactNode; ts: string | null }[] = [];
+ const detail = activityDetail(session.status);
+
+ events.push({
+ tone: "now",
+ node: (
+ <>
+
+
+
+ {detail ? — {detail} : null}
+ >
+ ),
+ ts: formatTimeCompact(session.updatedAt),
+ });
+
+ if (session.pullRequest) {
+ events.push({
+ tone: "good",
+ node: (
+ <>
+ Opened PR #{session.pullRequest.number}
+ >
+ ),
+ ts: null,
+ });
+ }
+
+ events.push({
+ tone: "neutral",
+ node: <>Created worktree & branch>,
+ ts: formatTimeCompact(session.createdAt ?? session.updatedAt),
+ });
+
+ return (
+
+ {events.map((event, index) => (
+
+
+
{event.node}
+ {event.ts ?
{event.ts}
: null}
+
+ ))}
+
+ );
+}
+
+function activityDetail(status: SessionStatus): string | null {
+ switch (status) {
+ case "idle":
+ return "Session idle";
+ case "needs_input":
+ return "Waiting for input";
+ case "working":
+ return null;
+ default:
+ return null;
+ }
+}
+
+const STATUS_PILL: Record<
+ ReturnType | "idle",
+ { label: string; tone: string; breathe: boolean }
+> = {
+ working: { label: "Working", tone: "var(--orange)", breathe: true },
+ needs_you: { label: "Needs input", tone: "var(--amber)", breathe: false },
+ ci_failed: { label: "CI failed", tone: "var(--red)", breathe: false },
+ mergeable: { label: "Ready", tone: "var(--green)", breathe: false },
+ done: { label: "Done", tone: "var(--fg-muted)", breathe: false },
+ idle: { label: "Idle", tone: "var(--fg-muted)", breathe: false },
+};
+
+function InspectorStatusPill({ session }: { session: WorkspaceSession }) {
+ const key = session.status === "idle" ? "idle" : workerDisplayStatus(session);
+ const { label, tone, breathe } = STATUS_PILL[key];
+ return (
+
+
+ {label}
+
+ );
+}
+
+function ChangesView({ session }: { session: WorkspaceSession }) {
+ const files = session.changedFiles ?? [];
+
+ return (
+
+
+ Changed
+ {files.length}
+
+
+
+
+ All files
+
+
+
+ Discard all
+
+
+
+ Stage all
+
+
+
+
+ {files.length === 0 ? (
+
No changes yet.
+ ) : (
+ files.map((file) => (
+
+ {file.path}
+ +{file.additions}
+ −{file.deletions}
+
+
+ ))
+ )}
+
+
+
+
+
+
+
+ Commit & Push
+
+
+
+
+
+
{session.branch || "—"}
+
+
+
+ Create PR
+
+
+
+ );
+}
+
+function BrowserView() {
+ return (
+
+
+
+
+
+
+
+
No live browser preview.
+
A browser plugin will render what the agent is viewing here.
+
+
+ );
+}
+
+function Row({ k, v, mono }: { k: string; v: string; mono?: boolean }) {
+ return (
+
+
{k}
+ {v}
+
+ );
+}
diff --git a/frontend/src/renderer/components/SessionView.test.tsx b/frontend/src/renderer/components/SessionView.test.tsx
new file mode 100644
index 000000000..58e632c6a
--- /dev/null
+++ b/frontend/src/renderer/components/SessionView.test.tsx
@@ -0,0 +1,207 @@
+import type { ReactNode, Ref } from "react";
+import { act, fireEvent, render, screen } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi, type Mock } from "vitest";
+import { SessionView } from "./SessionView";
+import { useUiStore } from "../stores/ui-store";
+import type { WorkspaceSession, WorkspaceSummary } from "../types/workspace";
+
+type FakePanelHandle = {
+ collapse: Mock;
+ expand: Mock;
+ getSize: Mock;
+ isCollapsed: Mock;
+ resize: Mock;
+};
+
+type PanelEntry = {
+ handle: FakePanelHandle;
+ onResize?: (size: { asPercentage: number; inPixels: number }) => void;
+};
+
+const { workspaces, panels } = vi.hoisted(() => {
+ const worker = {
+ id: "sess-1",
+ workspaceId: "proj-1",
+ workspaceName: "my-app",
+ title: "do the thing",
+ provider: "claude-code",
+ kind: "worker",
+ branch: "ao/sess-1",
+ status: "working",
+ updatedAt: "2026-06-10T00:00:00Z",
+ } satisfies WorkspaceSession;
+ const orchestrator = {
+ ...worker,
+ id: "sess-orch",
+ kind: "orchestrator",
+ title: "orchestrate",
+ } satisfies WorkspaceSession;
+ const workspaces: WorkspaceSummary[] = [
+ { id: "proj-1", name: "my-app", path: "/p", type: "main", sessions: [worker, orchestrator] },
+ ];
+ return { workspaces, panels: new Map() };
+});
+
+// The terminal, inspector body, and topbar pull in xterm/router/SSE machinery
+// irrelevant to the split under test.
+vi.mock("./CenterPane", () => ({ CenterPane: () =>
}));
+vi.mock("./SessionInspector", () => ({ SessionInspector: () =>
}));
+vi.mock("./Topbar", () => ({ Topbar: () => }));
+vi.mock("@tanstack/react-router", () => ({ useNavigate: () => vi.fn() }));
+vi.mock("../lib/shell-context", () => ({
+ useShell: () => ({ daemonStatus: { state: "ready" } }),
+}));
+vi.mock("../hooks/useWorkspaceQuery", () => ({
+ useWorkspaceQuery: () => ({ data: workspaces, isLoading: false }),
+}));
+
+// jsdom has no layout engine, so the real react-resizable-panels would never
+// produce meaningful sizes — record the props SessionView passes and expose a
+// fake imperative handle per panel instead.
+vi.mock("./ui/resizable", () => ({
+ ResizablePanelGroup: ({ children }: { children?: ReactNode }) => {children}
,
+ ResizableHandle: () =>
,
+ ResizablePanel: ({
+ children,
+ id,
+ defaultSize,
+ minSize,
+ maxSize,
+ collapsible,
+ panelRef,
+ onResize,
+ style: _style,
+ ...rest
+ }: {
+ children?: ReactNode;
+ id: string;
+ defaultSize?: number | string;
+ minSize?: number | string;
+ maxSize?: number | string;
+ collapsible?: boolean;
+ panelRef?: Ref;
+ onResize?: (size: { asPercentage: number; inPixels: number }) => void;
+ style?: React.CSSProperties;
+ }) => {
+ let entry = panels.get(id);
+ if (!entry) {
+ entry = {
+ handle: {
+ collapse: vi.fn(),
+ expand: vi.fn(),
+ getSize: vi.fn(() => ({ asPercentage: 28, inPixels: 280 })),
+ isCollapsed: vi.fn(() => false),
+ resize: vi.fn(),
+ },
+ };
+ panels.set(id, entry);
+ }
+ entry.onResize = onResize;
+ if (panelRef && typeof panelRef === "object") {
+ (panelRef as { current: FakePanelHandle | null }).current = entry.handle;
+ }
+ return (
+
+
+ {JSON.stringify([defaultSize, minSize, maxSize].filter((s) => s !== undefined))}
+
+ {children}
+
+ );
+ },
+}));
+
+function panelSizes(id: string): unknown[] {
+ return JSON.parse(screen.getByTestId(`panel-${id}-sizes`).textContent ?? "[]") as unknown[];
+}
+
+describe("SessionView", () => {
+ beforeEach(() => {
+ window.localStorage.clear();
+ useUiStore.setState({ isInspectorOpen: true });
+ panels.clear();
+ });
+
+ // Regression: react-resizable-panels v4 treats bare numeric sizes as PIXELS
+ // (numbers were percentages in the older API the shadcn examples use).
+ // defaultSize={28}/maxSize={45} clamped the inspector rail to a 45px sliver.
+ // Every size must be an explicit percentage string.
+ it("sizes the terminal/inspector split in percentages, not pixels", () => {
+ render( );
+
+ for (const panelId of ["terminal", "inspector"]) {
+ const sizes = panelSizes(panelId);
+ expect(sizes.length).toBeGreaterThan(0);
+ for (const size of sizes) {
+ expect(size, `${panelId} size ${String(size)} must be a percentage string`).toMatch(/^\d+(\.\d+)?%$/);
+ }
+ }
+ });
+
+ it("marks the inspector collapsible and renders the resize handle", () => {
+ render( );
+
+ expect(screen.getByTestId("panel-inspector")).toHaveAttribute("data-collapsible", "true");
+ expect(screen.getByTestId("resize-handle")).toBeInTheDocument();
+ expect(screen.getByTestId("panel-inspector")).not.toHaveAttribute("inert");
+ });
+
+ it("mounts collapsed and inert when the store says closed", () => {
+ useUiStore.setState({ isInspectorOpen: false });
+ render( );
+
+ expect(panelSizes("inspector")[0]).toBe("0%");
+ const pane = screen.getByTestId("panel-inspector");
+ expect(pane).toHaveAttribute("inert");
+ expect(pane).toHaveAttribute("aria-hidden", "true");
+ expect(panels.get("inspector")!.handle.collapse).toHaveBeenCalled();
+ });
+
+ it("toggles the inspector with mod+shift+B through the imperative panel API", () => {
+ render( );
+ const handle = panels.get("inspector")!.handle;
+
+ fireEvent.keyDown(window, { key: "B", metaKey: true, shiftKey: true });
+ expect(useUiStore.getState().isInspectorOpen).toBe(false);
+ expect(handle.collapse).toHaveBeenCalledTimes(1);
+
+ fireEvent.keyDown(window, { key: "B", ctrlKey: true, shiftKey: true });
+ expect(useUiStore.getState().isInspectorOpen).toBe(true);
+ expect(handle.expand).toHaveBeenCalled();
+
+ // Plain ⌘B belongs to the sidebar — the inspector must not react.
+ fireEvent.keyDown(window, { key: "b", metaKey: true });
+ expect(useUiStore.getState().isInspectorOpen).toBe(true);
+ });
+
+ it("syncs drag resizes back into the store and persists the split", () => {
+ render( );
+ const entry = panels.get("inspector")!;
+
+ // Dragging past minSize collapses the panel → store follows.
+ act(() => entry.onResize?.({ asPercentage: 0, inPixels: 0 }));
+ expect(useUiStore.getState().isInspectorOpen).toBe(false);
+
+ // Dragging it back open reopens + persists the width.
+ act(() => entry.onResize?.({ asPercentage: 31.5, inPixels: 400 }));
+ expect(useUiStore.getState().isInspectorOpen).toBe(true);
+ expect(window.localStorage.getItem("ao.inspector.split")).toBe("31.5");
+ });
+
+ it("restores the persisted split width", () => {
+ window.localStorage.setItem("ao.inspector.split", "40");
+ render( );
+ expect(panelSizes("inspector")[0]).toBe("40%");
+ });
+
+ it("renders no inspector panel or handle for orchestrator sessions", () => {
+ render( );
+
+ expect(screen.queryByTestId("panel-inspector")).not.toBeInTheDocument();
+ expect(screen.queryByTestId("resize-handle")).not.toBeInTheDocument();
+
+ // The shortcut is inactive without an inspector.
+ fireEvent.keyDown(window, { key: "B", metaKey: true, shiftKey: true });
+ expect(useUiStore.getState().isInspectorOpen).toBe(true);
+ });
+});
diff --git a/frontend/src/renderer/components/SessionView.tsx b/frontend/src/renderer/components/SessionView.tsx
new file mode 100644
index 000000000..f3c0a54ae
--- /dev/null
+++ b/frontend/src/renderer/components/SessionView.tsx
@@ -0,0 +1,161 @@
+import { useEffect, useRef, useState } from "react";
+import { useNavigate } from "@tanstack/react-router";
+import type { PanelImperativeHandle, PanelSize } from "react-resizable-panels";
+import { CenterPane } from "./CenterPane";
+import { SessionInspector } from "./SessionInspector";
+import { Topbar } from "./Topbar";
+import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "./ui/resizable";
+import { useUiStore } from "../stores/ui-store";
+import { useShell } from "../lib/shell-context";
+import { useWorkspaceQuery } from "../hooks/useWorkspaceQuery";
+import { isOrchestratorSession } from "../types/workspace";
+
+const INSPECTOR_MIN_PERCENT = 22;
+const INSPECTOR_MAX_PERCENT = 45;
+const inspectorSplitStorageKey = "ao.inspector.split";
+
+function initialSplitPercent(): number {
+ const raw = typeof window === "undefined" ? null : window.localStorage?.getItem(inspectorSplitStorageKey);
+ const parsed = raw === null ? Number.NaN : Number(raw);
+ if (!Number.isFinite(parsed)) return 28;
+ return Math.min(INSPECTOR_MAX_PERCENT, Math.max(INSPECTOR_MIN_PERCENT, parsed));
+}
+
+type SessionViewProps = {
+ sessionId: string;
+ /** When entered via /projects/$projectId/sessions/... — used for the back-nav target. */
+ projectId?: string;
+};
+
+// The session detail screen: the persistent terminal + git rail. Rendered by
+// both the project-scoped and cross-project session routes. The terminal lives
+// here (not in the shell) — switching sessions only changes route params, so
+// TanStack Router keeps this component mounted and the terminal re-points its
+// mux without remounting (useTerminalSession). Leaving for the board unmounts
+// it; the server's output ring replays on return.
+//
+// The split is shadcn's resizable (react-resizable-panels v4) with a fully
+// collapsible inspector: the panel is `collapsible` and driven to 0% via the
+// imperative API from the ui-store (Topbar button / ⌘⇧B), animated by the
+// flex-grow transition in styles.css. Content keeps a stable min-width inside
+// the clipped panel so nothing reflows mid-animation; split width persists.
+export function SessionView({ sessionId, projectId }: SessionViewProps) {
+ const navigate = useNavigate();
+ const workspaceQuery = useWorkspaceQuery();
+ const workspaces = workspaceQuery.data ?? [];
+ const { theme } = useUiStore();
+ const isInspectorOpen = useUiStore((state) => state.isInspectorOpen);
+ const toggleInspector = useUiStore((state) => state.toggleInspector);
+ const { daemonStatus } = useShell();
+ const inspectorRef = useRef(null);
+
+ const session = workspaces.flatMap((workspace) => workspace.sessions).find((s) => s.id === sessionId);
+ const isOrchestrator = session ? isOrchestratorSession(session) : false;
+ const workspace =
+ (session && workspaces.find((w) => w.id === session.workspaceId)) ??
+ (projectId ? workspaces.find((w) => w.id === projectId) : undefined);
+
+ // Orchestrator sessions are terminal-only; only worker sessions have the rail.
+ const hasInspector = !isOrchestrator;
+ // Frozen at mount: rrp re-registers the panel (a layout effect keyed on
+ // defaultSize, among others) whenever this prop's identity changes, and the
+ // imperative collapse()/expand() below can race that re-registration within
+ // the same commit — rrp then throws "Panel constraints not found for Panel
+ // inspector", which unwinds the whole route to the router's CatchBoundary
+ // (the toggle button looks dead and the session view is torn down).
+ // defaultSize only matters at first mount; afterwards the imperative API
+ // owns the size, so it must never track live open/closed state.
+ const [inspectorDefaultSize] = useState(() => (isInspectorOpen ? `${initialSplitPercent()}%` : "0%"));
+
+ useEffect(() => {
+ if (!hasInspector) return;
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (event.key.toLowerCase() !== "b" || !event.shiftKey) return;
+ if (!event.metaKey && !event.ctrlKey) return;
+ event.preventDefault();
+ toggleInspector();
+ };
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, [hasInspector, toggleInspector]);
+
+ // Drive the collapsible panel from the store so the Topbar button, ⌘⇧B, and
+ // drag-to-collapse all stay in sync.
+ useEffect(() => {
+ const panel = inspectorRef.current;
+ if (!panel) return;
+ if (isInspectorOpen) {
+ panel.expand();
+ // expand() restores the "most recent" size, which is 0 when the panel
+ // mounted collapsed — fall back to the persisted split.
+ if (panel.getSize().asPercentage === 0) panel.resize(`${initialSplitPercent()}%`);
+ } else {
+ panel.collapse();
+ }
+ }, [hasInspector, isInspectorOpen]);
+
+ // Persist drags and mirror collapse state (dragging past minSize collapses)
+ // back into the store. Read the store imperatively to avoid a stale closure.
+ const handleInspectorResize = (size: PanelSize) => {
+ const open = useUiStore.getState().isInspectorOpen;
+ if (size.asPercentage > 0) {
+ window.localStorage?.setItem(inspectorSplitStorageKey, String(size.asPercentage));
+ if (!open) toggleInspector();
+ } else if (open) {
+ toggleInspector();
+ }
+ };
+
+ if (!session && !workspaceQuery.isLoading) {
+ return (
+
+ Session not found. It may have been cleaned up — pick another from the sidebar.
+
+ );
+ }
+
+ return (
+
+
+ workspace
+ ? void navigate({ to: "/projects/$projectId", params: { projectId: workspace.id } })
+ : void navigate({ to: "/" })
+ }
+ projectLabel={workspace?.name}
+ session={session}
+ view={isOrchestrator ? "orchestrator" : "session"}
+ />
+
+ {/* react-resizable-panels v4: bare numbers are PIXELS; percentages must
+ be strings. Numeric sizes here once clamped the inspector to 45px. */}
+
+
+
+ {hasInspector ? (
+ <>
+
+
+ {/* Stable content width while the panel animates (yyork pattern):
+ the pane clips instead of reflowing the inspector mid-collapse. */}
+
+
+
+
+ >
+ ) : null}
+
+
+ );
+}
diff --git a/frontend/src/renderer/components/SessionsBoard.tsx b/frontend/src/renderer/components/SessionsBoard.tsx
new file mode 100644
index 000000000..1b2aad2e7
--- /dev/null
+++ b/frontend/src/renderer/components/SessionsBoard.tsx
@@ -0,0 +1,202 @@
+import { useNavigate } from "@tanstack/react-router";
+import {
+ type AttentionZone,
+ type WorkerDisplayStatus,
+ type WorkspaceSession,
+ attentionZone,
+ workerDisplayStatus,
+ workerSessions,
+} from "../types/workspace";
+import { useWorkspaceQuery } from "../hooks/useWorkspaceQuery";
+import { DashboardSubhead, DashboardTopbar } from "./DashboardTopbar";
+import { cn } from "../lib/utils";
+
+type SessionsBoardProps = {
+ /** When set, the board shows only this project's sessions. */
+ projectId?: string;
+};
+
+// The four kanban columns, left→right by flow (work → review → merge), ported
+// verbatim from agent-orchestrator (SIMPLE_KANBAN_LEVELS + AttentionZone +
+// mc-board.css). "done" is archived, not a column.
+type Column = {
+ level: AttentionZone;
+ label: string;
+ glow: string;
+ dot: string;
+ dotGlow: boolean;
+ titleClass: string;
+};
+const COLUMNS: Column[] = [
+ {
+ level: "working",
+ label: "Working",
+ glow: "color-mix(in srgb, var(--orange) 7%, transparent)",
+ dot: "var(--orange)",
+ dotGlow: true,
+ titleClass: "text-working",
+ },
+ {
+ level: "action",
+ label: "Needs you",
+ glow: "color-mix(in srgb, var(--amber) 6%, transparent)",
+ dot: "var(--amber)",
+ dotGlow: true,
+ titleClass: "text-warning",
+ },
+ {
+ level: "pending",
+ label: "In review",
+ glow: "var(--kanban-pending-glow)",
+ dot: "var(--fg-muted)",
+ dotGlow: false,
+ titleClass: "text-muted-foreground",
+ },
+ {
+ level: "merge",
+ label: "Ready to merge",
+ glow: "color-mix(in srgb, var(--green) 7%, transparent)",
+ dot: "var(--green)",
+ dotGlow: true,
+ titleClass: "text-success",
+ },
+];
+
+const BADGE: Record = {
+ working: { label: "Working", className: "text-working" },
+ needs_you: { label: "Needs input", className: "text-warning" },
+ ci_failed: { label: "CI failed", className: "text-error" },
+ mergeable: { label: "Ready", className: "text-success" },
+ done: { label: "Done", className: "text-passive" },
+};
+
+export function SessionsBoard({ projectId }: SessionsBoardProps) {
+ const navigate = useNavigate();
+ const workspaceQuery = useWorkspaceQuery();
+ const all = workspaceQuery.data ?? [];
+ const workspaces = projectId ? all.filter((w) => w.id === projectId) : all;
+ const sessions = workspaces.flatMap((w) => workerSessions(w.sessions));
+ const projectLabel = projectId ? (workspaces[0]?.name ?? projectId) : "agent-orchestrator";
+
+ const byZone = new Map();
+ for (const session of sessions) {
+ const zone = attentionZone(session);
+ (byZone.get(zone) ?? byZone.set(zone, []).get(zone)!).push(session);
+ }
+ const done = byZone.get("done") ?? [];
+
+ const openSession = (session: WorkspaceSession) =>
+ void navigate({
+ to: "/projects/$projectId/sessions/$sessionId",
+ params: { projectId: session.workspaceId, sessionId: session.id },
+ });
+
+ return (
+
+
+
+
+
+ {workspaceQuery.isError ? (
+
Could not load sessions.
+ ) : (
+
+ {COLUMNS.map((col) => (
+
+ ))}
+
+ )}
+
+
+ {done.length > 0 && (
+
+
+
+ Done
+ {done.length}
+
+
+ {done.map((s) => (
+ openSession(s)}
+ type="button"
+ >
+ {s.title}
+
+ ))}
+
+
+ )}
+
+ );
+}
+
+function ZoneColumn({
+ col,
+ sessions,
+ onOpen,
+}: {
+ col: Column;
+ sessions: WorkspaceSession[];
+ onOpen: (s: WorkspaceSession) => void;
+}) {
+ return (
+
+
+
+ {col.label}
+ {sessions.length}
+
+
+
+ {sessions.map((session) => (
+ onOpen(session)} />
+ ))}
+
+
+
+ );
+}
+
+function SessionCard({ session, onOpen }: { session: WorkspaceSession; onOpen: () => void }) {
+ const badge = BADGE[workerDisplayStatus(session)];
+ const branch = session.branch || `session/${session.id}`;
+ return (
+
+
+
+
+ {badge.label}
+
+ {session.id}
+
+
+ {session.title}
+
+ {branch}
+
+ {session.pullRequest ? `PR #${session.pullRequest.number} · ${session.pullRequest.state}` : "no PR yet"}
+
+
+ );
+}
diff --git a/frontend/src/renderer/components/SideRail.tsx b/frontend/src/renderer/components/SideRail.tsx
deleted file mode 100644
index 25e273ce1..000000000
--- a/frontend/src/renderer/components/SideRail.tsx
+++ /dev/null
@@ -1,206 +0,0 @@
-import { GitBranch, GitCommitHorizontal, GitPullRequest, LoaderCircle, Plus, Square, Trash2 } from "lucide-react";
-import type { WorkbenchView } from "../stores/ui-store";
-import {
- type WorkerDisplayStatus,
- type WorkspaceSession,
- type WorkspaceSummary,
- workerDisplayStatus,
- workerStatusLabel,
- workerStatusPulses,
-} from "../types/workspace";
-import { Button } from "./ui/button";
-import { cn } from "../lib/utils";
-
-// Session status is a single glyph, no text: spinner while working, a PR icon
-// when there's a PR, otherwise a colored dot. See DESIGN.md.
-const dotTone: Record = {
- working: "bg-accent",
- needs_you: "bg-warning",
- mergeable: "bg-success",
- ci_failed: "bg-error",
- done: "bg-passive",
-};
-
-const prTone: Record = {
- working: "text-accent",
- needs_you: "text-warning",
- mergeable: "text-success",
- ci_failed: "text-error",
- done: "text-muted-foreground",
-};
-
-function StatusGlyph({ worker }: { worker: WorkspaceSession }) {
- const status = workerDisplayStatus(worker);
- let glyph: React.ReactNode;
- if (status === "working") {
- glyph = ;
- } else if (worker.pullRequest) {
- glyph = ;
- } else {
- glyph = (
-
- );
- }
- return (
-
- {glyph}
-
- );
-}
-
-type SideRailProps = {
- view: WorkbenchView;
- session?: WorkspaceSession;
- workspaces: WorkspaceSummary[];
- onSelectSession: (sessionId: string, workspaceId: string) => void;
-};
-
-export function SideRail({ view, session, workspaces, onSelectSession }: SideRailProps) {
- return (
-
- {view === "orchestrator" ? (
-
- ) : (
-
- )}
-
- );
-}
-
-function WorkersList({
- workspaces,
- onSelectSession,
-}: {
- workspaces: WorkspaceSummary[];
- onSelectSession: (sessionId: string, workspaceId: string) => void;
-}) {
- const workers = workspaces.flatMap((workspace) => workspace.sessions);
- return (
- <>
-
-
- {workers.length === 0 ? (
-
No workers yet.
- ) : (
- workers.map((worker) => {
- const pr = worker.pullRequest;
- const subtitle = [worker.workspaceName, pr ? `PR #${pr.number}` : worker.branch]
- .filter(Boolean)
- .join(" · ");
- return (
-
onSelectSession(worker.id, worker.workspaceId)}
- type="button"
- >
-
-
- {worker.title}
- {subtitle}
-
-
- );
- })
- )}
-
- >
- );
-}
-
-function GitRail({ session }: { session?: WorkspaceSession }) {
- const files = session?.changedFiles ?? [];
-
- return (
- <>
-
-
-
-
- All files
-
-
-
- Discard all
-
-
-
- Stage all
-
-
-
-
- {files.length === 0 ? (
-
No changes yet.
- ) : (
- files.map((file) => (
-
- {file.path}
- +{file.additions}
- −{file.deletions}
-
-
- ))
- )}
-
-
-
-
-
-
-
- Commit & Push
-
-
-
-
-
-
{session?.branch || "—"}
-
-
-
- Create PR
-
-
- >
- );
-}
-
-function SideHead({ title, count }: { title: string; count: number }) {
- return (
-
- {title}
- {count}
-
- );
-}
diff --git a/frontend/src/renderer/components/Sidebar.tsx b/frontend/src/renderer/components/Sidebar.tsx
index f206e4ecb..80137a026 100644
--- a/frontend/src/renderer/components/Sidebar.tsx
+++ b/frontend/src/renderer/components/Sidebar.tsx
@@ -1,11 +1,47 @@
-import { ChevronsUpDown, Folder, Plus, Search, Settings, Waypoints } from "lucide-react";
+import { useNavigate, useParams, useRouterState } from "@tanstack/react-router";
+import { ChevronRight, GitPullRequest, Moon, Plus, Search, Settings, Sun, Waypoints } from "lucide-react";
import { useState } from "react";
-import { sessionIsActive, sessionNeedsAttention, type WorkspaceSummary } from "../types/workspace";
-import { useUiStore } from "../stores/ui-store";
+import { attentionZone, type WorkspaceSession, type WorkspaceSummary, workerSessions } from "../types/workspace";
import { aoBridge } from "../lib/bridge";
import { useEventsConnection } from "../hooks/useEventsConnection";
+import { useResizable } from "../hooks/useResizable";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuTrigger,
+} from "./ui/dropdown-menu";
+import {
+ Sidebar as SidebarRoot,
+ SidebarContent,
+ SidebarFooter,
+ SidebarGroup,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarMenu,
+ SidebarMenuAction,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarMenuSub,
+ SidebarMenuSubButton,
+ SidebarMenuSubItem,
+ SidebarTrigger,
+ useSidebar,
+} from "./ui/sidebar";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
import { cn } from "../lib/utils";
+import { useUiStore } from "../stores/ui-store";
+
+// macOS hiddenInset traffic lights (x:14, y:14) occupy the sidebar's top-left;
+// the sidebar gives them a real 40px titlebar strip (draggable; the fixed
+// TitlebarNav overlay sits beside the lights), and the collapsed icon rail
+// keeps a matching 40px inset. Windows/Linux keep the verbatim 14px padding.
+const isMac = typeof navigator !== "undefined" && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent);
+const dragStyle = isMac ? ({ WebkitAppRegion: "drag" } as React.CSSProperties) : undefined;
+const noDragStyle = isMac ? ({ WebkitAppRegion: "no-drag" } as React.CSSProperties) : undefined;
type SidebarProps = {
daemonStatus: { state: string; message?: string };
@@ -15,155 +51,370 @@ type SidebarProps = {
onNewWorker: (projectId: string) => void;
};
-function fleetSummary(workspaces: WorkspaceSummary[]) {
- const sessions = workspaces.flatMap((workspace) => workspace.sessions);
- const agents = sessions.filter(sessionIsActive).length;
- const needYou = sessions.filter(sessionNeedsAttention).length;
- return { agents, needYou };
+// Selection state comes from the URL: which project/session is active is the
+// route params, and clicks navigate rather than mutate a store.
+function useSelection() {
+ const navigate = useNavigate();
+ const params = useParams({ strict: false }) as { projectId?: string; sessionId?: string };
+ const pathname = useRouterState({ select: (state) => state.location.pathname });
+ return {
+ isHome: pathname === "/",
+ activeProjectId: params.projectId,
+ activeSessionId: params.sessionId,
+ goHome: () => void navigate({ to: "/" }),
+ goPrs: () => void navigate({ to: "/prs" }),
+ goReview: () => void navigate({ to: "/review" }),
+ goSettings: (projectId: string) => void navigate({ to: "/projects/$projectId/settings", params: { projectId } }),
+ goProject: (projectId: string) => void navigate({ to: "/projects/$projectId", params: { projectId } }),
+ goSession: (projectId: string, sessionId: string) =>
+ void navigate({ to: "/projects/$projectId/sessions/$sessionId", params: { projectId, sessionId } }),
+ };
}
-export function Sidebar({ daemonStatus, workspaceError, workspaces, onCreateProject, onNewWorker }: SidebarProps) {
- const {
- isSidebarOpen,
- view,
- selectedSessionId,
- selectedWorkspaceId,
- selectOrchestrator,
- selectSession,
- selectWorkspace,
- } = useUiStore();
- const { agents, needYou } = fleetSummary(workspaces);
- const eventsConnection = useEventsConnection();
-
- if (!isSidebarOpen) {
- return (
-
- );
- }
-
+// agent-orchestrator's SessionDot: 6px dot, neutral grey at rest, orange +
+// breathe while the agent is working. Other attention zones stay neutral here
+// (the board carries the richer colour coding).
+function SessionDot({ session }: { session: WorkspaceSession }) {
+ const working = attentionZone(session) === "working";
return (
-
-
- {/* Orchestrator anchor — ReverbCode's one addition over emdash. */}
-
-
- Orchestrator
- {agents} agents
-
-
-
- Projects
-
-
-
- {workspaceError ? (
-
-
Could not load projects.
-
{workspaceError}
-
- ) : workspaces.length === 0 ? (
-
-
No projects yet.
-
- Click + above to register a git repo.
-
-
- ) : (
- workspaces.map((workspace) => (
-
-
- selectWorkspace(workspace.id)}
- type="button"
- >
-
- {workspace.name}
-
- onNewWorker(workspace.id)}
- projectName={workspace.name}
- visible={selectedWorkspaceId === workspace.id && view !== "session"}
- />
-
-
- {workspace.sessions.map((session) => {
- const active = view === "session" && selectedSessionId === session.id;
- return (
- selectSession(session.id, workspace.id)}
- type="button"
- >
- {session.title}
-
- );
- })}
-
- ))
- )}
-
-
-
- } label="Search" shortcut="⌘K" />
- }
- label="Settings"
- shortcut="⌘,"
- />
-
-
-
-
- R
-
-
-
ReverbCode
-
- daemon {daemonStatus.state}
- {eventsConnection === "disconnected" && " · events offline"}
-
-
-
-
-
+
);
}
-function FooterRow({ icon, label, shortcut }: { icon: React.ReactNode; label: string; shortcut: string }) {
+// Built on shadcn's sidebar primitives (components/ui/sidebar): the provider in
+// _shell owns open state (synced to the ui-store) and `collapsible="icon"`
+// replaces the old hand-rolled CollapsedRail — the same tree restyles itself
+// via group-data-[collapsible=icon] into the 48px letter rail.
+export function Sidebar({ daemonStatus, workspaceError, workspaces, onCreateProject, onNewWorker }: SidebarProps) {
+ const selection = useSelection();
+ const eventsConnection = useEventsConnection();
+ const { state } = useSidebar();
+ const theme = useUiStore((s) => s.theme);
+ const toggleTheme = useUiStore((s) => s.toggleTheme);
+ // Disclosure state: projects are expanded by default; a project id present in
+ // this set is collapsed (sessions hidden).
+ const [collapsedIds, setCollapsedIds] = useState>(() => new Set());
+ const toggleCollapsed = (id: string) =>
+ setCollapsedIds((prev) => {
+ const next = new Set(prev);
+ next.has(id) ? next.delete(id) : next.add(id);
+ return next;
+ });
+ // agent-orchestrator's sidebar resize: drag the right edge (200–420px,
+ // persisted), double-click to reset to 240px. Drives --ao-sidebar-w on :root,
+ // which the provider forwards into shadcn's --sidebar-width.
+ const { onPointerDown: onResizePointerDown, onDoubleClick: onResizeDoubleClick } = useResizable({
+ cssVar: "--ao-sidebar-w",
+ storageKey: "ao-sidebar-w",
+ defaultWidth: 240,
+ min: 200,
+ max: 420,
+ edge: "right",
+ });
+
return (
-
- {icon}
- {label}
- {shortcut}
-
+
+
+ {/* Titlebar strip: a draggable 40px inset under the traffic lights and
+ the fixed TitlebarNav overlay (rendered once by the shell), kept in
+ both sidebar states. */}
+ {isMac &&
}
+
+ {/* Brand (project-sidebar__brand); in the icon rail it becomes the old
+ 36px board button wrapping the 22px accent mark. */}
+
+
+
+
+
+
+
+
+
+
+ Orchestrator board
+
+
+
+ Agent Orchestrator
+
+ {/* On macOS the toggle lives in the titlebar cluster instead. */}
+ {!isMac && (
+
+
+
+
+ Collapse sidebar · ⌘B
+
+ )}
+
+
+
+
+
+ {/* Section label (project-sidebar__nav-label) */}
+
+
+ Projects
+
+
+
+
+ {/* Tree (project-sidebar__tree) */}
+
+ {workspaceError ? (
+
+
Could not load projects.
+
{workspaceError}
+
+ ) : workspaces.length === 0 ? (
+
+
No projects yet.
+
+ Click + above to register a git repo.
+
+
+ ) : (
+
+ {workspaces.map((workspace) => (
+ toggleCollapsed(workspace.id)}
+ onNewWorker={() => onNewWorker(workspace.id)}
+ />
+ ))}
+
+ )}
+
+
+
+
+ {/* Footer (project-sidebar__footer) — single Settings menu. Divergence
+ (user-requested 2026-06-10): the trigger stretches the full row width
+ (flex-1) with a uniform 7px footer inset on all sides (reference uses
+ 12px top, 0 bottom, content-hugging button). The icon rail swaps it
+ for the old rail footer: New project (+ expand toggle off macOS). */}
+
+
+
+
+
+
+ Settings
+
+
+
+
+ {theme === "dark" ? : }
+ {theme === "dark" ? "Light mode" : "Dark mode"}
+
+
+
+
+ Pull requests
+
+
+
+ Reviews
+
+
+
+ Search
+ ⌘K
+
+ {selection.activeProjectId && (
+ <>
+
+ selection.goSettings(selection.activeProjectId!)}>
+
+ Project settings
+
+ >
+ )}
+
+
+
+
+
+
+
+ daemon {daemonStatus.state}
+ {eventsConnection === "disconnected" && " · events offline"}
+
+
+
+
+
+ {!isMac && (
+
+
+
+
+ Expand sidebar · ⌘B
+
+ )}
+
+
+
+
+
+ );
+}
+
+type Selection = ReturnType;
+
+function ProjectItem({
+ workspace,
+ expanded,
+ selection,
+ onToggle,
+ onNewWorker,
+}: {
+ workspace: WorkspaceSummary;
+ expanded: boolean;
+ selection: Selection;
+ onToggle: () => void;
+ onNewWorker: () => void;
+}) {
+ const projectActive = selection.activeProjectId === workspace.id && !selection.activeSessionId;
+
+ const onProjectClick = () => {
+ if (!expanded) {
+ onToggle();
+ selection.goProject(workspace.id);
+ } else if (projectActive) {
+ onToggle();
+ } else {
+ selection.goProject(workspace.id);
+ }
+ };
+
+ return (
+
+ {/* project-sidebar__proj-row */}
+
+
+ {workspace.name.charAt(0).toUpperCase()}
+ {workspace.name}
+
+ {workerSessions(workspace.sessions).length}
+
+
+ {/* project-sidebar__proj-actions — reveal over the count slot on hover */}
+
+
+
+
+
+
+ New worker in {workspace.name}
+
+
+ {/* project-sidebar__sessions */}
+ {expanded && workerSessions(workspace.sessions).length > 0 && (
+
+ {workerSessions(workspace.sessions).map((session) => {
+ const active = selection.activeSessionId === session.id;
+ return (
+
+
+ selection.goSession(workspace.id, session.id)}
+ type="button"
+ >
+
+
+
+ {session.title}
+
+ {session.id}
+
+
+
+
+ );
+ })}
+
+ )}
+
);
}
@@ -190,7 +441,7 @@ function CreateProjectButton({ onCreateProject }: Pick
);
}
-
-function NewWorkerButton({
- onClick,
- projectName,
- visible,
-}: {
- onClick: () => void;
- projectName: string;
- visible: boolean;
-}) {
- return (
-
-
-
-
-
-
- New worker in {projectName}
-
- );
-}
-
-function CollapsedRail({
- agents,
- needYou,
- workspaces,
- onCreateProject,
-}: {
- agents: number;
- needYou: number;
- workspaces: WorkspaceSummary[];
- onCreateProject: SidebarProps["onCreateProject"];
-}) {
- const { view, selectedWorkspaceId, selectOrchestrator, selectWorkspace } = useUiStore();
- return (
-
-
-
-
-
-
-
-
- Orchestrator · {agents} agents · {needYou} need you
-
-
-
-
- {workspaces.map((workspace) => (
-
-
- selectWorkspace(workspace.id)}
- type="button"
- >
-
-
-
- {workspace.name}
-
- ))}
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/frontend/src/renderer/components/SpawnWorkerModal.test.tsx b/frontend/src/renderer/components/SpawnWorkerModal.test.tsx
index 38d6f68f4..b9d5588ef 100644
--- a/frontend/src/renderer/components/SpawnWorkerModal.test.tsx
+++ b/frontend/src/renderer/components/SpawnWorkerModal.test.tsx
@@ -1,8 +1,8 @@
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
-import { SpawnWorkerModal } from "./SpawnWorkerModal";
import { TooltipProvider } from "./ui/tooltip";
+import { SpawnWorkerModal } from "./SpawnWorkerModal";
import type { WorkspaceSummary } from "../types/workspace";
const workspaces: WorkspaceSummary[] = [{ id: "proj-1", name: "my-app", path: "/p", type: "main", sessions: [] }];
@@ -23,12 +23,30 @@ function renderModal(onCreateTask = vi.fn().mockResolvedValue(undefined), onOpen
}
describe("SpawnWorkerModal", () => {
- it("requires a non-empty prompt before it can spawn", () => {
+ // Regression: "Based on main" must NOT send branch:"main" — git refuses a
+ // second worktree on a checked-out branch, so the daemon 409s. Omitting it
+ // lets the daemon mint a fresh ao/.
+ it("omits the base branch from the spawn payload", async () => {
+ const user = userEvent.setup();
+ const onCreateTask = renderModal();
+
+ await user.type(await screen.findByLabelText("Prompt"), "do the thing");
+ await user.click(screen.getByRole("button", { name: /Spawn worker/ }));
+
+ expect(onCreateTask).toHaveBeenCalledWith(
+ expect.objectContaining({ projectId: "proj-1", prompt: "do the thing", branch: undefined }),
+ );
+ });
+
+ it("requires a non-empty prompt before it can spawn", async () => {
const onCreateTask = renderModal();
expect(screen.getByRole("button", { name: /Spawn worker/ })).toBeDisabled();
expect(onCreateTask).not.toHaveBeenCalled();
});
+ // Regression: a failed spawn (e.g. 409 BRANCH_CHECKED_OUT_ELSEWHERE) must
+ // keep the modal open with the daemon's message inline and the input intact,
+ // disable submit only while in flight, and allow re-submitting.
it("keeps the modal open and shows the daemon error when the spawn fails", async () => {
const user = userEvent.setup();
const onOpenChange = vi.fn();
diff --git a/frontend/src/renderer/components/SpawnWorkerModal.tsx b/frontend/src/renderer/components/SpawnWorkerModal.tsx
index 02c7311da..e87b7ab78 100644
--- a/frontend/src/renderer/components/SpawnWorkerModal.tsx
+++ b/frontend/src/renderer/components/SpawnWorkerModal.tsx
@@ -18,6 +18,9 @@ const agentOptions: { value: AgentProvider; label: string }[] = [
];
const basedOnTabs = ["Branch", "Issue", "Pull Request"] as const;
+// The project's default branch — selecting it in "Based on" means "new session
+// branch off the default", not "check out the default branch itself".
+const BASE_BRANCH = "main";
type BasedOn = (typeof basedOnTabs)[number];
const NAME_RULE = /^[a-z0-9-]+$/;
@@ -27,7 +30,12 @@ type SpawnWorkerModalProps = {
onOpenChange: (open: boolean) => void;
workspaces: WorkspaceSummary[];
defaultProjectId?: string;
- onCreateTask: (input: { projectId: string; prompt: string; name?: string; harness?: AgentProvider }) => Promise;
+ onCreateTask: (input: {
+ projectId: string;
+ prompt: string;
+ branch?: string;
+ harness?: AgentProvider;
+ }) => Promise;
};
export function SpawnWorkerModal({
@@ -42,6 +50,7 @@ export function SpawnWorkerModal({
const [projectId, setProjectId] = useState(fallbackProjectId);
const [agent, setAgent] = useState("claude-code");
const [basedOn, setBasedOn] = useState("Branch");
+ const [branch, setBranch] = useState(BASE_BRANCH);
const [tab, setTab] = useState<"Prompt" | "Workspace">("Prompt");
const [prompt, setPrompt] = useState("");
const [error, setError] = useState(null);
@@ -56,6 +65,9 @@ export function SpawnWorkerModal({
}, [open, fallbackProjectId]);
const selectedWorkspace = workspaces.find((workspace) => workspace.id === projectId) ?? workspaces[0];
+ const branchOptions = Array.from(
+ new Set([BASE_BRANCH, ...(selectedWorkspace?.sessions.map((session) => session.branch).filter(Boolean) ?? [])]),
+ );
const nameValid = name === "" || NAME_RULE.test(name);
const canSubmit = prompt.trim().length > 0 && projectId !== "" && nameValid && !isSubmitting;
@@ -65,14 +77,23 @@ export function SpawnWorkerModal({
setError(null);
setIsSubmitting(true);
try {
+ // The API's `branch` field means "check out this exact branch in the
+ // session worktree" — valid for resuming an existing session branch, but
+ // never for the base branch itself (git refuses a second worktree on a
+ // checked-out branch; daemon manager.go). "Based on main" therefore
+ // OMITS branch so the daemon mints a fresh ao/ off the
+ // project's default branch.
+ const trimmedBranch = branch.trim();
await onCreateTask({
projectId,
prompt: prompt.trim(),
- name: name || undefined,
+ branch:
+ basedOn === "Branch" && trimmedBranch !== "" && trimmedBranch !== BASE_BRANCH ? trimmedBranch : undefined,
harness: agent,
});
setName("");
setPrompt("");
+ setBranch(BASE_BRANCH);
onOpenChange(false);
} catch (err) {
setError(err instanceof Error ? err.message : "Could not spawn worker");
@@ -159,15 +180,19 @@ export function SpawnWorkerModal({
-
- {/* The API has no per-spawn base branch — the worker branches off the
- project's configured default branch in a fresh worktree. */}
- {basedOn === "Branch"
- ? "Branches off the project's default branch in a fresh worktree."
- : basedOn === "Issue"
- ? "Pick an issue to start from."
- : "Pick a pull request to start from."}
-
+ {basedOn === "Branch" ? (
+
({ value: option, label: option }))}
+ />
+ ) : (
+
+ {basedOn === "Issue" ? "Pick an issue to start from." : "Pick a pull request to start from."}
+
+ )}
diff --git a/frontend/src/renderer/components/TerminalPane.tsx b/frontend/src/renderer/components/TerminalPane.tsx
index fc9631191..0adc146ac 100644
--- a/frontend/src/renderer/components/TerminalPane.tsx
+++ b/frontend/src/renderer/components/TerminalPane.tsx
@@ -1,13 +1,8 @@
-import { useEffect, useRef } from "react";
-import { Terminal } from "@xterm/xterm";
-import { CanvasAddon } from "@xterm/addon-canvas";
-import { WebglAddon } from "@xterm/addon-webgl";
-import { FitAddon } from "@xterm/addon-fit";
-import { SearchAddon } from "@xterm/addon-search";
-import { WebLinksAddon } from "@xterm/addon-web-links";
+import { useCallback, useEffect, useRef, useState } from "react";
import type { WorkspaceSession } from "../types/workspace";
import type { Theme } from "../stores/ui-store";
import { useTerminalSession, type AttachableTerminal, type TerminalSessionState } from "../hooks/useTerminalSession";
+import { XtermTerminal } from "./XtermTerminal";
type TerminalPaneProps = {
session?: WorkspaceSession;
@@ -31,37 +26,7 @@ export function TerminalPane({ session, theme, daemonReady }: TerminalPaneProps)
);
}
- return ;
-}
-
-function webgl2Available(): boolean {
- try {
- return Boolean(document.createElement("canvas").getContext("webgl2"));
- } catch {
- return false;
- }
-}
-
-// Load the GPU-accelerated WebGL renderer when a real WebGL2 context is
-// available, falling back to the 2D canvas renderer otherwise (software
-// rendering, older GPUs). Probing first avoids loading a half-initialised
-// WebglAddon that then throws on dispose. Renderer addons load after open().
-function attachRenderer(terminal: Terminal): void {
- if (webgl2Available()) {
- try {
- const webgl = new WebglAddon();
- webgl.onContextLoss(() => webgl.dispose());
- terminal.loadAddon(webgl);
- return;
- } catch {
- // WebGL init failed despite the probe; fall through to canvas.
- }
- }
- try {
- terminal.loadAddon(new CanvasAddon());
- } catch {
- // The renderer addon is an optimisation; the DOM renderer still works.
- }
+ return ;
}
function bannerText(state: TerminalSessionState, error?: string): string | undefined {
@@ -70,103 +35,64 @@ function bannerText(state: TerminalSessionState, error?: string): string | undef
return undefined;
}
-const CLEAR_SEQUENCE = "\x1b[3J\x1b[2J\x1b[H";
-
-function XtermTerminal({ session, theme, daemonReady }: TerminalPaneProps) {
- const containerRef = useRef(null);
- const terminalRef = useRef(null);
+function AttachedTerminal({ session, theme, daemonReady }: TerminalPaneProps) {
+ // One terminal instance per pane lifetime (yyork's core rule): switching
+ // sessions never remounts XtermTerminal — the attachment effect re-points
+ // the mux and clears the screen instead. A keyed remount would tear down the
+ // renderer mid-switch and lose the warm GPU surface.
+ const [terminal, setTerminal] = useState(null);
+ const [initFailed, setInitFailed] = useState(false);
const { attach, state, error } = useTerminalSession(session, { daemonReady });
+ const handleId = session?.terminalHandleId;
+ const hadAttachmentRef = useRef(false);
+
+ const handleReady = useCallback((handle: AttachableTerminal) => setTerminal(handle), []);
+ const handleInitError = useCallback((err: unknown) => {
+ console.error("xterm failed to initialize", err);
+ setInitFailed(true);
+ }, []);
useEffect(() => {
- if (!containerRef.current) return;
-
- const terminal = new Terminal({
- allowProposedApi: false,
- cursorBlink: true,
- fontFamily: 'Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
- fontSize: 13,
- lineHeight: 1.35,
- // Zellij owns scrollback inside the attached pane; keeping xterm
- // scrollback creates a dead scrollbar beside the alt-buffer app.
- scrollback: 0,
- theme: terminalTheme(theme),
- });
- terminalRef.current = terminal;
- const fitAddon = new FitAddon();
-
- terminal.loadAddon(fitAddon);
- terminal.loadAddon(new WebLinksAddon());
- terminal.loadAddon(new SearchAddon());
- terminal.open(containerRef.current);
- attachRenderer(terminal);
-
- let detach: (() => void) | undefined;
- let rafId: number | undefined;
-
- // The attachment forwards size changes itself (terminal.onResize → mux);
- // the component only owns fitting the terminal to its container.
- const fitTerminal = () => {
- if (!containerRef.current?.clientWidth || !containerRef.current.clientHeight) return;
- try {
- fitAddon.fit();
- } catch {
- // Electron can report zero-sized panels during startup; the next resize will retry.
- }
- };
-
- if (session?.terminalHandleId) {
- rafId = requestAnimationFrame(() => {
- fitTerminal();
- const attachable: AttachableTerminal = {
- get cols() {
- return terminal.cols;
- },
- get rows() {
- return terminal.rows;
- },
- write: (data) => terminal.write(data),
- writeln: (line) => terminal.writeln(line),
- clear: () => terminal.write(CLEAR_SEQUENCE),
- onData: (listener) => terminal.onData(listener),
- onResize: (listener) => terminal.onResize(listener),
- };
- detach = attach(attachable);
- });
- } else {
- rafId = requestAnimationFrame(fitTerminal);
- terminal.writeln("Agent Orchestrator");
- terminal.writeln("");
- terminal.writeln("\x1b[2mNo session selected. Pick a worker to attach its terminal.\x1b[0m");
+ if (!terminal) return;
+ // Reuse means the previous session's screen would linger; clear before
+ // re-pointing. Screen-clear only, never reset(): every pane PTY is
+ // `zellij attach` with identical modes, and a full RIS would wipe the
+ // mouse-tracking mode zellij enabled at attach — the 50KB ring replay
+ // can't re-enable it, leaving wheel scroll dead after the first session
+ // switch (yyork's frozen-scroll regression, solved there the same way).
+ // Skipped on the very first attachment: the buffer is empty and the first
+ // fit may not have run yet.
+ if (hadAttachmentRef.current) {
+ terminal.clear();
}
+ hadAttachmentRef.current = true;
+ return attach(terminal);
+ }, [terminal, handleId, attach]);
- const resizeObserver = new ResizeObserver(fitTerminal);
- resizeObserver.observe(containerRef.current);
-
- return () => {
- if (rafId !== undefined) cancelAnimationFrame(rafId);
- resizeObserver.disconnect();
- detach?.();
- terminalRef.current = null;
- try {
- terminal.dispose();
- } catch {
- // Some xterm renderer addons can throw during dispose in certain GPU
- // environments; the terminal is being torn down regardless.
- }
- };
- }, [session?.id, session?.terminalHandleId, attach]);
-
- useEffect(() => {
- if (terminalRef.current) {
- terminalRef.current.options.theme = terminalTheme(theme);
- }
- }, [theme]);
+ if (initFailed) {
+ return (
+
+ Terminal failed to initialize on this GPU/driver. Restart the app to retry.
+
+ );
+ }
const banner = bannerText(state, error);
+ const showEmptyState = !handleId;
return (
-
-
+
+
+ {showEmptyState && (
+
+
+
Agent Orchestrator
+
+ No session selected. Pick a worker to attach its terminal.
+
+
+
+ )}
{banner && (
{banner}
@@ -175,16 +101,3 @@ function XtermTerminal({ session, theme, daemonReady }: TerminalPaneProps) {
);
}
-
-// The terminal is the agent CLI; it keeps the emdash dark palette (green cursor) in
-// both themes — see DESIGN.md → Color. The `theme` arg is kept for the signature the
-// caller uses on theme change.
-function terminalTheme(_theme: Theme) {
- return {
- background: "#161616",
- foreground: "#d7d7d2",
- cursor: "#7bd88f",
- cursorAccent: "#161616",
- selectionBackground: "rgba(63, 142, 247, 0.35)",
- };
-}
diff --git a/frontend/src/renderer/components/TitlebarNav.tsx b/frontend/src/renderer/components/TitlebarNav.tsx
new file mode 100644
index 000000000..7614d6b8e
--- /dev/null
+++ b/frontend/src/renderer/components/TitlebarNav.tsx
@@ -0,0 +1,77 @@
+import { useCanGoBack, useRouter, useRouterState } from "@tanstack/react-router";
+import { ArrowLeft, ArrowRight, PanelLeft } from "lucide-react";
+import { useUiStore } from "../stores/ui-store";
+
+const isMac = typeof navigator !== "undefined" && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent);
+const noDragStyle = isMac ? ({ WebkitAppRegion: "no-drag" } as React.CSSProperties) : undefined;
+
+// macOS-only titlebar cluster (sidebar toggle + history arrows) pinned beside
+// the traffic lights, VS Code-style. Approved divergence from the web
+// reference, which has no window chrome (DESIGN.md banner, 2026-06-10).
+// Rendered once by the shell as a fixed overlay (.titlebar-nav in styles.css)
+// so the buttons occupy the exact same spot whether the sidebar is expanded
+// or collapsed; the collapsed-rail topbars pad past it (.is-under-titlebar-nav).
+export function TitlebarNav() {
+ const { isSidebarOpen, toggleSidebar } = useUiStore();
+ const router = useRouter();
+ const canGoBack = useCanGoBack();
+ // No useCanGoForward in the installed router; derive it from the history
+ // index the same way useCanGoBack does (any back/forward/push re-renders
+ // via the location store, so history.length is read fresh).
+ const canGoForward = useRouterState({
+ select: (state) => state.location.state.__TSR_index < router.history.length - 1,
+ });
+
+ if (!isMac) return null;
+
+ return (
+
+
+
+
+
router.history.back()} title="Go back">
+
+
+
router.history.forward()}
+ title="Go forward"
+ >
+
+
+
+ );
+}
+
+function TitlebarButton({
+ label,
+ title,
+ disabled,
+ onClick,
+ children,
+}: {
+ label: string;
+ title: string;
+ disabled?: boolean;
+ onClick: () => void;
+ children: React.ReactNode;
+}) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/frontend/src/renderer/components/Topbar.tsx b/frontend/src/renderer/components/Topbar.tsx
index b43cca101..466b68a4c 100644
--- a/frontend/src/renderer/components/Topbar.tsx
+++ b/frontend/src/renderer/components/Topbar.tsx
@@ -1,20 +1,20 @@
-import {
- Columns2,
- FileText,
- GitPullRequest,
- MoreHorizontal,
- PanelLeft,
- Pin,
- Plus,
- Terminal,
- Waypoints,
-} from "lucide-react";
-import type { WorkbenchTab, WorkbenchView } from "../stores/ui-store";
-import type { WorkspaceSession, WorkspaceSummary } from "../types/workspace";
+import { Bell, GitBranch, LayoutGrid, PanelRightClose, PanelRightOpen, Waypoints } from "lucide-react";
+import { type WorkbenchView, useUiStore } from "../stores/ui-store";
+import type { WorkerDisplayStatus, WorkspaceSession } from "../types/workspace";
import { workerDisplayStatus } from "../types/workspace";
-import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
import { cn } from "../lib/utils";
+// Session status → pill tone, mirroring agent-orchestrator's StatusBadge
+// (working=orange & breathing, input=amber, fail=red, ready=green, done=neutral).
+// Tones are theme vars so the pill tracks the light/dark status palettes.
+const STATUS_PILL: Record
= {
+ working: { label: "Working", tone: "var(--orange)", breathe: true },
+ needs_you: { label: "Needs input", tone: "var(--amber)", breathe: false },
+ ci_failed: { label: "CI failed", tone: "var(--red)", breathe: false },
+ mergeable: { label: "Ready", tone: "var(--green)", breathe: false },
+ done: { label: "Done", tone: "var(--fg-muted)", breathe: false },
+};
+
const isMac = typeof navigator !== "undefined" && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent);
const dragStyle = isMac ? ({ WebkitAppRegion: "drag" } as React.CSSProperties) : undefined;
const noDragStyle = isMac ? ({ WebkitAppRegion: "no-drag" } as React.CSSProperties) : undefined;
@@ -22,173 +22,107 @@ const noDragStyle = isMac ? ({ WebkitAppRegion: "no-drag" } as React.CSSProperti
type TopbarProps = {
view: WorkbenchView;
session?: WorkspaceSession;
- workspace?: WorkspaceSummary;
- workbenchTab: WorkbenchTab;
- onSetWorkbenchTab: (tab: WorkbenchTab) => void;
- onNewWorker: () => void;
- onToggleSidebar: () => void;
+ /** Project crumb for orchestrator sessions (matches AO topbar-project-line). */
+ projectLabel?: string;
+ /** Back-to-board navigation for the Kanban / Open Kanban button. */
+ onOpenBoard?: () => void;
};
-export function Topbar({
- view,
- session,
- workspace,
- workbenchTab,
- onSetWorkbenchTab,
- onNewWorker,
- onToggleSidebar,
-}: TopbarProps) {
+export function Topbar({ view, session, projectLabel, onOpenBoard }: TopbarProps) {
+ const isSidebarOpen = useUiStore((state) => state.isSidebarOpen);
+ const isInspectorOpen = useUiStore((state) => state.isInspectorOpen);
+ const toggleInspector = useUiStore((state) => state.toggleInspector);
+
return (
- {isMac && }
-
-
-
-
- {view === "orchestrator" ? (
-
-
- Orchestrator
-
- ) : (
-
-
{session?.workspaceName ?? workspace?.name ?? "—"}
-
/
-
{session?.title ?? "session"}
-
-
- )}
-
-
+
{view === "orchestrator" ? (
- <>
-
-
- New worker
-
-
-
-
-
-
-
- >
+
+
+
+ {projectLabel ?? session?.workspaceName ?? "Project"}
+
+
+ ·
+
+
+
+ Orchestrator
+
+
+
) : (
- <>
-
-
onSetWorkbenchTab("changes")}
- >
-
-
-
onSetWorkbenchTab("files")}>
-
-
-
onSetWorkbenchTab("terminal")}
- >
-
-
-
-
-
- >
+
+
+
+ {session?.branch || `session/${session?.id ?? ""}`}
+
+ {session ?
: null}
+
+ )}
+
+
+
+
+
+ {/* Bell leads the actions row, as in AO's SessionDetailHeader. */}
+
+
+
+
+
+ {view === "orchestrator" ? "Open Kanban" : "Kanban"}
+
+ {/* Inspector collapse (worker sessions only — orchestrators have no rail). */}
+ {view === "session" && (
+
+ {isInspectorOpen ? (
+
+ ) : (
+
+ )}
+
)}
);
}
-function IconToggle({
- label,
- active = false,
- onClick,
- children,
-}: {
- label: string;
- active?: boolean;
- onClick?: () => void;
- children: React.ReactNode;
-}) {
+// StatusBadge --pill: tinted bordered pill (inset 25%-tone hairline + 7%-tone
+// fill) with a 6px dot that breathes while the agent is working.
+function SessionStatusPill({ session }: { session: WorkspaceSession }) {
+ const { label, tone, breathe } = STATUS_PILL[workerDisplayStatus(session)];
return (
-
-
-
- {children}
-
-
- {label}
-
- );
-}
-
-function PrPill({ session, workspace }: { session?: WorkspaceSession; workspace?: WorkspaceSummary }) {
- const pr = session?.pullRequest ?? workspace?.pullRequest;
- const status = session ? workerDisplayStatus(session) : "working";
-
- if (!pr) {
- return (
-
-
- Open PR
-
- );
- }
-
- const tone =
- status === "ci_failed"
- ? "border-error/40 bg-error/10 text-error"
- : status === "needs_you"
- ? "border-warning/40 bg-warning/10 text-warning"
- : "border-success/40 bg-success/10 text-success";
- const label = status === "ci_failed" ? "CI failed" : status === "needs_you" ? "review requested" : "mergeable";
-
- return (
-
-
- PR #{pr.number} · {label}
-
+
+ {label}
+
);
}
diff --git a/frontend/src/renderer/components/XtermTerminal.tsx b/frontend/src/renderer/components/XtermTerminal.tsx
new file mode 100644
index 000000000..a27c5e8e1
--- /dev/null
+++ b/frontend/src/renderer/components/XtermTerminal.tsx
@@ -0,0 +1,203 @@
+// Self-contained xterm.js surface, ported from yyork's terminal architecture.
+//
+// Design rules (the reason this component exists):
+// - The mount effect is dependency-free: the terminal instance is created once
+// per mount and NEVER torn down because a callback identity or session
+// changed. Session switching is the owner's job (re-point the mux, clear the
+// screen) — see TerminalPane.
+// - Nothing writes into the buffer at mount. Status/empty-state belongs to DOM
+// chrome around the terminal, not inside it. Writing before layout settles
+// is what crashed xterm's Viewport (`dimensions` of a zero-sized renderer).
+// - Fitting runs on several triggers, not one: FitAddon derives the column
+// count from measured cell width, and if it measures before the monospace
+// font's real metrics are resolved it over-counts columns and the grid
+// overflows the panel. So: next frame, two settle timeouts, fonts.ready,
+// and a ResizeObserver. xterm itself only fires onResize when the grid
+// actually changed, so repeated fits don't spam the PTY.
+
+import { useEffect, useRef } from "react";
+import { Terminal } from "@xterm/xterm";
+import { CanvasAddon } from "@xterm/addon-canvas";
+import { FitAddon } from "@xterm/addon-fit";
+import { SearchAddon } from "@xterm/addon-search";
+import { Unicode11Addon } from "@xterm/addon-unicode11";
+import { WebLinksAddon } from "@xterm/addon-web-links";
+import { WebglAddon } from "@xterm/addon-webgl";
+import type { AttachableTerminal } from "../hooks/useTerminalSession";
+import { buildTerminalThemes } from "../lib/terminal-themes";
+import type { Theme } from "../stores/ui-store";
+
+export type XtermTerminalProps = {
+ ariaLabel?: string;
+ className?: string;
+ theme: Theme;
+ /** Terminal construction failed; the owner decides how to surface it. */
+ onError?: (error: unknown) => void;
+ /**
+ * The terminal is open in the DOM and ready to be attached to a PTY. The
+ * handle stays valid until unmount; cols/rows are live getters.
+ */
+ onReady?: (terminal: AttachableTerminal) => void;
+};
+
+// Prefer the WebGL renderer, fall back to 2D canvas. Both rasterize box-drawing
+// glyphs themselves onto a fixed cell grid; the DOM renderer does not, so TUI
+// borders would drift. Loaded after open().
+function loadRenderer(term: Terminal): void {
+ try {
+ const webgl = new WebglAddon();
+ webgl.onContextLoss(() => webgl.dispose());
+ term.loadAddon(webgl);
+ return;
+ } catch {
+ // WebGL context unavailable — fall through to the canvas renderer.
+ }
+ try {
+ term.loadAddon(new CanvasAddon());
+ } catch (error) {
+ console.warn("xterm: WebGL and canvas renderers unavailable; box-drawing may drift", error);
+ }
+}
+
+// xterm palette tracks the app theme (see lib/terminal-themes.ts + --term-* in
+// styles.css). The PTY content is still the agent's own ANSI output.
+const terminalThemes = buildTerminalThemes();
+
+// Erase scrollback (3J) + display (2J) and home the cursor — yyork's
+// terminalResetSequence. Deliberately NOT term.reset(): a full RIS also wipes
+// the DEC private modes zellij enabled when its attach process started (SGR
+// mouse tracking, alt screen), and the mux's 50KB ring replay no longer
+// contains those init sequences — after a RIS, xterm never re-enters
+// mouse-tracking mode, wheel events stop being forwarded to zellij, and
+// scrolling goes dead.
+const CLEAR_SEQUENCE = "\x1b[3J\x1b[2J\x1b[H";
+
+export function XtermTerminal(props: XtermTerminalProps) {
+ const hostRef = useRef
(null);
+ const termRef = useRef(null);
+ // Latest callbacks in a ref so the mount effect stays dependency-free — we
+ // never tear down and recreate the terminal because a handler identity
+ // changed between renders.
+ const callbacksRef = useRef(props);
+
+ useEffect(() => {
+ callbacksRef.current = props;
+ });
+
+ useEffect(() => {
+ const term = termRef.current;
+ if (!term) return;
+ term.options.theme = props.theme === "dark" ? terminalThemes.dark : terminalThemes.light;
+ }, [props.theme]);
+
+ useEffect(() => {
+ const host = hostRef.current;
+ if (!host) return undefined;
+
+ let term: Terminal;
+ try {
+ term = new Terminal({
+ // Required for the Unicode 11 width addon below.
+ allowProposedApi: true,
+ cursorBlink: true,
+ // Resolve the Nerd Font stack from --font-mono (styles.css) at
+ // construction so terminal glyphs follow the app's font tokens. The
+ // box-drawing grid is rasterized by the WebGL/canvas renderer itself,
+ // but powerline separators and file-type icons are real PUA codepoints
+ // that must come from a system-installed Nerd Font.
+ fontFamily:
+ getComputedStyle(host).getPropertyValue("--font-mono").trim() ||
+ 'ui-monospace, Menlo, Monaco, "Courier New", monospace',
+ fontSize: 13,
+ lineHeight: 1.35,
+ // Agent TUIs leave SGR bold active while using ANSI black for
+ // separators; keep bold weight-only so black stays black.
+ drawBoldTextInBrightColors: false,
+ // Auto-adjust glyph colors that don't clear WCAG AA against their cell
+ // background, the way VS Code's terminal does; without it dim colors
+ // render washed out.
+ minimumContrastRatio: 4.5,
+ // The mux PTY runs `zellij attach` (backend AttachCommand), a
+ // full-screen alt-buffer app that owns scrollback itself — same as
+ // yyork. xterm's own buffer never accumulates history (the alt screen
+ // doesn't feed scrollback), and wheel events reach zellij as mouse
+ // reports instead of scrolling locally. 0 also stops FitAddon
+ // reserving ~14px on the right for a scrollbar that can never appear.
+ scrollback: 0,
+ theme: props.theme === "dark" ? terminalThemes.dark : terminalThemes.light,
+ });
+ } catch (error) {
+ callbacksRef.current.onError?.(error);
+ return undefined;
+ }
+
+ termRef.current = term;
+
+ const fit = new FitAddon();
+ term.loadAddon(fit);
+ const unicode = new Unicode11Addon();
+ term.loadAddon(unicode);
+ term.unicode.activeVersion = "11";
+ term.loadAddon(new WebLinksAddon());
+ term.loadAddon(new SearchAddon());
+
+ term.open(host);
+ loadRenderer(term);
+
+ const fitTerminal = () => {
+ try {
+ fit.fit();
+ } catch {
+ // Container momentarily has no size (hidden/unmounting) — a later
+ // trigger retries.
+ }
+ };
+
+ const raf = requestAnimationFrame(fitTerminal);
+ const settleTimers = [window.setTimeout(fitTerminal, 50), window.setTimeout(fitTerminal, 250)];
+ if (document.fonts?.ready) {
+ void document.fonts.ready.then(fitTerminal);
+ }
+ const observer = new ResizeObserver(fitTerminal);
+ observer.observe(host);
+
+ // Live cols/rows getters: the owner reads the current grid at attach time,
+ // not a snapshot taken at ready time (the first fit may not have run yet).
+ const handle: AttachableTerminal = {
+ get cols() {
+ return term.cols;
+ },
+ get rows() {
+ return term.rows;
+ },
+ write: (data) => term.write(data),
+ writeln: (line) => term.writeln(line),
+ clear: () => term.write(CLEAR_SEQUENCE),
+ onData: (listener) => term.onData(listener),
+ onResize: (listener) => term.onResize(listener),
+ };
+ callbacksRef.current.onReady?.(handle);
+
+ return () => {
+ termRef.current = null;
+ cancelAnimationFrame(raf);
+ for (const timer of settleTimers) window.clearTimeout(timer);
+ observer.disconnect();
+ try {
+ term.dispose();
+ } catch {
+ // Some renderer addons can throw during dispose in certain GPU
+ // environments; the terminal is being torn down regardless.
+ }
+ };
+ }, []);
+
+ return (
+
+ );
+}
diff --git a/frontend/src/renderer/components/ui/card.tsx b/frontend/src/renderer/components/ui/card.tsx
new file mode 100644
index 000000000..601fe5e6e
--- /dev/null
+++ b/frontend/src/renderer/components/ui/card.tsx
@@ -0,0 +1,56 @@
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+function Card({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return
;
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+ return
;
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+ return
;
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };
diff --git a/frontend/src/renderer/components/ui/dropdown-menu.tsx b/frontend/src/renderer/components/ui/dropdown-menu.tsx
new file mode 100644
index 000000000..c2a0f2b7c
--- /dev/null
+++ b/frontend/src/renderer/components/ui/dropdown-menu.tsx
@@ -0,0 +1,74 @@
+import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
+import { cn } from "../../lib/utils";
+
+export const DropdownMenu = DropdownMenuPrimitive.Root;
+export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
+export const DropdownMenuGroup = DropdownMenuPrimitive.Group;
+export const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
+
+export function DropdownMenuContent({
+ className,
+ sideOffset = 6,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ );
+}
+
+export function DropdownMenuItem({
+ className,
+ inset,
+ ...props
+}: React.ComponentProps & { inset?: boolean }) {
+ return (
+
+ );
+}
+
+export function DropdownMenuLabel({
+ className,
+ inset,
+ ...props
+}: React.ComponentProps & { inset?: boolean }) {
+ return (
+
+ );
+}
+
+export function DropdownMenuSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+export function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
+ return ;
+}
diff --git a/frontend/src/renderer/components/ui/label.tsx b/frontend/src/renderer/components/ui/label.tsx
new file mode 100644
index 000000000..b673e3d13
--- /dev/null
+++ b/frontend/src/renderer/components/ui/label.tsx
@@ -0,0 +1,21 @@
+"use client";
+
+import * as React from "react";
+import { Label as LabelPrimitive } from "radix-ui";
+
+import { cn } from "@/lib/utils";
+
+function Label({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export { Label };
diff --git a/frontend/src/renderer/components/ui/resizable.tsx b/frontend/src/renderer/components/ui/resizable.tsx
new file mode 100644
index 000000000..68c2a2b86
--- /dev/null
+++ b/frontend/src/renderer/components/ui/resizable.tsx
@@ -0,0 +1,45 @@
+import { GripVerticalIcon } from "lucide-react";
+import * as ResizablePrimitive from "react-resizable-panels";
+
+import { cn } from "@/lib/utils";
+
+function ResizablePanelGroup({ className, ...props }: ResizablePrimitive.GroupProps) {
+ return (
+
+ );
+}
+
+function ResizablePanel({ ...props }: ResizablePrimitive.PanelProps) {
+ return ;
+}
+
+function ResizableHandle({
+ withHandle,
+ className,
+ ...props
+}: ResizablePrimitive.SeparatorProps & {
+ withHandle?: boolean;
+}) {
+ return (
+ div]:rotate-90",
+ className,
+ )}
+ {...props}
+ >
+ {withHandle && (
+
+
+
+ )}
+
+ );
+}
+
+export { ResizableHandle, ResizablePanel, ResizablePanelGroup };
diff --git a/frontend/src/renderer/components/ui/select.tsx b/frontend/src/renderer/components/ui/select.tsx
new file mode 100644
index 000000000..88ede14f4
--- /dev/null
+++ b/frontend/src/renderer/components/ui/select.tsx
@@ -0,0 +1,160 @@
+import * as React from "react";
+import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
+import { Select as SelectPrimitive } from "radix-ui";
+
+import { cn } from "@/lib/utils";
+
+function Select({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function SelectGroup({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function SelectValue({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function SelectTrigger({
+ className,
+ size = "default",
+ children,
+ ...props
+}: React.ComponentProps & {
+ size?: "sm" | "default";
+}) {
+ return (
+
+ {children}
+
+
+
+
+ );
+}
+
+function SelectContent({
+ className,
+ children,
+ position = "item-aligned",
+ align = "center",
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
+
+function SelectLabel({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function SelectItem({ className, children, ...props }: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ );
+}
+
+function SelectSeparator({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function SelectScrollUpButton({ className, ...props }: React.ComponentProps) {
+ return (
+
+
+
+ );
+}
+
+function SelectScrollDownButton({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ );
+}
+
+export {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectLabel,
+ SelectScrollDownButton,
+ SelectScrollUpButton,
+ SelectSeparator,
+ SelectTrigger,
+ SelectValue,
+};
diff --git a/frontend/src/renderer/components/ui/separator.tsx b/frontend/src/renderer/components/ui/separator.tsx
new file mode 100644
index 000000000..8de07c1de
--- /dev/null
+++ b/frontend/src/renderer/components/ui/separator.tsx
@@ -0,0 +1,28 @@
+"use client";
+
+import * as React from "react";
+import { Separator as SeparatorPrimitive } from "radix-ui";
+
+import { cn } from "@/lib/utils";
+
+function Separator({
+ className,
+ orientation = "horizontal",
+ decorative = true,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export { Separator };
diff --git a/frontend/src/renderer/components/ui/sheet.tsx b/frontend/src/renderer/components/ui/sheet.tsx
new file mode 100644
index 000000000..133c0c3f6
--- /dev/null
+++ b/frontend/src/renderer/components/ui/sheet.tsx
@@ -0,0 +1,107 @@
+"use client";
+
+import * as React from "react";
+import { XIcon } from "lucide-react";
+import { Dialog as SheetPrimitive } from "radix-ui";
+
+import { cn } from "@/lib/utils";
+
+function Sheet({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function SheetTrigger({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function SheetClose({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function SheetPortal({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function SheetOverlay({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function SheetContent({
+ className,
+ children,
+ side = "right",
+ showCloseButton = true,
+ ...props
+}: React.ComponentProps & {
+ side?: "top" | "right" | "bottom" | "left";
+ showCloseButton?: boolean;
+}) {
+ return (
+
+
+
+ {children}
+ {showCloseButton && (
+
+
+ Close
+
+ )}
+
+
+ );
+}
+
+function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return
;
+}
+
+function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return
;
+}
+
+function SheetTitle({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function SheetDescription({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export { Sheet, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription };
diff --git a/frontend/src/renderer/components/ui/sidebar.tsx b/frontend/src/renderer/components/ui/sidebar.tsx
new file mode 100644
index 000000000..b1d0676ab
--- /dev/null
+++ b/frontend/src/renderer/components/ui/sidebar.tsx
@@ -0,0 +1,677 @@
+"use client";
+
+import * as React from "react";
+import { cva, type VariantProps } from "class-variance-authority";
+import { PanelLeftIcon } from "lucide-react";
+import { Slot } from "radix-ui";
+
+import { useIsMobile } from "@/hooks/use-mobile";
+import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Separator } from "@/components/ui/separator";
+import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet";
+import { Skeleton } from "@/components/ui/skeleton";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
+
+const SIDEBAR_COOKIE_NAME = "sidebar_state";
+const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
+const SIDEBAR_WIDTH = "16rem";
+const SIDEBAR_WIDTH_MOBILE = "18rem";
+const SIDEBAR_WIDTH_ICON = "3rem";
+const SIDEBAR_KEYBOARD_SHORTCUT = "b";
+
+type SidebarContextProps = {
+ state: "expanded" | "collapsed";
+ open: boolean;
+ setOpen: (open: boolean) => void;
+ openMobile: boolean;
+ setOpenMobile: (open: boolean) => void;
+ isMobile: boolean;
+ toggleSidebar: () => void;
+};
+
+const SidebarContext = React.createContext(null);
+
+function useSidebar() {
+ const context = React.useContext(SidebarContext);
+ if (!context) {
+ throw new Error("useSidebar must be used within a SidebarProvider.");
+ }
+
+ return context;
+}
+
+function SidebarProvider({
+ defaultOpen = true,
+ open: openProp,
+ onOpenChange: setOpenProp,
+ className,
+ style,
+ children,
+ ...props
+}: React.ComponentProps<"div"> & {
+ defaultOpen?: boolean;
+ open?: boolean;
+ onOpenChange?: (open: boolean) => void;
+}) {
+ const isMobile = useIsMobile();
+ const [openMobile, setOpenMobile] = React.useState(false);
+
+ // This is the internal state of the sidebar.
+ // We use openProp and setOpenProp for control from outside the component.
+ const [_open, _setOpen] = React.useState(defaultOpen);
+ const open = openProp ?? _open;
+ const setOpen = React.useCallback(
+ (value: boolean | ((value: boolean) => boolean)) => {
+ const openState = typeof value === "function" ? value(open) : value;
+ if (setOpenProp) {
+ setOpenProp(openState);
+ } else {
+ _setOpen(openState);
+ }
+
+ // This sets the cookie to keep the sidebar state.
+ document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
+ },
+ [setOpenProp, open],
+ );
+
+ // Helper to toggle the sidebar.
+ const toggleSidebar = React.useCallback(() => {
+ return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
+ }, [isMobile, setOpen, setOpenMobile]);
+
+ // Adds a keyboard shortcut to toggle the sidebar.
+ React.useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
+ event.preventDefault();
+ toggleSidebar();
+ }
+ };
+
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, [toggleSidebar]);
+
+ // We add a state so that we can do data-state="expanded" or "collapsed".
+ // This makes it easier to style the sidebar with Tailwind classes.
+ const state = open ? "expanded" : "collapsed";
+
+ const contextValue = React.useMemo(
+ () => ({
+ state,
+ open,
+ setOpen,
+ isMobile,
+ openMobile,
+ setOpenMobile,
+ toggleSidebar,
+ }),
+ [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
+ );
+
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
+
+function Sidebar({
+ side = "left",
+ variant = "sidebar",
+ collapsible = "offcanvas",
+ className,
+ children,
+ ...props
+}: React.ComponentProps<"div"> & {
+ side?: "left" | "right";
+ variant?: "sidebar" | "floating" | "inset";
+ collapsible?: "offcanvas" | "icon" | "none";
+}) {
+ const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
+
+ if (collapsible === "none") {
+ return (
+
+ {children}
+
+ );
+ }
+
+ if (isMobile) {
+ return (
+
+
+
+ Sidebar
+ Displays the mobile sidebar.
+
+ {children}
+
+
+ );
+ }
+
+ return (
+
+ {/* This is what handles the sidebar gap on desktop */}
+
+
+
+ );
+}
+
+function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps) {
+ const { toggleSidebar } = useSidebar();
+
+ return (
+ {
+ onClick?.(event);
+ toggleSidebar();
+ }}
+ {...props}
+ >
+
+ Toggle Sidebar
+
+ );
+}
+
+function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
+ const { toggleSidebar } = useSidebar();
+
+ return (
+
+ );
+}
+
+function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
+ return (
+
+ );
+}
+
+function SidebarInput({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function SidebarSeparator({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function SidebarGroupLabel({
+ className,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"div"> & { asChild?: boolean }) {
+ const Comp = asChild ? Slot.Root : "div";
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
+ className,
+ )}
+ {...props}
+ />
+ );
+}
+
+function SidebarGroupAction({
+ className,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> & { asChild?: boolean }) {
+ const Comp = asChild ? Slot.Root : "button";
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ // Increases the hit area of the button on mobile.
+ "after:absolute after:-inset-2 md:after:hidden",
+ "group-data-[collapsible=icon]:hidden",
+ className,
+ )}
+ {...props}
+ />
+ );
+}
+
+function SidebarGroupContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
+ return (
+
+ );
+}
+
+function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
+ return (
+
+ );
+}
+
+const sidebarMenuButtonVariants = cva(
+ "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
+ outline:
+ "bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_var(--sidebar-accent)]",
+ },
+ size: {
+ default: "h-8 text-sm",
+ sm: "h-7 text-xs",
+ lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+);
+
+function SidebarMenuButton({
+ asChild = false,
+ isActive = false,
+ variant = "default",
+ size = "default",
+ tooltip,
+ className,
+ ...props
+}: React.ComponentProps<"button"> & {
+ asChild?: boolean;
+ isActive?: boolean;
+ tooltip?: string | React.ComponentProps;
+} & VariantProps) {
+ const Comp = asChild ? Slot.Root : "button";
+ const { isMobile, state } = useSidebar();
+
+ const button = (
+
+ );
+
+ if (!tooltip) {
+ return button;
+ }
+
+ if (typeof tooltip === "string") {
+ tooltip = {
+ children: tooltip,
+ };
+ }
+
+ return (
+
+ {button}
+
+
+ );
+}
+
+function SidebarMenuAction({
+ className,
+ asChild = false,
+ showOnHover = false,
+ ...props
+}: React.ComponentProps<"button"> & {
+ asChild?: boolean;
+ showOnHover?: boolean;
+}) {
+ const Comp = asChild ? Slot.Root : "button";
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ // Increases the hit area of the button on mobile.
+ "after:absolute after:-inset-2 md:after:hidden",
+ "peer-data-[size=sm]/menu-button:top-1",
+ "peer-data-[size=default]/menu-button:top-1.5",
+ "peer-data-[size=lg]/menu-button:top-2.5",
+ "group-data-[collapsible=icon]:hidden",
+ showOnHover &&
+ "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground data-[state=open]:opacity-100 md:opacity-0",
+ className,
+ )}
+ {...props}
+ />
+ );
+}
+
+function SidebarMenuBadge({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function SidebarMenuSkeleton({
+ className,
+ showIcon = false,
+ ...props
+}: React.ComponentProps<"div"> & {
+ showIcon?: boolean;
+}) {
+ // Random width between 50 to 90%.
+ const width = React.useMemo(() => {
+ return `${Math.floor(Math.random() * 40) + 50}%`;
+ }, []);
+
+ return (
+
+ {showIcon && }
+
+
+ );
+}
+
+function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
+ return (
+
+ );
+}
+
+function SidebarMenuSubItem({ className, ...props }: React.ComponentProps<"li">) {
+ return (
+
+ );
+}
+
+function SidebarMenuSubButton({
+ asChild = false,
+ size = "md",
+ isActive = false,
+ className,
+ ...props
+}: React.ComponentProps<"a"> & {
+ asChild?: boolean;
+ size?: "sm" | "md";
+ isActive?: boolean;
+}) {
+ const Comp = asChild ? Slot.Root : "a";
+
+ return (
+ span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
+ "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
+ size === "sm" && "text-xs",
+ size === "md" && "text-sm",
+ "group-data-[collapsible=icon]:hidden",
+ className,
+ )}
+ {...props}
+ />
+ );
+}
+
+export {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarGroup,
+ SidebarGroupAction,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarInput,
+ SidebarInset,
+ SidebarMenu,
+ SidebarMenuAction,
+ SidebarMenuBadge,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarMenuSkeleton,
+ SidebarMenuSub,
+ SidebarMenuSubButton,
+ SidebarMenuSubItem,
+ SidebarProvider,
+ SidebarRail,
+ SidebarSeparator,
+ SidebarTrigger,
+ useSidebar,
+};
diff --git a/frontend/src/renderer/components/ui/skeleton.tsx b/frontend/src/renderer/components/ui/skeleton.tsx
new file mode 100644
index 000000000..126f08790
--- /dev/null
+++ b/frontend/src/renderer/components/ui/skeleton.tsx
@@ -0,0 +1,7 @@
+import { cn } from "@/lib/utils";
+
+function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
+ return
;
+}
+
+export { Skeleton };
diff --git a/frontend/src/renderer/components/ui/table.tsx b/frontend/src/renderer/components/ui/table.tsx
new file mode 100644
index 000000000..8f53d126e
--- /dev/null
+++ b/frontend/src/renderer/components/ui/table.tsx
@@ -0,0 +1,76 @@
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+function Table({ className, ...props }: React.ComponentProps<"table">) {
+ return (
+
+ );
+}
+
+function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
+ return ;
+}
+
+function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
+ return ;
+}
+
+function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
+ return (
+ tr]:last:border-b-0", className)}
+ {...props}
+ />
+ );
+}
+
+function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
+ return (
+
+ );
+}
+
+function TableHead({ className, ...props }: React.ComponentProps<"th">) {
+ return (
+ [role=checkbox]]:translate-y-[2px]",
+ className,
+ )}
+ {...props}
+ />
+ );
+}
+
+function TableCell({ className, ...props }: React.ComponentProps<"td">) {
+ return (
+ [role=checkbox]]:translate-y-[2px]",
+ className,
+ )}
+ {...props}
+ />
+ );
+}
+
+function TableCaption({ className, ...props }: React.ComponentProps<"caption">) {
+ return (
+
+ );
+}
+
+export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
diff --git a/frontend/src/renderer/hooks/use-mobile.tsx b/frontend/src/renderer/hooks/use-mobile.tsx
new file mode 100644
index 000000000..21e5c1f0d
--- /dev/null
+++ b/frontend/src/renderer/hooks/use-mobile.tsx
@@ -0,0 +1,19 @@
+import * as React from "react";
+
+const MOBILE_BREAKPOINT = 768;
+
+export function useIsMobile() {
+ const [isMobile, setIsMobile] = React.useState(undefined);
+
+ React.useEffect(() => {
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
+ const onChange = () => {
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
+ };
+ mql.addEventListener("change", onChange);
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
+ return () => mql.removeEventListener("change", onChange);
+ }, []);
+
+ return !!isMobile;
+}
diff --git a/frontend/src/renderer/hooks/useResizable.ts b/frontend/src/renderer/hooks/useResizable.ts
new file mode 100644
index 000000000..4f66eb680
--- /dev/null
+++ b/frontend/src/renderer/hooks/useResizable.ts
@@ -0,0 +1,76 @@
+import { useCallback, useEffect, useRef } from "react";
+
+interface UseResizableOptions {
+ /** CSS custom property to drive (set on :root), e.g. "--ao-sidebar-w". */
+ cssVar: string;
+ /** localStorage key to persist the width. */
+ storageKey: string;
+ defaultWidth: number;
+ min: number;
+ max: number;
+ /**
+ * Which edge the drag handle sits on relative to the panel it resizes.
+ * "right" (sidebar handle) grows with rightward drag; "left" (inspector
+ * handle) grows with leftward drag.
+ */
+ edge: "left" | "right";
+}
+
+/**
+ * Pointer-driven panel resize, cloned from agent-orchestrator's useResizable.
+ * Persists the width to localStorage and applies it via a CSS custom property
+ * on :root (so the consuming layout reads it with `width: var(--cssVar, default)`),
+ * avoiding any inline `style=`.
+ */
+export function useResizable({ cssVar, storageKey, defaultWidth, min, max, edge }: UseResizableOptions) {
+ const widthRef = useRef(defaultWidth);
+
+ const apply = useCallback(
+ (next: number) => {
+ const clamped = Math.min(max, Math.max(min, next));
+ widthRef.current = clamped;
+ document.documentElement.style.setProperty(cssVar, `${clamped}px`);
+ },
+ [cssVar, max, min],
+ );
+
+ // Restore persisted width on mount.
+ useEffect(() => {
+ const saved = Number(window.localStorage.getItem(storageKey));
+ apply(Number.isFinite(saved) && saved > 0 ? saved : defaultWidth);
+ return () => {
+ document.documentElement.style.removeProperty(cssVar);
+ };
+ }, [apply, cssVar, defaultWidth, storageKey]);
+
+ const onPointerDown = useCallback(
+ (event: React.PointerEvent) => {
+ event.preventDefault();
+ const startX = event.clientX;
+ const startWidth = widthRef.current;
+ const sign = edge === "right" ? 1 : -1;
+ document.body.classList.add("is-resizing-x");
+
+ const onMove = (e: PointerEvent) => {
+ apply(startWidth + sign * (e.clientX - startX));
+ };
+ const onUp = () => {
+ window.removeEventListener("pointermove", onMove);
+ window.removeEventListener("pointerup", onUp);
+ document.body.classList.remove("is-resizing-x");
+ window.localStorage.setItem(storageKey, String(widthRef.current));
+ };
+ window.addEventListener("pointermove", onMove);
+ window.addEventListener("pointerup", onUp);
+ },
+ [apply, edge, storageKey],
+ );
+
+ /** Double-click the handle to reset to the default width. */
+ const onDoubleClick = useCallback(() => {
+ apply(defaultWidth);
+ window.localStorage.setItem(storageKey, String(defaultWidth));
+ }, [apply, defaultWidth, storageKey]);
+
+ return { onPointerDown, onDoubleClick };
+}
diff --git a/frontend/src/renderer/hooks/useTerminalSession.test.tsx b/frontend/src/renderer/hooks/useTerminalSession.test.tsx
index af97483a1..882ad7e52 100644
--- a/frontend/src/renderer/hooks/useTerminalSession.test.tsx
+++ b/frontend/src/renderer/hooks/useTerminalSession.test.tsx
@@ -167,9 +167,20 @@ describe("useTerminalSession", () => {
terminal.typeKeys("ls\r");
expect(muxes[0].inputs).toEqual([["handle-1", "ls\r"]]);
terminal.emitResize(120, 40);
+ act(() => void vi.advanceTimersByTime(100));
expect(muxes[0].resizes).toContainEqual(["handle-1", 120, 40]);
});
+ it("collapses a drag's burst of grid changes into one trailing PTY resize", () => {
+ const { terminal, muxes } = setup();
+ const initialResizes = muxes[0].resizes.length; // connect() sends the opening size
+ terminal.emitResize(100, 30);
+ terminal.emitResize(110, 34);
+ terminal.emitResize(120, 40);
+ act(() => void vi.advanceTimersByTime(100));
+ expect(muxes[0].resizes.slice(initialResizes)).toEqual([["handle-1", 120, 40]]);
+ });
+
it("marks exit in the terminal and refetches workspace state instead of writing status", () => {
const { view, terminal, muxes, invalidateSpy } = setup();
act(() => muxes[0].emitExit("handle-1"));
diff --git a/frontend/src/renderer/hooks/useTerminalSession.ts b/frontend/src/renderer/hooks/useTerminalSession.ts
index dcfb8eee4..4d2a240c7 100644
--- a/frontend/src/renderer/hooks/useTerminalSession.ts
+++ b/frontend/src/renderer/hooks/useTerminalSession.ts
@@ -24,8 +24,10 @@ export type AttachableTerminal = {
write: (data: Uint8Array) => void;
writeln: (line: string) => void;
/**
- * Erase screen + scrollback while preserving terminal modes. A full reset
- * drops zellij mouse tracking, so wheel scroll stops after reconnect.
+ * Erase screen + scrollback and home the cursor, preserving terminal modes.
+ * Never a full reset (RIS): that would drop the mouse-tracking mode zellij
+ * enabled at attach, which the ring replay cannot re-establish — killing
+ * wheel scroll (see XtermTerminal's CLEAR_SEQUENCE).
*/
clear: () => void;
onData: (listener: (data: string) => void) => { dispose: () => void };
@@ -49,6 +51,10 @@ export type UseTerminalSessionOptions = {
const RETRY_BASE_MS = 500;
const RETRY_MAX_MS = 8_000;
+// Trailing debounce on grid changes: a pane drag emits a burst of intermediate
+// sizes; the attached program should get one SIGWINCH when the drag settles,
+// not dozens (yyork's terminal-panel does the same at its socket layer).
+const RESIZE_DEBOUNCE_MS = 100;
function defaultCreateMux(): TerminalMux {
// Resolved per connect, not per hook: a daemon restart can change the port.
@@ -73,6 +79,7 @@ export function useTerminalSession(session: WorkspaceSession | undefined, option
handle: null as string | null,
disposers: [] as Array<() => void>,
retryTimer: null as ReturnType | null,
+ resizeTimer: null as ReturnType | null,
attempts: 0,
firstAttach: true,
detached: true,
@@ -93,6 +100,10 @@ export function useTerminalSession(session: WorkspaceSession | undefined, option
clearTimeout(r.retryTimer);
r.retryTimer = null;
}
+ if (r.resizeTimer) {
+ clearTimeout(r.resizeTimer);
+ r.resizeTimer = null;
+ }
r.disposers.forEach((dispose) => dispose());
r.disposers = [];
r.mux?.dispose();
@@ -148,18 +159,26 @@ export function useTerminalSession(session: WorkspaceSession | undefined, option
}),
);
const input = terminal.onData((data) => mux.sendInput(handle, data));
- const resize = terminal.onResize(({ cols, rows }) => mux.resize(handle, cols, rows));
+ // xterm only fires onResize when the grid actually changed; the debounce
+ // additionally collapses a drag's burst of changes into one PTY resize.
+ const resize = terminal.onResize(({ cols, rows }) => {
+ if (r.resizeTimer) clearTimeout(r.resizeTimer);
+ r.resizeTimer = setTimeout(() => {
+ r.resizeTimer = null;
+ mux.resize(handle, cols, rows);
+ }, RESIZE_DEBOUNCE_MS);
+ });
r.disposers.push(
() => input.dispose(),
() => resize.dispose(),
);
- if (r.firstAttach) {
- terminal.writeln(`\x1b[2mAttaching to ${sessionRef.current?.title ?? handle}…\x1b[0m`);
- } else {
- // The server replays the recent-output ring on open (backend
- // internal/terminal/ring.go); clear the stale screen so it isn't doubled.
- // Screen-clear only, never reset(): RIS wipes zellij's mouse mode.
+ // Connection status is chrome (the pane's banner), never buffer content —
+ // the PTY owns the buffer. On reattach the server replays the recent-output
+ // ring (backend internal/terminal/ring.go); clear the stale screen so it
+ // isn't doubled. Screen-clear only, never reset(): RIS would wipe zellij's
+ // mouse-tracking mode and freeze wheel scrolling after every reconnect.
+ if (!r.firstAttach) {
terminal.clear();
}
r.firstAttach = false;
diff --git a/frontend/src/renderer/hooks/useWorkspaceQuery.ts b/frontend/src/renderer/hooks/useWorkspaceQuery.ts
index 18c00974f..12b432f8c 100644
--- a/frontend/src/renderer/hooks/useWorkspaceQuery.ts
+++ b/frontend/src/renderer/hooks/useWorkspaceQuery.ts
@@ -29,18 +29,24 @@ async function fetchWorkspaces(): Promise {
workspaceName: project.name,
title: session.displayName ?? session.issueId ?? session.id,
provider: toAgentProvider(session.harness),
- branch: "",
+ kind: session.kind === "orchestrator" ? "orchestrator" : session.kind === "worker" ? "worker" : undefined,
+ branch: `session/${session.id}`,
status: toSessionStatus(session.status, session.isTerminated),
- updatedAt: new Date(session.updatedAt).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }),
+ createdAt: session.createdAt,
+ updatedAt: session.updatedAt,
})),
}));
}
+// Shared so route loaders can prefetch via queryClient.ensureQueryData (paired
+// with the router's defaultPreload: "intent") and the hook reads the same cache.
+export const workspaceQueryOptions = {
+ queryKey: workspaceQueryKey,
+ queryFn: fetchWorkspaces,
+ retry: 1,
+ refetchInterval: 15_000,
+};
+
export function useWorkspaceQuery() {
- return useQuery({
- queryKey: workspaceQueryKey,
- queryFn: fetchWorkspaces,
- retry: 1,
- refetchInterval: 15_000,
- });
+ return useQuery(workspaceQueryOptions);
}
diff --git a/frontend/src/renderer/lib/api-client.ts b/frontend/src/renderer/lib/api-client.ts
index c1aff35e3..c2bd44d16 100644
--- a/frontend/src/renderer/lib/api-client.ts
+++ b/frontend/src/renderer/lib/api-client.ts
@@ -72,3 +72,20 @@ export const apiClient = createClient({
baseUrl: initialApiBaseUrl,
fetch: runtimeFetch,
});
+
+/**
+ * Human-readable message from an openapi-fetch `error` value. The daemon's
+ * error body is `{ error, code, message, requestId }` (backend apierr) — a
+ * plain object, so `String(error)` renders "[object Object]". Falls back
+ * through Error instances and strings.
+ */
+export function apiErrorMessage(error: unknown, fallback = "Request failed"): string {
+ if (error instanceof Error) return error.message;
+ if (typeof error === "string" && error !== "") return error;
+ if (typeof error === "object" && error !== null) {
+ const body = error as { message?: unknown; error?: unknown };
+ if (typeof body.message === "string" && body.message !== "") return body.message;
+ if (typeof body.error === "string" && body.error !== "") return body.error;
+ }
+ return fallback;
+}
diff --git a/frontend/src/renderer/lib/format-time.ts b/frontend/src/renderer/lib/format-time.ts
new file mode 100644
index 000000000..445e0b0cd
--- /dev/null
+++ b/frontend/src/renderer/lib/format-time.ts
@@ -0,0 +1,14 @@
+/** Compact relative time — ported from agent-orchestrator session-detail-utils. */
+export function formatTimeCompact(isoDate: string | null | undefined): string {
+ if (!isoDate) return "just now";
+ const ts = new Date(isoDate).getTime();
+ if (!Number.isFinite(ts)) return "just now";
+ const diffMs = Date.now() - ts;
+ if (diffMs <= 0) return "just now";
+ const diffMins = Math.floor(diffMs / 60000);
+ const diffHours = Math.floor(diffMins / 60);
+ if (diffMins < 1) return "just now";
+ if (diffMins < 60) return `${diffMins}m ago`;
+ if (diffHours < 24) return `${diffHours}h ago`;
+ return `${Math.floor(diffHours / 24)}d ago`;
+}
diff --git a/frontend/src/renderer/lib/mock-data.ts b/frontend/src/renderer/lib/mock-data.ts
index 71e8ac117..d838046b0 100644
--- a/frontend/src/renderer/lib/mock-data.ts
+++ b/frontend/src/renderer/lib/mock-data.ts
@@ -15,7 +15,8 @@ export const mockWorkspaces: WorkspaceSummary[] = [
provider: "claude-code",
branch: "feat/refactor-mux",
status: "working",
- updatedAt: "now",
+ updatedAt: new Date().toISOString(),
+ createdAt: new Date(Date.now() - 4 * 60 * 60 * 1000).toISOString(),
changedFiles: [
{
path: "internal/mux/terminal_mux.go",
@@ -40,7 +41,8 @@ export const mockWorkspaces: WorkspaceSummary[] = [
provider: "codex",
branch: "fix/webgl-fallback",
status: "needs_input",
- updatedAt: "now",
+ updatedAt: new Date().toISOString(),
+ createdAt: new Date(Date.now() - 4 * 60 * 60 * 1000).toISOString(),
},
],
},
diff --git a/frontend/src/renderer/lib/shell-context.ts b/frontend/src/renderer/lib/shell-context.ts
new file mode 100644
index 000000000..a1f9cb323
--- /dev/null
+++ b/frontend/src/renderer/lib/shell-context.ts
@@ -0,0 +1,24 @@
+import { createContext, useContext } from "react";
+import type { useDaemonStatus } from "../hooks/useDaemonStatus";
+import type { AgentProvider } from "../types/workspace";
+
+// Shared state the persistent _shell layout owns and route content reads. The
+// daemon status effect (IPC poll + event transport) must run exactly once, so
+// it lives in the shell and is handed down here rather than re-run per route.
+export type ShellContextValue = {
+ daemonStatus: ReturnType;
+ /** Open the spawn-worker modal, optionally pre-selecting a project. */
+ openSpawn: (projectId?: string) => void;
+ createProject: (input: { path: string }) => Promise;
+ createTask: (input: { projectId: string; prompt: string; branch?: string; harness?: AgentProvider }) => Promise;
+};
+
+const ShellContext = createContext(null);
+
+export const ShellProvider = ShellContext.Provider;
+
+export function useShell(): ShellContextValue {
+ const ctx = useContext(ShellContext);
+ if (!ctx) throw new Error("useShell must be used within the _shell layout route");
+ return ctx;
+}
diff --git a/frontend/src/renderer/lib/spawn-orchestrator.ts b/frontend/src/renderer/lib/spawn-orchestrator.ts
new file mode 100644
index 000000000..15d73b863
--- /dev/null
+++ b/frontend/src/renderer/lib/spawn-orchestrator.ts
@@ -0,0 +1,18 @@
+import { apiClient } from "./api-client";
+
+/** Spawn the project's orchestrator session via the daemon API. */
+export async function spawnOrchestrator(projectId: string): Promise {
+ const { data, error, response } = await apiClient.POST("/api/v1/orchestrators", {
+ body: { projectId },
+ });
+
+ if (error || !data?.orchestrator?.id) {
+ const message =
+ error && typeof error === "object" && "message" in error && typeof error.message === "string"
+ ? error.message
+ : `Failed to spawn orchestrator (${response.status})`;
+ throw new Error(message);
+ }
+
+ return data.orchestrator.id;
+}
diff --git a/frontend/src/renderer/lib/terminal-themes.ts b/frontend/src/renderer/lib/terminal-themes.ts
new file mode 100644
index 000000000..01ad4f916
--- /dev/null
+++ b/frontend/src/renderer/lib/terminal-themes.ts
@@ -0,0 +1,62 @@
+import type { ITheme } from "@xterm/xterm";
+
+/** xterm palettes harmonized to styles.css tokens (agent-orchestrator pattern). */
+export function buildTerminalThemes(): { dark: ITheme; light: ITheme } {
+ const accent = {
+ cursor: "#f59f4c",
+ selDark: "rgba(77, 141, 255, 0.30)",
+ selLight: "rgba(37, 99, 235, 0.25)",
+ };
+
+ const dark: ITheme = {
+ background: "#15171b",
+ foreground: "#d7d7d2",
+ cursor: accent.cursor,
+ cursorAccent: "#15171b",
+ selectionBackground: accent.selDark,
+ selectionInactiveBackground: "rgba(128, 128, 128, 0.2)",
+ black: "#15171b",
+ red: "#ef6b6b",
+ green: "#74b98a",
+ yellow: "#e8c14a",
+ blue: "#4d8dff",
+ magenta: "#a78bfa",
+ cyan: "#6fb3c9",
+ white: "#d7d7d2",
+ brightBlack: "#7c7c7c",
+ brightRed: "#ff8a8a",
+ brightGreen: "#8fd6a6",
+ brightYellow: "#f0d06b",
+ brightBlue: "#7eaaff",
+ brightMagenta: "#c4b0fc",
+ brightCyan: "#8fcfe0",
+ brightWhite: "#f4f5f7",
+ };
+
+ const light: ITheme = {
+ background: "#fafafa",
+ foreground: "#24292f",
+ cursor: accent.cursor,
+ cursorAccent: "#fafafa",
+ selectionBackground: accent.selLight,
+ selectionInactiveBackground: "rgba(128, 128, 128, 0.15)",
+ black: "#24292f",
+ red: "#b42318",
+ green: "#1a7f37",
+ yellow: "#9a6b00",
+ blue: "#2563eb",
+ magenta: "#8e24aa",
+ cyan: "#0b7285",
+ white: "#4b5563",
+ brightBlack: "#374151",
+ brightRed: "#912018",
+ brightGreen: "#176639",
+ brightYellow: "#6f4a00",
+ brightBlue: "#1d4ed8",
+ brightMagenta: "#7b1fa2",
+ brightCyan: "#155e75",
+ brightWhite: "#374151",
+ };
+
+ return { dark, light };
+}
diff --git a/frontend/src/renderer/routeTree.gen.ts b/frontend/src/renderer/routeTree.gen.ts
index 20a1f6f6e..0ba366e13 100644
--- a/frontend/src/renderer/routeTree.gen.ts
+++ b/frontend/src/renderer/routeTree.gen.ts
@@ -9,85 +9,224 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from "./routes/__root";
-import { Route as IndexRouteImport } from "./routes/index";
-import { Route as WorkspacesWorkspaceIdRouteImport } from "./routes/workspaces.$workspaceId";
-import { Route as WorkspacesWorkspaceIdSessionsSessionIdRouteImport } from "./routes/workspaces.$workspaceId_.sessions.$sessionId";
+import { Route as ShellRouteImport } from "./routes/_shell";
+import { Route as ShellIndexRouteImport } from "./routes/_shell.index";
+import { Route as ShellReviewsRouteImport } from "./routes/_shell.reviews";
+import { Route as ShellReviewRouteImport } from "./routes/_shell.review";
+import { Route as ShellPrsRouteImport } from "./routes/_shell.prs";
+import { Route as ShellSessionsSessionIdRouteImport } from "./routes/_shell.sessions.$sessionId";
+import { Route as ShellProjectsProjectIdRouteImport } from "./routes/_shell.projects.$projectId";
+import { Route as ShellProjectsProjectIdSettingsRouteImport } from "./routes/_shell.projects.$projectId_.settings";
+import { Route as ShellProjectsProjectIdSessionsSessionIdRouteImport } from "./routes/_shell.projects.$projectId_.sessions.$sessionId";
-const IndexRoute = IndexRouteImport.update({
+const ShellRoute = ShellRouteImport.update({
+ id: "/_shell",
+ getParentRoute: () => rootRouteImport,
+} as any);
+const ShellIndexRoute = ShellIndexRouteImport.update({
id: "/",
path: "/",
- getParentRoute: () => rootRouteImport,
+ getParentRoute: () => ShellRoute,
} as any);
-const WorkspacesWorkspaceIdRoute = WorkspacesWorkspaceIdRouteImport.update({
- id: "/workspaces/$workspaceId",
- path: "/workspaces/$workspaceId",
- getParentRoute: () => rootRouteImport,
+const ShellReviewsRoute = ShellReviewsRouteImport.update({
+ id: "/reviews",
+ path: "/reviews",
+ getParentRoute: () => ShellRoute,
} as any);
-const WorkspacesWorkspaceIdSessionsSessionIdRoute = WorkspacesWorkspaceIdSessionsSessionIdRouteImport.update({
- id: "/workspaces/$workspaceId_/sessions/$sessionId",
- path: "/workspaces/$workspaceId/sessions/$sessionId",
- getParentRoute: () => rootRouteImport,
+const ShellReviewRoute = ShellReviewRouteImport.update({
+ id: "/review",
+ path: "/review",
+ getParentRoute: () => ShellRoute,
+} as any);
+const ShellPrsRoute = ShellPrsRouteImport.update({
+ id: "/prs",
+ path: "/prs",
+ getParentRoute: () => ShellRoute,
+} as any);
+const ShellSessionsSessionIdRoute = ShellSessionsSessionIdRouteImport.update({
+ id: "/sessions/$sessionId",
+ path: "/sessions/$sessionId",
+ getParentRoute: () => ShellRoute,
+} as any);
+const ShellProjectsProjectIdRoute = ShellProjectsProjectIdRouteImport.update({
+ id: "/projects/$projectId",
+ path: "/projects/$projectId",
+ getParentRoute: () => ShellRoute,
+} as any);
+const ShellProjectsProjectIdSettingsRoute = ShellProjectsProjectIdSettingsRouteImport.update({
+ id: "/projects/$projectId_/settings",
+ path: "/projects/$projectId/settings",
+ getParentRoute: () => ShellRoute,
+} as any);
+const ShellProjectsProjectIdSessionsSessionIdRoute = ShellProjectsProjectIdSessionsSessionIdRouteImport.update({
+ id: "/projects/$projectId_/sessions/$sessionId",
+ path: "/projects/$projectId/sessions/$sessionId",
+ getParentRoute: () => ShellRoute,
} as any);
export interface FileRoutesByFullPath {
- "/": typeof IndexRoute;
- "/workspaces/$workspaceId": typeof WorkspacesWorkspaceIdRoute;
- "/workspaces/$workspaceId/sessions/$sessionId": typeof WorkspacesWorkspaceIdSessionsSessionIdRoute;
+ "/": typeof ShellIndexRoute;
+ "/prs": typeof ShellPrsRoute;
+ "/review": typeof ShellReviewRoute;
+ "/reviews": typeof ShellReviewsRoute;
+ "/projects/$projectId": typeof ShellProjectsProjectIdRoute;
+ "/sessions/$sessionId": typeof ShellSessionsSessionIdRoute;
+ "/projects/$projectId/settings": typeof ShellProjectsProjectIdSettingsRoute;
+ "/projects/$projectId/sessions/$sessionId": typeof ShellProjectsProjectIdSessionsSessionIdRoute;
}
export interface FileRoutesByTo {
- "/": typeof IndexRoute;
- "/workspaces/$workspaceId": typeof WorkspacesWorkspaceIdRoute;
- "/workspaces/$workspaceId/sessions/$sessionId": typeof WorkspacesWorkspaceIdSessionsSessionIdRoute;
+ "/prs": typeof ShellPrsRoute;
+ "/review": typeof ShellReviewRoute;
+ "/reviews": typeof ShellReviewsRoute;
+ "/": typeof ShellIndexRoute;
+ "/projects/$projectId": typeof ShellProjectsProjectIdRoute;
+ "/sessions/$sessionId": typeof ShellSessionsSessionIdRoute;
+ "/projects/$projectId/settings": typeof ShellProjectsProjectIdSettingsRoute;
+ "/projects/$projectId/sessions/$sessionId": typeof ShellProjectsProjectIdSessionsSessionIdRoute;
}
export interface FileRoutesById {
__root__: typeof rootRouteImport;
- "/": typeof IndexRoute;
- "/workspaces/$workspaceId": typeof WorkspacesWorkspaceIdRoute;
- "/workspaces/$workspaceId_/sessions/$sessionId": typeof WorkspacesWorkspaceIdSessionsSessionIdRoute;
+ "/_shell": typeof ShellRouteWithChildren;
+ "/_shell/prs": typeof ShellPrsRoute;
+ "/_shell/review": typeof ShellReviewRoute;
+ "/_shell/reviews": typeof ShellReviewsRoute;
+ "/_shell/": typeof ShellIndexRoute;
+ "/_shell/projects/$projectId": typeof ShellProjectsProjectIdRoute;
+ "/_shell/sessions/$sessionId": typeof ShellSessionsSessionIdRoute;
+ "/_shell/projects/$projectId_/settings": typeof ShellProjectsProjectIdSettingsRoute;
+ "/_shell/projects/$projectId_/sessions/$sessionId": typeof ShellProjectsProjectIdSessionsSessionIdRoute;
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath;
- fullPaths: "/" | "/workspaces/$workspaceId" | "/workspaces/$workspaceId/sessions/$sessionId";
+ fullPaths:
+ | "/"
+ | "/prs"
+ | "/review"
+ | "/reviews"
+ | "/projects/$projectId"
+ | "/sessions/$sessionId"
+ | "/projects/$projectId/settings"
+ | "/projects/$projectId/sessions/$sessionId";
fileRoutesByTo: FileRoutesByTo;
- to: "/" | "/workspaces/$workspaceId" | "/workspaces/$workspaceId/sessions/$sessionId";
- id: "__root__" | "/" | "/workspaces/$workspaceId" | "/workspaces/$workspaceId_/sessions/$sessionId";
+ to:
+ | "/prs"
+ | "/review"
+ | "/reviews"
+ | "/"
+ | "/projects/$projectId"
+ | "/sessions/$sessionId"
+ | "/projects/$projectId/settings"
+ | "/projects/$projectId/sessions/$sessionId";
+ id:
+ | "__root__"
+ | "/_shell"
+ | "/_shell/prs"
+ | "/_shell/review"
+ | "/_shell/reviews"
+ | "/_shell/"
+ | "/_shell/projects/$projectId"
+ | "/_shell/sessions/$sessionId"
+ | "/_shell/projects/$projectId_/settings"
+ | "/_shell/projects/$projectId_/sessions/$sessionId";
fileRoutesById: FileRoutesById;
}
export interface RootRouteChildren {
- IndexRoute: typeof IndexRoute;
- WorkspacesWorkspaceIdRoute: typeof WorkspacesWorkspaceIdRoute;
- WorkspacesWorkspaceIdSessionsSessionIdRoute: typeof WorkspacesWorkspaceIdSessionsSessionIdRoute;
+ ShellRoute: typeof ShellRouteWithChildren;
}
declare module "@tanstack/react-router" {
interface FileRoutesByPath {
- "/": {
- id: "/";
+ "/_shell": {
+ id: "/_shell";
+ path: "";
+ fullPath: "/";
+ preLoaderRoute: typeof ShellRouteImport;
+ parentRoute: typeof rootRouteImport;
+ };
+ "/_shell/": {
+ id: "/_shell/";
path: "/";
fullPath: "/";
- preLoaderRoute: typeof IndexRouteImport;
- parentRoute: typeof rootRouteImport;
+ preLoaderRoute: typeof ShellIndexRouteImport;
+ parentRoute: typeof ShellRoute;
};
- "/workspaces/$workspaceId": {
- id: "/workspaces/$workspaceId";
- path: "/workspaces/$workspaceId";
- fullPath: "/workspaces/$workspaceId";
- preLoaderRoute: typeof WorkspacesWorkspaceIdRouteImport;
- parentRoute: typeof rootRouteImport;
+ "/_shell/reviews": {
+ id: "/_shell/reviews";
+ path: "/reviews";
+ fullPath: "/reviews";
+ preLoaderRoute: typeof ShellReviewsRouteImport;
+ parentRoute: typeof ShellRoute;
};
- "/workspaces/$workspaceId_/sessions/$sessionId": {
- id: "/workspaces/$workspaceId_/sessions/$sessionId";
- path: "/workspaces/$workspaceId/sessions/$sessionId";
- fullPath: "/workspaces/$workspaceId/sessions/$sessionId";
- preLoaderRoute: typeof WorkspacesWorkspaceIdSessionsSessionIdRouteImport;
- parentRoute: typeof rootRouteImport;
+ "/_shell/review": {
+ id: "/_shell/review";
+ path: "/review";
+ fullPath: "/review";
+ preLoaderRoute: typeof ShellReviewRouteImport;
+ parentRoute: typeof ShellRoute;
+ };
+ "/_shell/prs": {
+ id: "/_shell/prs";
+ path: "/prs";
+ fullPath: "/prs";
+ preLoaderRoute: typeof ShellPrsRouteImport;
+ parentRoute: typeof ShellRoute;
+ };
+ "/_shell/sessions/$sessionId": {
+ id: "/_shell/sessions/$sessionId";
+ path: "/sessions/$sessionId";
+ fullPath: "/sessions/$sessionId";
+ preLoaderRoute: typeof ShellSessionsSessionIdRouteImport;
+ parentRoute: typeof ShellRoute;
+ };
+ "/_shell/projects/$projectId": {
+ id: "/_shell/projects/$projectId";
+ path: "/projects/$projectId";
+ fullPath: "/projects/$projectId";
+ preLoaderRoute: typeof ShellProjectsProjectIdRouteImport;
+ parentRoute: typeof ShellRoute;
+ };
+ "/_shell/projects/$projectId_/settings": {
+ id: "/_shell/projects/$projectId_/settings";
+ path: "/projects/$projectId/settings";
+ fullPath: "/projects/$projectId/settings";
+ preLoaderRoute: typeof ShellProjectsProjectIdSettingsRouteImport;
+ parentRoute: typeof ShellRoute;
+ };
+ "/_shell/projects/$projectId_/sessions/$sessionId": {
+ id: "/_shell/projects/$projectId_/sessions/$sessionId";
+ path: "/projects/$projectId/sessions/$sessionId";
+ fullPath: "/projects/$projectId/sessions/$sessionId";
+ preLoaderRoute: typeof ShellProjectsProjectIdSessionsSessionIdRouteImport;
+ parentRoute: typeof ShellRoute;
};
}
}
+interface ShellRouteChildren {
+ ShellPrsRoute: typeof ShellPrsRoute;
+ ShellReviewRoute: typeof ShellReviewRoute;
+ ShellReviewsRoute: typeof ShellReviewsRoute;
+ ShellIndexRoute: typeof ShellIndexRoute;
+ ShellProjectsProjectIdRoute: typeof ShellProjectsProjectIdRoute;
+ ShellSessionsSessionIdRoute: typeof ShellSessionsSessionIdRoute;
+ ShellProjectsProjectIdSettingsRoute: typeof ShellProjectsProjectIdSettingsRoute;
+ ShellProjectsProjectIdSessionsSessionIdRoute: typeof ShellProjectsProjectIdSessionsSessionIdRoute;
+}
+
+const ShellRouteChildren: ShellRouteChildren = {
+ ShellPrsRoute: ShellPrsRoute,
+ ShellReviewRoute: ShellReviewRoute,
+ ShellReviewsRoute: ShellReviewsRoute,
+ ShellIndexRoute: ShellIndexRoute,
+ ShellProjectsProjectIdRoute: ShellProjectsProjectIdRoute,
+ ShellSessionsSessionIdRoute: ShellSessionsSessionIdRoute,
+ ShellProjectsProjectIdSettingsRoute: ShellProjectsProjectIdSettingsRoute,
+ ShellProjectsProjectIdSessionsSessionIdRoute: ShellProjectsProjectIdSessionsSessionIdRoute,
+};
+
+const ShellRouteWithChildren = ShellRoute._addFileChildren(ShellRouteChildren);
+
const rootRouteChildren: RootRouteChildren = {
- IndexRoute: IndexRoute,
- WorkspacesWorkspaceIdRoute: WorkspacesWorkspaceIdRoute,
- WorkspacesWorkspaceIdSessionsSessionIdRoute: WorkspacesWorkspaceIdSessionsSessionIdRoute,
+ ShellRoute: ShellRouteWithChildren,
};
export const routeTree = rootRouteImport._addFileChildren(rootRouteChildren)._addFileTypes();
diff --git a/frontend/src/renderer/routes/_shell.index.tsx b/frontend/src/renderer/routes/_shell.index.tsx
new file mode 100644
index 000000000..6bfeef7fc
--- /dev/null
+++ b/frontend/src/renderer/routes/_shell.index.tsx
@@ -0,0 +1,6 @@
+import { createFileRoute } from "@tanstack/react-router";
+import { SessionsBoard } from "../components/SessionsBoard";
+
+export const Route = createFileRoute("/_shell/")({
+ component: () => ,
+});
diff --git a/frontend/src/renderer/routes/_shell.projects.$projectId.tsx b/frontend/src/renderer/routes/_shell.projects.$projectId.tsx
new file mode 100644
index 000000000..3c641245e
--- /dev/null
+++ b/frontend/src/renderer/routes/_shell.projects.$projectId.tsx
@@ -0,0 +1,11 @@
+import { createFileRoute } from "@tanstack/react-router";
+import { SessionsBoard } from "../components/SessionsBoard";
+
+export const Route = createFileRoute("/_shell/projects/$projectId")({
+ component: ProjectBoardRoute,
+});
+
+function ProjectBoardRoute() {
+ const { projectId } = Route.useParams();
+ return ;
+}
diff --git a/frontend/src/renderer/routes/_shell.projects.$projectId_.sessions.$sessionId.tsx b/frontend/src/renderer/routes/_shell.projects.$projectId_.sessions.$sessionId.tsx
new file mode 100644
index 000000000..0a2b22616
--- /dev/null
+++ b/frontend/src/renderer/routes/_shell.projects.$projectId_.sessions.$sessionId.tsx
@@ -0,0 +1,11 @@
+import { createFileRoute } from "@tanstack/react-router";
+import { SessionView } from "../components/SessionView";
+
+export const Route = createFileRoute("/_shell/projects/$projectId_/sessions/$sessionId")({
+ component: ProjectSessionRoute,
+});
+
+function ProjectSessionRoute() {
+ const { projectId, sessionId } = Route.useParams();
+ return ;
+}
diff --git a/frontend/src/renderer/routes/_shell.projects.$projectId_.settings.tsx b/frontend/src/renderer/routes/_shell.projects.$projectId_.settings.tsx
new file mode 100644
index 000000000..dd97630f9
--- /dev/null
+++ b/frontend/src/renderer/routes/_shell.projects.$projectId_.settings.tsx
@@ -0,0 +1,11 @@
+import { createFileRoute } from "@tanstack/react-router";
+import { ProjectSettingsForm } from "../components/ProjectSettingsForm";
+
+export const Route = createFileRoute("/_shell/projects/$projectId_/settings")({
+ component: ProjectSettingsRoute,
+});
+
+function ProjectSettingsRoute() {
+ const { projectId } = Route.useParams();
+ return ;
+}
diff --git a/frontend/src/renderer/routes/_shell.prs.tsx b/frontend/src/renderer/routes/_shell.prs.tsx
new file mode 100644
index 000000000..bf2f3cb22
--- /dev/null
+++ b/frontend/src/renderer/routes/_shell.prs.tsx
@@ -0,0 +1,6 @@
+import { createFileRoute } from "@tanstack/react-router";
+import { PullRequestsPage } from "../components/PullRequestsPage";
+
+export const Route = createFileRoute("/_shell/prs")({
+ component: PullRequestsPage,
+});
diff --git a/frontend/src/renderer/routes/_shell.review.tsx b/frontend/src/renderer/routes/_shell.review.tsx
new file mode 100644
index 000000000..e1583bd23
--- /dev/null
+++ b/frontend/src/renderer/routes/_shell.review.tsx
@@ -0,0 +1,6 @@
+import { createFileRoute } from "@tanstack/react-router";
+import { ReviewDashboard } from "../components/ReviewDashboard";
+
+export const Route = createFileRoute("/_shell/review")({
+ component: ReviewDashboard,
+});
diff --git a/frontend/src/renderer/routes/_shell.reviews.tsx b/frontend/src/renderer/routes/_shell.reviews.tsx
new file mode 100644
index 000000000..ecb1de893
--- /dev/null
+++ b/frontend/src/renderer/routes/_shell.reviews.tsx
@@ -0,0 +1,8 @@
+import { createFileRoute, redirect } from "@tanstack/react-router";
+
+// /reviews is an alias for /review (matches agent-orchestrator).
+export const Route = createFileRoute("/_shell/reviews")({
+ beforeLoad: () => {
+ throw redirect({ to: "/review" });
+ },
+});
diff --git a/frontend/src/renderer/routes/_shell.sessions.$sessionId.tsx b/frontend/src/renderer/routes/_shell.sessions.$sessionId.tsx
new file mode 100644
index 000000000..d7b0f6ae8
--- /dev/null
+++ b/frontend/src/renderer/routes/_shell.sessions.$sessionId.tsx
@@ -0,0 +1,11 @@
+import { createFileRoute } from "@tanstack/react-router";
+import { SessionView } from "../components/SessionView";
+
+export const Route = createFileRoute("/_shell/sessions/$sessionId")({
+ component: SessionRoute,
+});
+
+function SessionRoute() {
+ const { sessionId } = Route.useParams();
+ return ;
+}
diff --git a/frontend/src/renderer/routes/_shell.tsx b/frontend/src/renderer/routes/_shell.tsx
new file mode 100644
index 000000000..9c7f0122f
--- /dev/null
+++ b/frontend/src/renderer/routes/_shell.tsx
@@ -0,0 +1,194 @@
+import { createFileRoute, Outlet, useNavigate } from "@tanstack/react-router";
+import { useQueryClient } from "@tanstack/react-query";
+import { useCallback, useEffect, useState } from "react";
+import { Sidebar } from "../components/Sidebar";
+import { SidebarProvider } from "../components/ui/sidebar";
+import { SpawnWorkerModal } from "../components/SpawnWorkerModal";
+import { TitlebarNav } from "../components/TitlebarNav";
+import { useDaemonStatus } from "../hooks/useDaemonStatus";
+import { useWorkspaceQuery, workspaceQueryKey, workspaceQueryOptions } from "../hooks/useWorkspaceQuery";
+import { apiClient, apiErrorMessage } from "../lib/api-client";
+import { ShellProvider } from "../lib/shell-context";
+import { readStoredTheme, type Theme, useUiStore } from "../stores/ui-store";
+import { toAgentProvider, toSessionStatus, type AgentProvider, type WorkspaceSummary } from "../types/workspace";
+
+export const Route = createFileRoute("/_shell")({
+ // Prefetch the workspace list for the whole shell (parent loaders run before
+ // children); pairs with the router's defaultPreload: "intent" so a hovered
+ // nav target is warm before the click.
+ loader: ({ context }) => context.queryClient.ensureQueryData(workspaceQueryOptions),
+ component: ShellLayout,
+});
+
+function systemTheme(): Theme {
+ return window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
+}
+
+function errorMessage(error: unknown) {
+ return error instanceof Error ? error.message : "Could not load projects";
+}
+
+// Persistent app shell: the Sidebar + shared state survive route changes; only
+// the content (board / session / settings / …) swaps. Lifted out of
+// the old single , with selection now owned by the router (route params)
+// instead of Zustand. The daemon-status effect runs here exactly once.
+function ShellLayout() {
+ const navigate = useNavigate();
+ const queryClient = useQueryClient();
+ const workspaceQuery = useWorkspaceQuery();
+ const workspaces = workspaceQuery.data ?? [];
+ const daemonStatus = useDaemonStatus(queryClient);
+ const { theme, setTheme, isSidebarOpen, toggleSidebar } = useUiStore();
+ const [spawnOpen, setSpawnOpen] = useState(false);
+ const [spawnProjectId, setSpawnProjectId] = useState(undefined);
+
+ const openSpawn = useCallback((projectId?: string) => {
+ setSpawnProjectId(projectId);
+ setSpawnOpen(true);
+ }, []);
+
+ const updateWorkspaces = useCallback(
+ (updater: (workspaces: WorkspaceSummary[]) => WorkspaceSummary[]) => {
+ queryClient.setQueryData(workspaceQueryKey, (current = []) => updater(current));
+ },
+ [queryClient],
+ );
+
+ const createProject = useCallback(
+ async (input: { path: string }) => {
+ const { data, error } = await apiClient.POST("/api/v1/projects", { body: { path: input.path } });
+ if (error) throw new Error(apiErrorMessage(error));
+ if (!data?.project) throw new Error("Project creation returned no project");
+
+ const workspace: WorkspaceSummary = {
+ id: data.project.id,
+ name: data.project.name,
+ path: data.project.path,
+ type: "main",
+ sessions: [],
+ };
+ updateWorkspaces((current) => [workspace, ...current.filter((item) => item.id !== workspace.id)]);
+ void navigate({ to: "/projects/$projectId", params: { projectId: workspace.id } });
+ },
+ [navigate, updateWorkspaces],
+ );
+
+ const createTask = useCallback(
+ async (input: { projectId: string; prompt: string; branch?: string; harness?: AgentProvider }) => {
+ const { data, error } = await apiClient.POST("/api/v1/sessions", {
+ body: {
+ projectId: input.projectId,
+ kind: "worker",
+ harness: input.harness,
+ prompt: input.prompt,
+ branch: input.branch || undefined,
+ },
+ });
+ if (error || !data?.session) throw new Error(error ? apiErrorMessage(error) : "No session returned");
+
+ const session = data.session;
+ updateWorkspaces((current) =>
+ current.map((item) =>
+ item.id === input.projectId
+ ? {
+ ...item,
+ sessions: [
+ {
+ id: session.id,
+ terminalHandleId: session.terminalHandleId,
+ workspaceId: item.id,
+ workspaceName: item.name,
+ title: input.prompt,
+ provider: toAgentProvider(session.harness),
+ branch: input.branch ?? "",
+ status: toSessionStatus(session.status, session.isTerminated),
+ updatedAt: "now",
+ },
+ ...item.sessions.filter((existing) => existing.id !== session.id),
+ ],
+ }
+ : item,
+ ),
+ );
+ void navigate({
+ to: "/projects/$projectId/sessions/$sessionId",
+ params: { projectId: input.projectId, sessionId: session.id },
+ });
+ },
+ [navigate, updateWorkspaces],
+ );
+
+ useEffect(() => {
+ document.documentElement.dataset.theme = theme;
+ document.documentElement.style.colorScheme = theme;
+ }, [theme]);
+
+ // Follow OS appearance only until the user picks a theme explicitly.
+ useEffect(() => {
+ if (readStoredTheme()) return;
+
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: light)");
+ const handleChange = () => setTheme(systemTheme());
+ mediaQuery.addEventListener("change", handleChange);
+ return () => mediaQuery.removeEventListener("change", handleChange);
+ }, [setTheme]);
+
+ // ⌘B lives in SidebarProvider (shadcn's built-in shortcut), which routes
+ // through onOpenChange back into the ui-store.
+ useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if ((event.metaKey || event.ctrlKey) && /^[1-9]$/.test(event.key)) {
+ const workspace = workspaces[Number(event.key) - 1];
+ if (workspace) {
+ event.preventDefault();
+ void navigate({ to: "/projects/$projectId", params: { projectId: workspace.id } });
+ }
+ }
+ };
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, [navigate, workspaces]);
+
+ return (
+
+ {/* Controlled by the ui-store so TitlebarNav / Topbar toggles (which call
+ the store directly) stay in sync. --sidebar-width chains to the
+ drag-resizable --ao-sidebar-w set on :root by useResizable. */}
+ open !== isSidebarOpen && toggleSidebar()}
+ open={isSidebarOpen}
+ style={
+ { "--sidebar-width": "var(--ao-sidebar-w, 240px)", "--sidebar-width-icon": "48px" } as React.CSSProperties
+ }
+ >
+
+
+
+
+ {/* Fixed macOS titlebar cluster beside the traffic lights — rendered
+ once here so the toggle/history buttons never move when the
+ sidebar collapses or expands. MUST come after the sidebar/topbars
+ in the DOM: Electron builds the window-drag region in document
+ order (drag rects add, no-drag rects subtract), so the cluster's
+ no-drag holes only survive if they're processed after the drag
+ strips they overlap. Rendered first, real clicks get swallowed by
+ window-drag even though DOM hit-testing looks correct. */}
+
+
+
+
+ );
+}
diff --git a/frontend/src/renderer/routes/index.tsx b/frontend/src/renderer/routes/index.tsx
deleted file mode 100644
index 9d84ea16a..000000000
--- a/frontend/src/renderer/routes/index.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import { createFileRoute } from "@tanstack/react-router";
-import { App } from "../App";
-
-export const Route = createFileRoute("/")({
- component: App,
-});
diff --git a/frontend/src/renderer/routes/workspaces.$workspaceId.tsx b/frontend/src/renderer/routes/workspaces.$workspaceId.tsx
deleted file mode 100644
index 23ee006bd..000000000
--- a/frontend/src/renderer/routes/workspaces.$workspaceId.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import { createFileRoute } from "@tanstack/react-router";
-import { App } from "../App";
-
-export const Route = createFileRoute("/workspaces/$workspaceId")({
- component: WorkspaceRoute,
-});
-
-function WorkspaceRoute() {
- const { workspaceId } = Route.useParams();
- return ;
-}
diff --git a/frontend/src/renderer/routes/workspaces.$workspaceId_.sessions.$sessionId.tsx b/frontend/src/renderer/routes/workspaces.$workspaceId_.sessions.$sessionId.tsx
deleted file mode 100644
index 4ebff9898..000000000
--- a/frontend/src/renderer/routes/workspaces.$workspaceId_.sessions.$sessionId.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import { createFileRoute } from "@tanstack/react-router";
-import { App } from "../App";
-
-export const Route = createFileRoute("/workspaces/$workspaceId_/sessions/$sessionId")({
- component: SessionRoute,
-});
-
-function SessionRoute() {
- const { workspaceId, sessionId } = Route.useParams();
- return ;
-}
diff --git a/frontend/src/renderer/stores/ui-store.ts b/frontend/src/renderer/stores/ui-store.ts
index 8ae619a1b..fd33ebb06 100644
--- a/frontend/src/renderer/stores/ui-store.ts
+++ b/frontend/src/renderer/stores/ui-store.ts
@@ -1,27 +1,30 @@
import { create } from "zustand";
export type Theme = "light" | "dark";
-/** Orchestrator-led: the app lands on the orchestrator; a worker row drills in. */
+/** Whether a terminal pane shows the orchestrator or a worker session. */
export type WorkbenchView = "orchestrator" | "session";
-/** Worker topbar view toggles — Changes (Git rail) is the default. */
+/** Worker detail view toggles — Changes (Git rail) is the default. */
export type WorkbenchTab = "changes" | "files" | "terminal";
+// Selection (which project/session is open) now lives in the URL — the router
+// is the single source of truth, read via route params. This store holds only
+// ephemeral, route-independent UI: theme, sidebar/inspector collapse, and the
+// active workbench tab within a session.
type UiState = {
- view: WorkbenchView;
workbenchTab: WorkbenchTab;
isSidebarOpen: boolean;
- selectedSessionId: string | null;
- selectedWorkspaceId: string | null;
+ isInspectorOpen: boolean;
theme: Theme;
setWorkbenchTab: (tab: WorkbenchTab) => void;
- setSystemTheme: (theme: Theme) => void;
+ setTheme: (theme: Theme) => void;
+ toggleTheme: () => void;
toggleSidebar: () => void;
- selectOrchestrator: () => void;
- selectWorkspace: (workspaceId: string) => void;
- selectSession: (sessionId: string, workspaceId?: string) => void;
+ toggleInspector: () => void;
};
const sidebarStorageKey = "ao.sidebar.open";
+const inspectorStorageKey = "ao.inspector.open";
+const themeStorageKey = "ao.theme";
function getLocalStorage() {
if (typeof window === "undefined" || !window.localStorage) return null;
@@ -32,34 +35,52 @@ function initialSidebarOpen() {
return getLocalStorage()?.getItem(sidebarStorageKey) !== "false";
}
-function initialTheme(): Theme {
- if (typeof window === "undefined") return "dark";
+function initialInspectorOpen() {
+ return getLocalStorage()?.getItem(inspectorStorageKey) !== "false";
+}
+function systemTheme(): Theme {
+ if (typeof window === "undefined") return "dark";
return window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
}
+function initialTheme(): Theme {
+ const stored = getLocalStorage()?.getItem(themeStorageKey);
+ if (stored === "light" || stored === "dark") return stored;
+ return systemTheme();
+}
+
+export function readStoredTheme(): Theme | null {
+ const stored = getLocalStorage()?.getItem(themeStorageKey);
+ return stored === "light" || stored === "dark" ? stored : null;
+}
+
export const useUiStore = create((set) => ({
- view: "orchestrator",
workbenchTab: "changes",
isSidebarOpen: initialSidebarOpen(),
- selectedSessionId: null,
- selectedWorkspaceId: null,
+ isInspectorOpen: initialInspectorOpen(),
theme: initialTheme(),
setWorkbenchTab: (workbenchTab) => set({ workbenchTab }),
- setSystemTheme: (theme) => set({ theme }),
+ setTheme: (theme) => {
+ getLocalStorage()?.setItem(themeStorageKey, theme);
+ set({ theme });
+ },
+ toggleTheme: () =>
+ set((state) => {
+ const theme = state.theme === "dark" ? "light" : "dark";
+ getLocalStorage()?.setItem(themeStorageKey, theme);
+ return { theme };
+ }),
toggleSidebar: () =>
set((state) => {
const isSidebarOpen = !state.isSidebarOpen;
getLocalStorage()?.setItem(sidebarStorageKey, String(isSidebarOpen));
return { isSidebarOpen };
}),
- selectOrchestrator: () => set({ view: "orchestrator" }),
- selectWorkspace: (selectedWorkspaceId) => set({ selectedWorkspaceId }),
- selectSession: (selectedSessionId, workspaceId) =>
- set((state) => ({
- selectedSessionId,
- selectedWorkspaceId: workspaceId ?? state.selectedWorkspaceId,
- view: "session",
- workbenchTab: "changes",
- })),
+ toggleInspector: () =>
+ set((state) => {
+ const isInspectorOpen = !state.isInspectorOpen;
+ getLocalStorage()?.setItem(inspectorStorageKey, String(isInspectorOpen));
+ return { isInspectorOpen };
+ }),
}));
diff --git a/frontend/src/renderer/styles.css b/frontend/src/renderer/styles.css
index 8cc7748bf..991bf166b 100644
--- a/frontend/src/renderer/styles.css
+++ b/frontend/src/renderer/styles.css
@@ -10,19 +10,37 @@
--font-sans:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Helvetica Neue",
sans-serif;
- --font-mono: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+ /* Nerd Font stack (ported from yyork): the terminal reads --font-mono so
+ * agent TUIs get powerline separators and file-type icons. These are
+ * system-installed fonts — the app bundles none — and the browser picks the
+ * first one present, falling back to plain monospace (no icon glyphs) if a
+ * user has no Nerd Font. JetBrainsMono Nerd Font is the primary target. */
+ --font-mono:
+ "JetBrainsMono Nerd Font Mono", "JetBrainsMono Nerd Font", "FiraCode Nerd Font Mono", "FiraCode Nerd Font",
+ "MesloLGS NF", "CaskaydiaCove Nerd Font Mono", "CaskaydiaCove Nerd Font", "Hack Nerd Font Mono", "Hack Nerd Font",
+ "Symbols Nerd Font Mono", "Symbols Nerd Font", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
+ "Liberation Mono", "Courier New", monospace;
/* Surfaces */
--color-background: var(--bg);
--color-foreground: var(--fg);
--color-surface: var(--bg-1);
--color-card: var(--bg-1);
+ --color-card-foreground: var(--fg);
--color-raised: var(--bg-2);
--color-overlay: var(--bg-3);
--color-popover: var(--bg-1);
--color-popover-foreground: var(--fg);
- --color-sidebar: var(--bg);
+ /* shadcn input border uses border-input; map to our hairline border */
+ --color-input: var(--border);
+ --color-sidebar: var(--sidebar-bg);
--color-sidebar-foreground: var(--fg-muted);
+ --color-sidebar-primary: var(--accent);
+ --color-sidebar-primary-foreground: var(--accent-fg);
+ --color-sidebar-accent: var(--bg-2);
+ --color-sidebar-accent-foreground: var(--fg);
+ --color-sidebar-border: var(--border);
+ --color-sidebar-ring: var(--accent);
--color-terminal: var(--term-bg);
/* Text */
@@ -45,12 +63,22 @@
--color-border: var(--border);
--color-border-strong: var(--border-1);
- /* Status — map 1:1 to the daemon's derived statuses */
+ /* Status — map 1:1 to the daemon's derived statuses (agent-orchestrator
+ * palette): working=orange, needs-you=amber, mergeable=green, fail=red. */
+ --color-working: var(--orange);
--color-success: var(--green);
--color-success-strong: var(--green-strong);
--color-warning: var(--amber);
--color-error: var(--red);
+ /* shadcn "destructive" → our error red (Button destructive, Alert, etc.) */
+ --color-destructive: var(--red);
+ --color-destructive-foreground: #ffffff;
+
+ /* Theme-adaptive interactive surfaces (sidebar hovers, tabs, kanban chrome) */
+ --color-interactive-hover: var(--interactive-hover);
+ --color-interactive-active: var(--interactive-active);
+
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
@@ -62,31 +90,41 @@
}
:root {
- /* Dark — primary. sRGB approximations of emdash's display-p3 neutral ramp. */
- --bg: #111111;
- --bg-1: #191919;
- --bg-2: #222222;
- --bg-3: #2a2a2a;
- --fg: #eeeeee;
- --fg-muted: #b4b4b4;
- --fg-passive: #6e6e6e;
- --border: #3a3a3a;
- --border-1: #484848;
- --accent: #5b9dff;
- --accent-fg: #0b0d12;
- --accent-weak: rgb(91 157 255 / 0.16);
- --accent-dim: #2a4a7a;
- --green: #6cb16c;
- --green-strong: #7bd88f;
- --amber: #ffcc4a;
- --red: #d4544f;
- --term-bg: #161616;
+ /* Dark — cloned from the agent-orchestrator web app (packages/web). #0a0b0d
+ * base, #15171b is the only bordered surface, hairline white-alpha lines,
+ * blue accent, orange=working / amber=needs-you / green=mergeable / red=fail. */
+ --bg: #0a0b0d;
+ --bg-1: #15171b;
+ --bg-2: #1b1d22;
+ --bg-3: #212329;
+ --fg: #f4f5f7;
+ --fg-muted: #9ba1aa;
+ --fg-passive: #646a73;
+ --border: rgb(255 255 255 / 0.06);
+ --border-1: rgb(255 255 255 / 0.1);
+ --accent: #4d8dff;
+ --accent-fg: #ffffff;
+ --accent-weak: rgb(77 141 255 / 0.16);
+ --accent-dim: #24406e;
+ --orange: #f59f4c;
+ --green: #74b98a;
+ --green-strong: #74b98a;
+ --amber: #e8c14a;
+ --red: #ef6b6b;
+ /* Terminal — dark palette (light override below). */
+ --term-bg: #15171b;
--term-fg: #d7d7d2;
- --term-green: #7bd88f;
+ --term-green: #74b98a;
--term-dim: #7c7c7c;
- --term-blue: #5b9dff;
- --term-amber: #ffcc4a;
- --shadow: 0 1px 0 rgb(0 0 0 / 0.4), 0 12px 32px rgb(0 0 0 / 0.45);
+ --term-blue: #4d8dff;
+ --term-amber: #e8c14a;
+ --shadow: 0 1px 0 rgb(0 0 0 / 0.4), 0 12px 32px rgb(0 0 0 / 0.5);
+ /* Sidebar surface sits a step below --bg (cloned #08090b). */
+ --sidebar-bg: #08090b;
+ --interactive-hover: rgb(255 255 255 / 0.04);
+ --interactive-active: rgb(255 255 255 / 0.07);
+ --kanban-column-bg: rgb(255 255 255 / 0.018);
+ --kanban-pending-glow: rgb(255 255 255 / 0.02);
}
:root[data-theme="light"] {
@@ -104,17 +142,22 @@
--accent-fg: #ffffff;
--accent-weak: rgb(37 99 235 / 0.12);
--accent-dim: #bcd2f7;
+ --sidebar-bg: var(--bg);
+ --orange: #c2410c;
--green: #1a7f37;
--green-strong: #1a7f37;
--amber: #9a6b00;
--red: #c0392b;
- /* Terminal keeps its own dark palette in both themes (it is the agent CLI). */
- --term-bg: #161616;
- --term-fg: #d7d7d2;
- --term-green: #7bd88f;
- --term-dim: #7c7c7c;
- --term-blue: #5b9dff;
- --term-amber: #ffcc4a;
+ --interactive-hover: rgb(0 0 0 / 0.04);
+ --interactive-active: rgb(0 0 0 / 0.07);
+ --kanban-column-bg: rgb(0 0 0 / 0.02);
+ --kanban-pending-glow: rgb(0 0 0 / 0.03);
+ --term-bg: #fafafa;
+ --term-fg: #24292f;
+ --term-green: #1a7f37;
+ --term-dim: #6b7280;
+ --term-blue: #2563eb;
+ --term-amber: #9a6b00;
--shadow: 0 1px 0 rgb(0 0 0 / 0.03), 0 12px 30px rgb(20 30 50 / 0.1);
}
@@ -150,6 +193,14 @@ select {
height: 100%;
}
+:root[data-theme="light"] .xterm .xterm-viewport::-webkit-scrollbar-thumb {
+ background: rgb(0 0 0 / 0.12);
+}
+
+:root[data-theme="light"] .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb {
+ background: rgb(0 0 0 / 0.2);
+}
+
@keyframes status-pulse {
0%,
100% {
@@ -179,3 +230,746 @@ select {
transform: translate(-50%, -50%) scale(1);
}
}
+
+/* ── Resizable panel handles (sidebar + inspector) ──────────────────────── */
+.resize-handle {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ width: 7px;
+ z-index: 5;
+ cursor: col-resize;
+ background: transparent;
+ touch-action: none;
+}
+
+.resize-handle--right {
+ right: -3px;
+}
+
+.resize-handle--left {
+ left: -3px;
+}
+
+.resize-handle::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 50%;
+ width: 1px;
+ transform: translateX(-50%);
+ background: transparent;
+ transition: background 0.12s ease;
+}
+
+.resize-handle:hover::after,
+body.is-resizing-x .resize-handle::after {
+ background: var(--color-accent);
+}
+
+body.is-resizing-x {
+ cursor: col-resize;
+ user-select: none;
+}
+
+/* No width transition while actively dragging the sidebar resize handle
+ * (shadcn's sidebar animates width for collapse/expand). */
+body.is-resizing-x [data-slot="sidebar-gap"],
+body.is-resizing-x [data-slot="sidebar-container"] {
+ transition: none;
+}
+
+/* ── Dashboard / session topbar (agent-orchestrator .dashboard-app-header) ─ */
+.dashboard-app-header {
+ display: flex;
+ height: 56px;
+ flex-shrink: 0;
+ align-items: center;
+ gap: 13px;
+ padding: 0 20px;
+ border-bottom: 1px solid var(--border);
+ background: var(--bg);
+ z-index: 10;
+}
+
+.dashboard-app-header__project {
+ font-size: 14.5px;
+ font-weight: 600;
+ letter-spacing: -0.01em;
+ line-height: 1;
+ color: var(--fg);
+ white-space: nowrap;
+}
+
+.topbar-project-line {
+ display: inline-flex;
+ min-width: 0;
+ align-items: center;
+ gap: 6px;
+}
+
+.dashboard-app-header__tabs {
+ display: inline-flex;
+ align-items: center;
+ gap: 2px;
+}
+
+.dashboard-app-header__tab {
+ display: inline-flex;
+ height: 28px;
+ align-items: center;
+ border-radius: 6px;
+ padding: 0 11px;
+ font-size: 12.5px;
+ line-height: 1;
+ color: var(--fg-passive);
+ transition:
+ background 0.12s ease,
+ color 0.12s ease;
+}
+
+.dashboard-app-header__tab:hover {
+ color: var(--fg);
+}
+
+.dashboard-app-header__tab.is-active {
+ background: var(--interactive-active);
+ color: var(--fg);
+}
+
+.dashboard-app-header__spacer {
+ flex: 1;
+ min-width: 0;
+}
+
+.dashboard-app-header__actions {
+ display: flex;
+ flex-shrink: 0;
+ align-items: center;
+ gap: 6px;
+}
+
+.dashboard-app-header__icon-btn {
+ display: grid;
+ width: 34px;
+ height: 34px;
+ place-items: center;
+ border-radius: 7px;
+ color: var(--fg-muted);
+ transition:
+ background 0.12s ease,
+ color 0.12s ease;
+}
+
+.dashboard-app-header__icon-btn:hover {
+ background: var(--interactive-hover);
+ color: var(--fg);
+}
+
+.dashboard-app-header__primary-btn {
+ display: inline-flex;
+ height: 34px;
+ align-items: center;
+ gap: 6px;
+ border-radius: 7px;
+ padding: 0 15px;
+ font-size: 13px;
+ font-weight: 600;
+ line-height: 1;
+ background: var(--accent);
+ color: var(--accent-fg);
+ transition: filter 0.12s ease;
+}
+
+.dashboard-app-header__primary-btn:hover {
+ filter: brightness(1.08);
+}
+
+.dashboard-app-header__primary-btn:disabled {
+ opacity: 0.6;
+}
+
+.dashboard-app-header__accent-btn {
+ display: inline-flex;
+ height: 34px;
+ align-items: center;
+ gap: 6px;
+ border-radius: 7px;
+ border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
+ padding: 0 15px;
+ font-size: 13px;
+ font-weight: 600;
+ line-height: 1;
+ background: var(--accent-weak);
+ color: var(--accent);
+ transition: background 0.12s ease;
+}
+
+.dashboard-app-header__accent-btn:hover {
+ background: color-mix(in srgb, var(--accent-weak) 80%, transparent);
+}
+
+.session-topbar.dashboard-app-header {
+ height: 56px;
+ gap: 13px;
+ padding: 0 16px 0 14px;
+}
+
+.session-topbar__lead {
+ display: flex;
+ min-width: 0;
+ align-items: center;
+ gap: 13px;
+}
+
+/* ── macOS titlebar cluster (TitlebarNav) ─────────────────────────────────
+ * Fixed beside the traffic lights so the toggle/history buttons occupy the
+ * exact same spot in both sidebar states. Lights span x=14–66 (y center 20);
+ * 28px buttons in a 40px strip share that center, and the 6.5px icon inset
+ * puts the first glyph at ~76px — the standard 10px gap after the lights. */
+.titlebar-nav {
+ position: fixed;
+ top: 0;
+ left: 70px;
+ z-index: 20;
+ display: flex;
+ height: 40px;
+ align-items: center;
+ gap: 2px;
+}
+
+.titlebar-nav__btn {
+ width: 28px;
+ height: 28px;
+}
+
+/* Collapsed-rail topbars start their content past the fixed cluster:
+ * cluster right edge 70 + 3×28 + 2×2 = 158px, + the header's 13px gap,
+ * − the 48px icon rail = 123px. Transition mirrors the sidebar's width
+ * animation (200ms linear) so content tracks the moving sidebar edge. */
+.dashboard-app-header {
+ transition: padding-left 0.2s linear;
+}
+
+.dashboard-app-header.is-under-titlebar-nav {
+ padding-left: 123px;
+}
+
+.topbar-project-pills-group {
+ display: inline-flex;
+ min-width: 0;
+ align-items: center;
+ gap: 8px;
+}
+
+.topbar-identity-sep {
+ font-size: 12px;
+ line-height: 1;
+ color: var(--fg-passive);
+}
+
+.session-detail-mode-badge {
+ display: inline-flex;
+ height: 24px;
+ align-items: center;
+ gap: 4px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 0 8px;
+ font-size: 10px;
+ font-weight: 600;
+ line-height: 1;
+ letter-spacing: 0.05em;
+}
+
+.session-detail-mode-badge--neutral {
+ background: var(--surface);
+ color: var(--fg-muted);
+}
+
+.session-topbar__identity {
+ display: flex;
+ min-width: 0;
+ align-items: center;
+ gap: 11px;
+}
+
+.session-topbar__branch {
+ display: inline-flex;
+ min-width: 0;
+ align-items: center;
+ gap: 5px;
+ font-family: var(--font-mono);
+ font-size: 10.5px;
+ line-height: 1;
+ color: var(--fg-passive);
+}
+
+/* ── Session inspector rail (agent-orchestrator SessionInspector) ───────── */
+.session-inspector {
+ display: flex;
+ height: 100%;
+ min-height: 0;
+ flex-direction: column;
+ overflow: hidden;
+ background: var(--bg);
+ border-left: 1px solid var(--border);
+}
+
+.session-inspector__tabs {
+ display: flex;
+ align-items: center;
+ gap: 3px;
+ height: 47px;
+ flex-shrink: 0;
+ padding: 0 11px;
+ border-bottom: 1px solid var(--border);
+}
+
+.session-inspector__tab {
+ flex: 1;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ min-width: 0;
+ padding: 7px 6px;
+ border-radius: 7px;
+ font-size: 11.5px;
+ font-weight: 600;
+ color: var(--fg-passive);
+ cursor: pointer;
+ transition:
+ background 0.12s ease,
+ color 0.12s ease;
+}
+
+.session-inspector__tab:hover {
+ background: var(--interactive-hover);
+ color: var(--fg);
+}
+
+.session-inspector__tab.is-active {
+ background: var(--interactive-active);
+ color: var(--fg);
+}
+
+.session-inspector__tab-icon {
+ display: inline-flex;
+ flex-shrink: 0;
+}
+
+.session-inspector__tab-icon svg {
+ width: 14px;
+ height: 14px;
+}
+
+.session-inspector__tab-label {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.session-inspector__body {
+ flex: 1;
+ min-height: 0;
+ overflow-y: auto;
+ padding: 18px 18px 40px;
+}
+
+/* Inspector resize handle (shadcn resizable separator, AO visual): a 7px
+ * transparent grab strip whose centered 1px line lights accent on hover, drag
+ * (rrp sets data-separator="active"), and keyboard focus. */
+.session-inspector__resize-handle {
+ width: 7px;
+ cursor: col-resize;
+ background: transparent;
+ touch-action: none;
+}
+
+.session-inspector__resize-handle::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 50%;
+ width: 1px;
+ transform: translateX(-50%);
+ background: transparent;
+ transition: background 0.12s ease;
+}
+
+.session-inspector__resize-handle:hover::after,
+.session-inspector__resize-handle:focus-visible::after,
+.session-inspector__resize-handle[data-separator="active"]::after {
+ background: var(--accent);
+}
+
+/* Collapse/expand animation for the inspector panel: rrp v4 drives panel
+ * sizes via flex-grow, which is animatable. Suppress it while the separator
+ * is actively dragged so resizing tracks the pointer 1:1. */
+.session-split #inspector[data-panel] {
+ transition: flex-grow 0.2s linear;
+}
+
+.session-split:has([data-separator="active"]) #inspector[data-panel] {
+ transition: none;
+}
+
+.inspector-section {
+ margin-bottom: 22px;
+}
+
+.inspector-section__head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 11px;
+ font-size: 10.5px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.09em;
+ color: var(--fg-passive);
+}
+
+.inspector-section__link {
+ font-size: 11px;
+ font-weight: 500;
+ text-transform: none;
+ letter-spacing: 0;
+ color: var(--accent);
+}
+
+.inspector-section__link:hover {
+ text-decoration: underline;
+}
+
+.inspector-empty {
+ font-size: 12px;
+ color: var(--fg-muted);
+ line-height: 1.5;
+}
+
+.inspector-empty--center {
+ padding: 24px 8px;
+ text-align: center;
+}
+
+.inspector-empty--browser {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 9px;
+ padding: 48px 18px;
+ text-align: center;
+}
+
+.inspector-empty--browser svg {
+ width: 30px;
+ height: 30px;
+ color: var(--fg-passive);
+}
+
+.inspector-empty--browser p {
+ font-size: 12.5px;
+ color: var(--fg-muted);
+}
+
+.inspector-empty--browser span {
+ font-size: 11.5px;
+ color: var(--fg-passive);
+ line-height: 1.5;
+}
+
+.inspector-kv {
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+}
+
+.inspector-kv__row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 6px 2px;
+ font-size: 12.5px;
+}
+
+.inspector-kv__k {
+ width: 74px;
+ flex-shrink: 0;
+ color: var(--fg-muted);
+}
+
+.inspector-kv__v {
+ min-width: 0;
+ color: var(--fg);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.inspector-kv__v--mono {
+ font-family: var(--font-mono);
+ font-size: 11.5px;
+}
+
+.inspector-timeline {
+ position: relative;
+ padding-left: 20px;
+}
+
+.inspector-timeline::before {
+ content: "";
+ position: absolute;
+ left: 5px;
+ top: 4px;
+ bottom: 6px;
+ width: 1px;
+ background: var(--border);
+}
+
+.inspector-timeline__ev {
+ position: relative;
+ padding: 0 0 15px;
+}
+
+.inspector-timeline__ev:last-child {
+ padding-bottom: 0;
+}
+
+.inspector-timeline__node {
+ position: absolute;
+ left: -18px;
+ top: 3px;
+ width: 9px;
+ height: 9px;
+ border-radius: 50%;
+ background: var(--fg-passive);
+ box-shadow: 0 0 0 3px var(--bg);
+}
+
+.inspector-timeline__ev--now .inspector-timeline__node {
+ background: var(--orange);
+ box-shadow:
+ 0 0 0 3px var(--bg),
+ 0 0 8px var(--orange);
+}
+
+.inspector-timeline__ev--good .inspector-timeline__node {
+ background: var(--green);
+}
+
+.inspector-timeline__ev--warn .inspector-timeline__node {
+ background: var(--amber);
+}
+
+.inspector-timeline__et {
+ font-size: 12px;
+ color: var(--fg);
+ line-height: 1.45;
+}
+
+.inspector-timeline__et b {
+ font-weight: 600;
+}
+
+.inspector-timeline__badge {
+ display: inline-flex;
+ vertical-align: middle;
+}
+
+.inspector-timeline__detail {
+ color: var(--fg-muted);
+}
+
+.inspector-timeline__ets {
+ font-size: 10.5px;
+ color: var(--fg-passive);
+ font-family: var(--font-mono);
+ margin-top: 2px;
+}
+
+/* Changes tab — git rail (second screenshot) */
+.inspector-changes__head {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin: -6px -6px 10px;
+ padding: 0 6px 10px;
+ border-bottom: 1px solid var(--border);
+}
+
+.inspector-changes__title {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--fg);
+}
+
+.inspector-changes__count {
+ font-family: var(--font-mono);
+ font-size: 11px;
+ color: var(--fg-passive);
+}
+
+.inspector-changes__actions {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 8px;
+ font-size: 12px;
+}
+
+.inspector-changes__action {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ color: var(--fg-muted);
+ transition: color 0.12s ease;
+}
+
+.inspector-changes__action:hover {
+ color: var(--fg);
+}
+
+.inspector-changes__action--danger {
+ color: var(--red);
+}
+
+.inspector-changes__action--danger:hover {
+ opacity: 0.85;
+}
+
+.inspector-changes__action--end {
+ margin-left: auto;
+}
+
+.inspector-changes__list {
+ min-height: 120px;
+ margin-bottom: 12px;
+}
+
+.inspector-changes__file {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ height: 28px;
+ padding: 0 8px;
+ border-radius: 6px;
+ font-family: var(--font-mono);
+ font-size: 12px;
+ transition: background 0.12s ease;
+}
+
+.inspector-changes__file:hover {
+ background: var(--interactive-hover);
+}
+
+.inspector-changes__path {
+ min-width: 0;
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ color: var(--fg);
+}
+
+.inspector-changes__add {
+ flex-shrink: 0;
+ color: var(--green);
+}
+
+.inspector-changes__del {
+ flex-shrink: 0;
+ color: var(--red);
+}
+
+.inspector-changes__stage {
+ width: 13px;
+ height: 13px;
+ flex-shrink: 0;
+ color: var(--fg-passive);
+}
+
+.inspector-changes__stage.is-staged {
+ color: var(--accent);
+}
+
+.inspector-changes__commit {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding-top: 12px;
+ border-top: 1px solid var(--border);
+}
+
+.inspector-changes__input,
+.inspector-changes__textarea {
+ width: 100%;
+ border-radius: 6px;
+ border: 1px solid var(--border);
+ background: transparent;
+ padding: 6px 10px;
+ font-size: 12.5px;
+ color: var(--fg);
+}
+
+.inspector-changes__input::placeholder,
+.inspector-changes__textarea::placeholder {
+ color: var(--fg-passive);
+}
+
+.inspector-changes__input:focus-visible,
+.inspector-changes__textarea:focus-visible {
+ border-color: var(--accent);
+ outline: 2px solid var(--accent-weak);
+ outline-offset: 0;
+}
+
+.inspector-changes__textarea {
+ resize: none;
+}
+
+.inspector-changes__footer {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-top: 12px;
+ padding-top: 12px;
+ border-top: 1px solid var(--border);
+ font-family: var(--font-mono);
+ font-size: 11px;
+ color: var(--fg-passive);
+}
+
+.inspector-changes__footer svg {
+ width: 12px;
+ height: 12px;
+ flex-shrink: 0;
+}
+
+.inspector-changes__branch {
+ min-width: 0;
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ color: var(--fg-muted);
+}
+
+.inspector-changes__pr {
+ display: inline-flex;
+ flex-shrink: 0;
+ align-items: center;
+ gap: 4px;
+ border-radius: 6px;
+ border: 1px solid var(--border);
+ padding: 2px 8px;
+ color: var(--fg-muted);
+ transition: background 0.12s ease;
+}
+
+.inspector-changes__pr:hover {
+ background: var(--interactive-hover);
+}
diff --git a/frontend/src/renderer/types/workspace.test.ts b/frontend/src/renderer/types/workspace.test.ts
index ccbfb1b0e..deefad752 100644
--- a/frontend/src/renderer/types/workspace.test.ts
+++ b/frontend/src/renderer/types/workspace.test.ts
@@ -1,11 +1,14 @@
import { describe, expect, it } from "vitest";
import {
+ attentionZone,
sessionIsActive,
sessionNeedsAttention,
toAgentProvider,
toSessionStatus,
workerDisplayStatus,
workerStatusPulses,
+ type AttentionZone,
+ type SessionStatus,
type WorkspaceSession,
} from "./workspace";
@@ -26,7 +29,6 @@ function sessionWith(overrides: Partial): WorkspaceSession {
describe("toSessionStatus", () => {
it("passes through a known status", () => {
expect(toSessionStatus("mergeable")).toBe("mergeable");
- expect(toSessionStatus("no_signal")).toBe("no_signal");
});
it("overrides to terminated when the session is terminated", () => {
@@ -51,7 +53,6 @@ describe("workerDisplayStatus", () => {
["needs_input", "needs_you"],
["changes_requested", "needs_you"],
["review_pending", "needs_you"],
- ["no_signal", "needs_you"],
["ci_failed", "ci_failed"],
["approved", "mergeable"],
["mergeable", "mergeable"],
@@ -106,3 +107,29 @@ describe("toAgentProvider", () => {
expect(toAgentProvider(undefined)).toBe("codex");
});
});
+
+describe("attentionZone", () => {
+ const cases: Array<[SessionStatus, AttentionZone]> = [
+ ["mergeable", "merge"],
+ ["approved", "merge"],
+ ["needs_input", "action"],
+ ["ci_failed", "action"],
+ ["changes_requested", "action"],
+ ["review_pending", "pending"],
+ ["pr_open", "pending"],
+ ["draft", "pending"],
+ ["working", "working"],
+ ["idle", "working"],
+ ["merged", "done"],
+ ["terminated", "done"],
+ ];
+
+ it.each(cases)("buckets %s into the %s zone", (status, zone) => {
+ expect(attentionZone(sessionWith({ status }))).toBe(zone);
+ });
+
+ it("prioritizes merge as the highest-ROI zone", () => {
+ // merge is checked before action/pending so an approved PR always surfaces.
+ expect(attentionZone(sessionWith({ status: "approved" }))).toBe("merge");
+ });
+});
diff --git a/frontend/src/renderer/types/workspace.ts b/frontend/src/renderer/types/workspace.ts
index 70ad080bf..d3db3178f 100644
--- a/frontend/src/renderer/types/workspace.ts
+++ b/frontend/src/renderer/types/workspace.ts
@@ -10,7 +10,6 @@ export type SessionStatus =
| "merged"
| "needs_input"
| "idle"
- | "no_signal"
| "terminated";
const sessionStatuses = new Set([
@@ -25,7 +24,6 @@ const sessionStatuses = new Set([
"merged",
"needs_input",
"idle",
- "no_signal",
"terminated",
]);
@@ -67,6 +65,8 @@ export type ChangedFile = {
staged?: boolean;
};
+export type SessionKind = "worker" | "orchestrator";
+
export type WorkspaceSession = {
id: string;
terminalHandleId?: string;
@@ -74,8 +74,12 @@ export type WorkspaceSession = {
workspaceName: string;
title: string;
provider: AgentProvider;
+ kind?: SessionKind;
branch: string;
status: SessionStatus;
+ /** ISO timestamp from the daemon — used for relative time in the inspector. */
+ createdAt?: string;
+ /** ISO timestamp from the daemon. */
updatedAt: string;
/** The session's git diff against its base, when known. */
changedFiles?: ChangedFile[];
@@ -101,9 +105,6 @@ export function workerDisplayStatus(session: WorkspaceSession): WorkerDisplaySta
case "needs_input":
case "changes_requested":
case "review_pending":
- // no_signal: the daemon has never heard from this agent — a human
- // should look at the pane, so it surfaces as needs-you.
- case "no_signal":
return "needs_you";
case "ci_failed":
return "ci_failed";
@@ -118,6 +119,22 @@ export function workerDisplayStatus(session: WorkspaceSession): WorkerDisplaySta
}
}
+export function isOrchestratorSession(session: WorkspaceSession): boolean {
+ return session.kind === "orchestrator" || session.id.endsWith("-orchestrator");
+}
+
+export function findProjectOrchestrator(
+ workspaces: WorkspaceSummary[],
+ projectId: string,
+): WorkspaceSession | undefined {
+ const workspace = workspaces.find((w) => w.id === projectId);
+ return workspace?.sessions.find(isOrchestratorSession);
+}
+
+export function workerSessions(sessions: WorkspaceSession[]): WorkspaceSession[] {
+ return sessions.filter((s) => !isOrchestratorSession(s));
+}
+
export function sessionIsActive(session: WorkspaceSession): boolean {
return session.status !== "merged" && session.status !== "terminated";
}
@@ -144,6 +161,55 @@ export function workerStatusPulses(status: WorkerDisplayStatus): boolean {
return status === "working" || status === "needs_you";
}
+/**
+ * Kanban attention zone, ordered by human-action urgency — ported from
+ * agent-orchestrator's getAttentionLevel (packages/web/src/lib/types.ts),
+ * collapsed to its default "simple" set and rebound to reverbcode's
+ * {@link SessionStatus}. The board groups sessions into these columns so the
+ * highest-ROrI work (a one-click merge) sits leftmost.
+ */
+export type AttentionZone = "merge" | "action" | "pending" | "working" | "done";
+
+/** Columns left→right, most-urgent first. "done" is the archive column. */
+export const attentionZoneOrder: AttentionZone[] = ["merge", "action", "pending", "working", "done"];
+
+export const attentionZoneLabel: Record = {
+ merge: "Ready to merge",
+ action: "Needs you",
+ pending: "Pending",
+ working: "Working",
+ done: "Done",
+};
+
+export function attentionZone(session: WorkspaceSession): AttentionZone {
+ switch (session.status) {
+ // Terminal — archive.
+ case "merged":
+ case "terminated":
+ return "done";
+ // One click to clear — highest ROI, checked first.
+ case "approved":
+ case "mergeable":
+ return "merge";
+ // Agent waiting on a human (respond) or a problem to investigate (review);
+ // agent-orchestrator collapses these into one "action" zone by default.
+ case "needs_input":
+ case "ci_failed":
+ case "changes_requested":
+ return "action";
+ // Waiting on an external reviewer / CI — nothing to do right now.
+ case "review_pending":
+ case "pr_open":
+ case "draft":
+ return "pending";
+ // Agents doing their thing — don't interrupt.
+ case "working":
+ case "idle":
+ default:
+ return "working";
+ }
+}
+
export type WorkspaceSummary = {
id: string;
name: string;
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index d2bb8837e..45e11d243 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -17,7 +17,11 @@
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
- "noFallthroughCasesInSwitch": true
+ "noFallthroughCasesInSwitch": true,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/renderer/*"]
+ }
},
"include": [
"src/**/*.ts",
diff --git a/frontend/vite.renderer.config.ts b/frontend/vite.renderer.config.ts
index 9db36de86..d5ede3499 100644
--- a/frontend/vite.renderer.config.ts
+++ b/frontend/vite.renderer.config.ts
@@ -1,7 +1,9 @@
// defineConfig comes from vitest/config (a superset of vite's) so the `test`
-// block below typechecks; the produced config is a plain vite config object.
+// block typechecks; vitest itself must be pointed at this file explicitly
+// (package.json test script) because it only auto-discovers vite.config.*.
import { defineConfig } from "vitest/config";
import type { Plugin } from "vite";
+import { fileURLToPath, URL } from "node:url";
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
@@ -37,6 +39,12 @@ const injectCspMeta: Plugin = {
};
export default defineConfig({
+ // "@/" → the renderer root (src/renderer), the shadcn/ui import convention.
+ resolve: {
+ alias: {
+ "@": fileURLToPath(new URL("./src/renderer", import.meta.url)),
+ },
+ },
// Dev proxy for VITE_NO_ELECTRON=1 browser preview — forwards /api and /mux
// to the daemon so the renderer can be tested against a running daemon from
// a plain browser without an Electron shell.