The map of the `apps/app` React 19 + Vite UI (the single front-end every deployment loads — Electron, web, or talking to legalwork-server/opencode/Den). Its spine is a hard layering invariant: `src/app/` and `src/i18n/` are framework-agnostic and may never import from `src/react-app/`, leaf modules import nothing, `kernel/`+`infra/` sit below feature `domains/`, and `shell/` sits on top — all five rules verified by `madge --circular` reporting zero cycles. Documents the provider stack (Server → GlobalSDK → GlobalSync → Local), Zustand + TanStack Query state ownership, and the rule that the active workspace/session is read from the URL, never from app-global mutable state. Before adding or moving anything in `apps/app` — it tells you which layer a file belongs in and which imports are forbidden.
App Architecture (src/react-app/ + src/app/)
apps/app is a React 19 + Vite app. It is the UI for every LegalWork
deployment: the Electron desktop shell loads it, plain web serves it, and it
talks to legalwork-server / opencode / Den over HTTP. (The Solid runtime it
replaced is fully removed; src/index.react.tsx is the only entry.)
Layers
src/
├── app/ Framework-agnostic layer (no React imports — enforced invariant)
│ ├── lib/ Clients + bridges: opencode, legalwork-server, den, desktop (IPC),
│ │ │ analytics, app-inspector
│ │ ├── runtime-env.ts Leaf: isElectronRuntime/isDesktopRuntime
│ │ ├── desktop-types.ts Leaf: desktop IPC wire types (WorkspaceInfo = shared WorkspaceWire)
│ │ └── den-types.ts Leaf: Den wire types (den.ts re-exports)
│ ├── extensions.ts Leaf: extension manifest contract (owns ReloadReason)
│ ├── types.ts Shared app types (type-only imports of leaves)
│ ├── constants.ts, utils/ Shared constants/helpers
│ └── cloud/, session/, … Framework-free feature helpers
├── i18n/ Locales + t(); owns LANGUAGE_PREF_KEY; imports nothing from app/
└── react-app/
├── shell/ Bootstrap, providers composition, routes (session-route,
│ settings-route), command palette, menus, boot/loading states
├── kernel/ App-wide state + provider stack (server → global-sdk →
│ global-sync → local), zustand store, platform
├── infra/ React-only runtime infra (query-client, provider-list-query)
├── design-system/ Reusable presentational primitives
└── domains/ Feature-scoped code, one folder per product domain
├── session/ chat/ surface/ sync/ composer, sidebar/, panel/, terminal/,
│ voice/, artifacts/, modals/, …
├── workspace/ Create/rename/share workspace flows
├── settings/ state/ + pages/ + modals/ (settings shell)
├── connections/ MCP + provider auth UI
├── cloud/ Den sign-in and cloud surfaces
└── onboarding/ Welcome + first-run flows
Dependency rules (enforced, all verified by madge --circular: zero cycles)
src/app/andsrc/i18n/never import fromsrc/react-app/orsrc/components/. If something in the agnostic layer needs UI behavior, invert it (callback registration) or move the primitive down.- Leaf modules (
runtime-env,desktop-types,den-types,extensions) import nothing (or types-only from other leaves). Low-level clients (opencode,legalwork-server,den) import leaves — never theutils/barrel (it drags in i18n). kernel/andinfra/sit belowdomains/: they must not import domain code. Shared query/state infrastructure lives ininfra/.shell/sits on top and may import everything.- Wire contracts shared with other processes live in packages/types
(e.g.
WorkspaceWire); producer types assert assignability against them.
Toasts are rendered with sonner (@/components/ui/sonner), mounted once via
<Toaster /> in shell/providers.tsx, driven imperatively with toast().
Data flow
src/index.react.tsx React entry
└─ QueryClientProvider + PlatformProvider
└─ react-app/shell/providers.tsx (AppProviders composition)
ServerProvider
└─ GlobalSDKProvider
└─ GlobalSyncProvider
└─ LocalProvider
└─ react-app/shell/app-root.tsx → routes
├─ shell/session-route.tsx → domains/session
├─ shell/settings-route.tsx → domains/settings, connections
└─ domains/{workspace,cloud,onboarding} flows
State ownership
react-app/kernel/store.ts: app-wide Zustand store; domain selectors inkernel/selectors.ts.react-app/infra/query-client.ts: TanStack Query singleton.react-app/infra/provider-list-query.ts: shared provider-list cache used by kernel, shell, and connections.- Feature state tightly coupled to one domain lives inside that domain
(
domains/session/sync/,domains/settings/state/).
Active workspace and session
Workspace and session identity are route state, not app-global mutable state.
Canonical workspace-scoped routes:
/workspace/:workspaceId/session/workspace/:workspaceId/session/:sessionId/workspace/:workspaceId/settings/:tab/workspace/:workspaceId/settings/extensions/:section
Use react-app/shell/workspace-routes.ts to build these paths. Do not
hand-build /session/... or /settings/... URLs for workspace-scoped flows.
Rules for agents and future code:
- In session or workspace-scoped settings routes, read the active workspace
from the URL
workspaceIdparam first. - Read the active session from the URL
sessionIdparam. A selected session should never imply a different workspace than the URL workspace. - The legacy
legalwork.react.activeWorkspaceandlegalwork.react.sessionByWorkspacevalues are only restore/fallback memory. They are not authoritative while a workspace-scoped URL is active. /session,/session/:sessionId, and/settings/*are compatibility entry points. They should redirect to workspace-scoped URLs when the workspace can be resolved.- Missing URL resources should not silently fall back to the first workspace. Show a not-found state and let the user pick from the sidebar.
- Workspace-scoped actions (rename workspace, create session, open MCP/settings tabs, quick actions, commands, delete session) should use the URL-derived workspace/session context or receive explicit ids from the caller.
Practical examples:
- From session B in workspace B, opening settings navigates to
/workspace/B/settings/general. - Opening a session from the command palette navigates to
/workspace/<owner-workspace-id>/session/<session-id>, owner found from the session list. - Creating a new task in a workspace navigates to
/workspace/<workspace-id>/session/<new-session-id>.
Testing
- Unit:
bun test tests/(CI-gated). Pure logic and parsers belong here. - Smoke/e2e:
pnpm test:e2eandscripts/*.mjs(health, sessions, events). - E2E:
pnpm test:e2efrom the repo root drives the real app checks.