@hachej/boring-workspace 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +94 -0
- package/dist/CodeEditor-DQqOn4xz.js +266 -0
- package/dist/CommandPalette-aM61U-b0.js +5229 -0
- package/dist/FileTree-DRq_bfue.js +245 -0
- package/dist/MarkdownEditor-DjiHxnRv.js +349 -0
- package/dist/WorkspaceLoadingState-By0dZoPD.js +568 -0
- package/dist/agent-tool-NvxKfist.d.ts +28 -0
- package/dist/app-front.d.ts +485 -0
- package/dist/app-front.js +452 -0
- package/dist/app-server.d.ts +53 -0
- package/dist/app-server.js +769 -0
- package/dist/bootstrapServer-BRUqUpVW.d.ts +66 -0
- package/dist/boring-workspace.css +1 -0
- package/dist/charts.d.ts +114 -0
- package/dist/charts.js +143 -0
- package/dist/events.d.ts +178 -0
- package/dist/events.js +88 -0
- package/dist/explorer-DtLUnuah.d.ts +129 -0
- package/dist/panel-DnvDNQac.js +6 -0
- package/dist/server.d.ts +84 -0
- package/dist/server.js +811 -0
- package/dist/shared.d.ts +113 -0
- package/dist/shared.js +11 -0
- package/dist/testing-e2e.d.ts +68 -0
- package/dist/testing-e2e.js +45 -0
- package/dist/testing.d.ts +464 -0
- package/dist/testing.js +10984 -0
- package/dist/utils-B6yFEsav.js +8 -0
- package/dist/workspace.css +5780 -0
- package/dist/workspace.d.ts +2119 -0
- package/dist/workspace.js +1884 -0
- package/docs/INTERFACES.md +58 -0
- package/docs/PLUGIN_STRUCTURE.md +162 -0
- package/docs/README.md +19 -0
- package/docs/bridge.md +135 -0
- package/docs/panels.md +102 -0
- package/docs/plans/GENERIC_EXPLORER_PLUGIN_PLAN.md +455 -0
- package/docs/plans/MACRO_PLUGIN_GENERIC_HELPERS_AUDIT.md +962 -0
- package/docs/plans/PLUGIN_OUTPUTS_ISOLATION_PLAN.md +301 -0
- package/docs/plans/README.md +9 -0
- package/docs/plans/UI_BRIDGE_OWNERSHIP_REFACTOR.md +303 -0
- package/docs/plans/archive/CODE_OWNERSHIP_CLEANUP_PLAN.md +387 -0
- package/docs/plans/archive/COMMAND_PALETTE_REGISTRY.md +814 -0
- package/docs/plans/archive/DECLARATIVE_LAYOUT_MIGRATION.md +277 -0
- package/docs/plans/archive/PLUGIN_MODEL.md +3674 -0
- package/docs/plans/archive/SRC_FOLDER_REORG_PLAN.md +307 -0
- package/docs/plans/archive/UNIFIED_EVENT_BUS.md +647 -0
- package/docs/plans/archive/WORKSPACE_V2_PLAN.md +2489 -0
- package/docs/plugins.md +158 -0
- package/package.json +164 -0
|
@@ -0,0 +1,2489 @@
|
|
|
1
|
+
# Workspace v2 — Architecture & Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **Historical plan.** This file is kept for implementation history. Use
|
|
4
|
+
> `../../INTERFACES.md` and `../../PLUGIN_STRUCTURE.md` for the current workspace
|
|
5
|
+
> contracts and plugin ownership rules.
|
|
6
|
+
|
|
7
|
+
## Execution Tracker (2026-04-24)
|
|
8
|
+
|
|
9
|
+
Use this section as the live handoff ledger while executing this plan.
|
|
10
|
+
|
|
11
|
+
### Plugin-model merger finalization (2026-04-29)
|
|
12
|
+
|
|
13
|
+
- j9p7 Phase 1.5 completed the migration from imperative chat shell wiring to declarative layouts.
|
|
14
|
+
- Current public surface is the three-tier API in `packages/workspace/README.md`: preset layouts, declarative shell, and raw dock runtime.
|
|
15
|
+
- App-level chat/workbench integration now passes callbacks through `ChatLayout` panel params; no shell context compatibility layer remains in the package source.
|
|
16
|
+
|
|
17
|
+
### Surface resolver migration note (2026-04-30)
|
|
18
|
+
|
|
19
|
+
- File routing examples below that mention `filePatterns`, `fileFallback`, or
|
|
20
|
+
`PanelRegistry.resolve` are historical.
|
|
21
|
+
- Current source uses generic `surface-resolver` plugin outputs plus the
|
|
22
|
+
`openSurface` UI command. Filesystem path matching belongs to
|
|
23
|
+
`plugins/filesystemPlugin/surfaceResolver.ts`; data catalog row opening
|
|
24
|
+
belongs to `plugins/dataCatalogPlugin`.
|
|
25
|
+
|
|
26
|
+
### Pass 2 — Full milestone verification (2026-04-24)
|
|
27
|
+
|
|
28
|
+
- Methodology: Three parallel verification agents compared every Phase 1-4 task item against actual source files, reading implementations line-by-line.
|
|
29
|
+
- **Result: ALL Phase 1-4 tasks verified as complete.** No gaps found.
|
|
30
|
+
- Verified items:
|
|
31
|
+
- **Phase 1 (Foundation):** package.json deps ✓, 18 shadcn components vendored ✓, DockviewShell with LayoutConfig ✓, PanelChrome ✓, dockview-overrides.css ✓, Zustand persist with 2 partition keys ✓, PanelRegistry (register/get/list/has/resolve + filePatterns + lazy loading) ✓, IdeLayout (~42 lines) + ChatLayout (~45 lines) ✓
|
|
32
|
+
- **Phase 2 (Panes):** FileTree (react-arborist) ✓, MarkdownEditor (tiptap, 10 extensions) ✓, CodeEditor (CodeMirror 6) ✓, DataCatalogPane ✓, EmptyPane ✓, DataProvider + useFileContent/useFileList/useFileWrite ✓, useEditorLifecycle shared hook ✓
|
|
33
|
+
- **Phase 3a (Integration):** Bridge SSE client ✓, Bridge command dispatch via Zustand ✓, ArtifactSurfacePane with nested DockviewShell ✓, WorkspaceProvider ✓
|
|
34
|
+
- **Phase 4 (Polish):** useTheme hook ✓, CommandPalette (Cmd+P/K) ✓, ResponsiveDockviewShell with Sheet for mobile ✓, PanelErrorBoundary ✓, Testing module (@boring/workspace/testing) ✓
|
|
35
|
+
- Current assessment: Package is feature-complete per this plan. Remaining work is app-shell migration (tracked in beads).
|
|
36
|
+
|
|
37
|
+
### Pass 1 — Initial validation (2026-04-24)
|
|
38
|
+
|
|
39
|
+
- Full package verification: `pnpm --dir packages/workspace typecheck && pnpm --dir packages/workspace test` (green: 601 tests passing).
|
|
40
|
+
- Replaced placeholder lint script with real gate: `packages/workspace/package.json` now uses `"lint": "pnpm run typecheck"`.
|
|
41
|
+
- Core plan phases (foundation, panes, bridge integration, responsive behaviors, testing harness) are present and covered by the existing suite.
|
|
42
|
+
- Remaining plan execution is now mostly migration/integration work in app shells (`apps/ide`, `apps/chat`, etc.), tracked in beads rather than missing package internals.
|
|
43
|
+
|
|
44
|
+
## Vision
|
|
45
|
+
|
|
46
|
+
A clean-slate workspace package that provides **layout composition with persistence**, **shadcn-native UI**, and a **live collaboration surface** where agents and users share the same view. Agents can generate panes at runtime (dynamic `import()` from the agent workspace folder) and manipulate the UI through a shared state bridge.
|
|
47
|
+
|
|
48
|
+
## Decisions (from requirements interview)
|
|
49
|
+
|
|
50
|
+
| Decision | Choice | Notes |
|
|
51
|
+
|----------|--------|-------|
|
|
52
|
+
| Layouts | IDE + Chat-centered (presets, fully composable) | Both ship as thin config presets over DockviewShell. Every slot overridable. Apps can skip presets and compose DockviewShell directly, or skip dockview entirely and use standalone components. |
|
|
53
|
+
| Composability | 3-tier: presets → shell → standalone | Presets (IdeLayout/ChatLayout) for defaults with slot overrides. DockviewShell for custom group arrangements. Standalone components (FileTree, CodeEditor) for no-dockview apps. |
|
|
54
|
+
| Groups vs panels | Groups fixed, panels dynamic | LayoutConfig defines the group skeleton (declarative, static). Panels are dynamic content within groups (added/removed at runtime via useDockviewApi()). DockviewShell auto-manages placeholders and collapse. |
|
|
55
|
+
| Nested dockview | Supported (DockviewShell inside a pane) | ChatLayout's artifact surface renders its own DockviewShell with own state, persistence key, and API. Outer shell doesn't know inner is dockview. Follows v1's SurfaceDockview pattern. |
|
|
56
|
+
| Sizing constraints | GroupConfig only | PanelConfig has no constraints. Sizing is the group's concern, not the panel's. Clean separation: panels describe behavior, groups describe layout. |
|
|
57
|
+
| Panel engine | Dockview | Evaluated react-resizable-panels. Dockview wins: tabs, drag-drop, `toJSON`/`fromJSON`, group locking, header hiding. ~130h to replicate with resizable-panels. |
|
|
58
|
+
| Persistence | Zustand persist middleware, two keys | `boring-ui-v2:layout:{workspaceId}` (reset on schema change) + `boring-ui-v2:preferences` (theme, persists forever). Separate lifecycle. Auto-hydration on mount. `workspaceId` optional — falls back to `boring-ui-v2:layout` if not provided. |
|
|
59
|
+
| Styling | Full shadcn/tailwind | Minimal custom CSS (~150 lines dockview-overrides.css for sash/drop targets). All workspace code is tailwind-only. |
|
|
60
|
+
| Dockview styling | Wrap panels | All visible chrome is shadcn. Dockview is layout engine only. |
|
|
61
|
+
| Code editor | CodeMirror 6 (NEW, not a port) | v1 uses react-simple-code-editor + Prism. v2 upgrades to CodeMirror 6. Full rewrite of code editing, not a port. |
|
|
62
|
+
| Markdown editor | Tiptap (port from v1, reduced) | 10 extensions (down from 16). Drop Table suite (4 pkgs) + ImageResize + DiffExtension (deferred with diff mode). Replace with official Image. See Appendix J for full extension inventory. |
|
|
63
|
+
| Data layer | HTTP only, thin `DataProvider` context (React Query hooks + typed fetch) | Single HTTP provider shipped (no offline/second provider). `DataProvider` is a React context wrapper so panes can access data without threading fetch config through props — not an abstraction for swappable backends. |
|
|
64
|
+
| Panes | File tree, Markdown editor, Data catalog, Code editor | Core set. Agent pane consumed from `@boring/agent`. Each pane = component + dockview wrapper. Components reusable standalone. |
|
|
65
|
+
| Component vs Pane | Components export standalone | `FileTree` (component) usable anywhere. `FileTreePane` = FileTree in dockview. Apps like minimal import the component directly. |
|
|
66
|
+
| Layout toggle | Dropped | Apps that need IDE↔Chat toggle implement it themselves. Not workspace's job. |
|
|
67
|
+
| Offline mode | Dropped | agent-frontend's LightningFS/isomorphic-git stack not supported. HTTP-only. |
|
|
68
|
+
| Dynamic panes | Out of v2 scope | Post-launch feature — see Appendix I below. Not blocking v2 delivery. |
|
|
69
|
+
| Agent bridge | Read-only state + imperative commands | Agent reads Zustand store (reactive subscriptions). Writes go through typed bridge commands (openFile, openPanel, etc.) — NOT direct store mutations. See architecture below. |
|
|
70
|
+
| Boot boundary | Workspace = layout + context provider | App shell handles auth, routing, data fetching. Workspace provides `<WorkspaceProvider>` (registry + bridge + theme). Data provider injected by app shell. |
|
|
71
|
+
| Repo structure | v2 monorepo | `boring-ui-v2/packages/workspace`, `boring-ui-v2/packages/agent`, `boring-ui-v2/packages/core` |
|
|
72
|
+
| Capability gating | Hybrid | Static registration + optional runtime check for dynamic panes. |
|
|
73
|
+
| Pane lazy-loading | All panes lazy-loaded | Every built-in pane (including FileTree) loaded via dynamic import(). Only `empty` is eager. Initial bundle = shell + registry only. |
|
|
74
|
+
| Dynamic pane transform | Out of v2 scope | See Appendix I. Decision: server-side esbuild when implemented. |
|
|
75
|
+
| Migration | Hard cut | v2 replaces v1 when ready. No coexistence period. |
|
|
76
|
+
| Layout hooks | Shared from Phase 1 | Extract useSidebarLayout/usePanelSizing before building either layout. |
|
|
77
|
+
| Hydration strategy | Block mount | Don't render DockviewShell until Zustand onRehydrateStorage fires. Clean loading state. |
|
|
78
|
+
| Testing | 4-layer (unit + Storybook + Playwright + Bombadil) | Property-based exploration via Bombadil for unknown unknowns. |
|
|
79
|
+
| Language | TypeScript (.tsx) from day 1 | v1 is 100% .jsx with no types. v2 is clean-slate .tsx. Not a migration — new files with typed props. |
|
|
80
|
+
| React version | React 19 safe | dockview 4.13.1 already supports React 19. All deps compatible. |
|
|
81
|
+
| Git UI in workspace | Dropped entirely | No git status badges, no git diff viewer. Agent owns all git UI. |
|
|
82
|
+
| File tree sections | Dropped | Flat tree. No Projects/Sources sections. |
|
|
83
|
+
| Editor modes | Normal only in v1 | Drop git-diff modes entirely (side-by-side vs HEAD + unified). Git UI dropped, diff modes return when git routes ship in v1.x. |
|
|
84
|
+
| Backend | **None — workspace is frontend-only** | All HTTP routes (files, tree, stat, ui-bridge, agent) are hosted by `@boring/agent`. Workspace consumes them. See `boring-ui-v2/packages/agent/docs/plans/agent-package-spec.md` for the full HTTP surface. |
|
|
85
|
+
| ConnectedFileTree | Dropped (seventh pass) | Redundant. Keep FileTree (props) + FileTreePane (dockview wrapper). Pane IS the connected variant. |
|
|
86
|
+
| v1 REST bridge | Dropped (seventh pass) | Hard cut. No backward-compat REST endpoints. SSE + POST only (ninth pass). |
|
|
87
|
+
| Error recovery | 2-tier (seventh pass, clarified tenth) | Tier 1: error boundary per pane (includes stripping unknown panels during restore). Tier 2: full reset to defaults. The "strip unknown panels" step is part of Tier 1 restore, not a separate tier. |
|
|
88
|
+
| Bridge commands | Return Promise\<CommandResult\> (seventh pass, updated eighth) | openFile(), openPanel(), etc. return `Promise<CommandResult>` with seq, status, and optional error. Fire-and-forget still works. |
|
|
89
|
+
| Implementation ref | Added in sixth pass | API surface, WorkspaceProvider, core deps, bridge protocol, dockview config, shadcn inventory, phase DAG — see §Implementation Reference below Risk 14. |
|
|
90
|
+
| Bridge transport | SSE + POST, command-based (agent-hosted, tenth pass) | Workspace Zustand is the authority. Agent posts commands via `POST /api/v1/ui/commands`; SSE `GET /api/v1/ui/commands/next` streams `event: command` to workspace; workspace executes locally; workspace pushes state via `PUT /api/v1/ui/state`; agent reads via `get_ui_state` tool. All events carry `v:1` protocol version. Short-poll (2s) fallback. |
|
|
91
|
+
| Bridge validation | Per-kind Zod schemas (eighth pass, simplified ninth) | Zod validation per command kind (path length, allowed chars, registry existence). Rate limiting deferred to post-launch. |
|
|
92
|
+
| Bridge events | Typed BridgeEventMap + select() (eighth pass) | Discriminated union replaces `subscribe(event: string, handler: Function)`. Selector-based `select<T>(selector, handler)` for slice subscriptions. Apps extend via module augmentation. |
|
|
93
|
+
| Bridge state scope | Panels + lightweight file hints (eighth pass) | Bridge sends openPanels, activePanel, activeFile, visible file paths. NO full file tree state — agent queries files via its own tools. Eliminates bandwidth problem. |
|
|
94
|
+
| Shell API | Extended (eighth pass) | Add `activatePanel()`, `updatePanelParams()`, `movePanel()`, `batch()` to DockviewShellApi. Batch defers layout recalculation for multi-op sequences. |
|
|
95
|
+
| Panel lifecycle | Four hooks via PanelLifecycleApi (eighth pass, tenth) | PanelConfig gains `onActivate`, `onDeactivate`, `onClose` (returns Promise to block close), `serializeState`. Hooks receive workspace-owned `PanelLifecycleApi` (not raw DockviewPanelApi) to maintain dockview encapsulation. |
|
|
96
|
+
| Cascading errors | Dropped (ninth pass) | Individual pane error boundaries + bridge reconnection banner are sufficient for launch. WorkspaceHealthMonitor deferred to post-launch if correlated failures become a real problem. |
|
|
97
|
+
| Dirty files | Store-level slice (eighth pass) | `dirtyFiles: Map<path, { panelId, savedAt }>` in Zustand store. Editors call `bridge.markDirty/markClean`. Agent gets `bridge.getDirtyFiles()`. |
|
|
98
|
+
| Persistence hardening | Full (eighth pass) | Zod schema validation on restore, QuotaExceededError handling (disable + toast), cross-tab `storage` event listener, size budget <50KB. |
|
|
99
|
+
| Persist debounce | 300ms + flush (eighth pass) | `onDidLayoutChange` fires per-pixel during sash drags. Debounce toJSON/persist to 300ms. Flush on `beforeunload`. |
|
|
100
|
+
| File tree library | react-arborist (eighth pass) | ~15KB gz. Virtualized tree with keyboard nav, ARIA semantics, drag-and-drop. Replaces react-window + 300-500 LOC custom tree logic. |
|
|
101
|
+
| CM6 large files | Simple cutoff at 1MB (eighth pass) | Full highlighting <1MB, read-only above 1MB. No worker tokenization (doesn't exist in CM6). |
|
|
102
|
+
| Bundle budget | Two numbers (ninth pass) | Initial bundle (shell + registry) <150KB gz, total all-loaded <800KB gz. All panes lazy-loaded except `empty`. |
|
|
103
|
+
| Pane loading | All lazy (eighth pass) | Every built-in pane is lazy-loaded via dynamic import(). Initial bundle = dockview + shadcn + zustand + registry only. ChatLayout apps never download FileTree/editors. |
|
|
104
|
+
| Zustand selectors | Architectural (ninth pass) | `useWorkspaceStore()` is NOT exported. Only atomic hooks exported: `useActiveFile()`, `useActivePanel()`, `useSidebarState()`, `useOpenPanels()`, `useDirtyFiles()`. Selector discipline enforced by API design, not ESLint. |
|
|
105
|
+
| Nested shell isolation | Minimal + allowedPanels (ninth pass, tenth) | Each nested DockviewShell gets own `storageKey` + optional `allowedPanels` prop to filter registry. No panel ID namespacing, no bridge `shell` param. Add formal isolation protocol post-launch if multi-shell routing is needed. |
|
|
106
|
+
| CSP | Target policy + test (eighth pass) | `style-src 'unsafe-inline'` for CM6 style-mod, tiptap HTML sanitizer configured, Playwright CSP test. |
|
|
107
|
+
| definePanel() | Dropped (ninth pass) | Unnecessary — a typed object literal (`const myPanel: PanelConfig = { ... }`) gives the same autocomplete. Less API surface. |
|
|
108
|
+
| Test harness | @boring/workspace/testing (eighth pass) | TestWorkspaceProvider, createMockBridge(), renderPane(). Child apps run full test suite themselves. |
|
|
109
|
+
| Agent tool-selection regressions | `@boring/agent/testing` eval framework | YAML fixtures + matcher pipeline + live-LLM canary. Workspace and child apps write their own driver scripts that boot `createWorkspaceAgentApp` and call `runEvalSuite({ app, fixturesPath })`. See `packages/agent/docs/plans/AGENT_EVAL_FRAMEWORK.md`. |
|
|
110
|
+
| Crash recovery | Accepted risk (eighth pass) | 1s auto-save debounce gap on crash accepted. No sessionStorage journal — complexity not worth it. |
|
|
111
|
+
| Command palette | Static + file quick-open, separate CommandRegistry (eighth pass, tenth) | `registry/CommandRegistry.ts` with `CommandConfig { id, title, run, shortcut?, when? }`. Exported via `useCommandRegistry()`. Separate from PanelRegistry. Dynamic command providers deferred to post-launch. |
|
|
112
|
+
| i18n | Dropped (ninth pass) | No `t()` wrapper, no `yarn extract-messages`. Plain strings. When i18n is needed, run a codemod (ast-grep/jscodeshift) to extract. |
|
|
113
|
+
| Editor lifecycle | Shared hook with flushSave (ninth pass, tenth) | `useEditorLifecycle()` hook (~100 LOC) for Tiptap and CM6: dirty tracking, auto-save debounce, external file change detection, bridge markDirty/markClean. Exposes `flushSave()` for onClose integration: onClose calls flushSave() first, then prompts if still dirty. Single save path. |
|
|
114
|
+
| Git sidebar | Dropped (ninth pass) | No git changes view in sidebar. Agent owns all git UI. File tree is files-only. |
|
|
115
|
+
| Offline mode | Permanently dropped (ninth pass) | No DataProvider abstraction, no LightningFS, no isomorphic-git, no Pyodide. HTTP-only is permanent. agent-frontend app cannot exist on v2. |
|
|
116
|
+
| Terminal/PTY | Not used (ninth pass) | Terminal panels are no longer used in any app. Remove from migration concerns and "files not to port" rationale. |
|
|
117
|
+
| Dockview risk | Accepted (ninth pass) | Single-maintainer risk accepted. DockviewShell encapsulates all dockview interaction — panes never import dockview directly. If dockview dies, only DockviewShell internals (~500 LOC) change. No adapter layer (YAGNI). |
|
|
118
|
+
| Sample app | Minimal playground (ninth pass) | `apps/workspace-playground`: IdeLayout + mock data provider, no backend needed. `pnpm --filter workspace-playground dev`. Isolated test environment for workspace package. |
|
|
119
|
+
| Bridge E2E test | Added (ninth pass) | Playwright test that opens workspace + simulates agent HTTP client. Agent posts openFile command via POST → workspace receives on SSE `event: command` → applies locally → assert file panel appears. Tests real bridge protocol. ~50 LOC. |
|
|
120
|
+
| CM6 languages | Measure first (ninth pass) | Bundle all 6 languages (JS/TS, Python, JSON, YAML, Markdown, SQL). Measure actual chunk size in Phase 2. Cut to 3 bundled + lazy rest only if total budget exceeded. |
|
|
121
|
+
| Store topology | Single store, partitioned persist (tenth pass) | One `useWorkspaceStore()`. Persist middleware uses `partialize` to route: layout→localStorage, preferences→localStorage, bridge state→ephemeral (NOT persisted). Single file `store/index.ts`. `persistence/` and `bridge/` are thin modules reading/writing the single store. |
|
|
122
|
+
| Bridge authority | Workspace Zustand is source of truth (tenth pass) | SSE streams commands (not state). Workspace executes locally, PUTs resulting state. Agent reads via `get_ui_state` tool. `causedBy: 'user' \| 'agent' \| 'restore'` on PUT body prevents echo loops. All events carry `v:1` protocol version field. |
|
|
123
|
+
| PanelLifecycleApi | Workspace-owned wrapper (tenth pass) | Lifecycle hooks (`onActivate`, `onDeactivate`, `onClose`) receive `PanelLifecycleApi { panelId, title, setTitle, close, focus, isActive }` — not raw `DockviewPanelApi`. PanelChrome adapts. Maintains dockview encapsulation. |
|
|
124
|
+
| Layout version | String + migration callback (tenth pass) | `version: '2.0'` (string, not number). `WorkspaceProviderProps.onLayoutVersionMismatch?(persisted, current, layout) => SerializedLayout \| null`. Default: null (reset). Zero cost now, clean extension point for future layout migrations. |
|
|
125
|
+
| Standalone contract | Props-only, zero context (tenth pass) | Tier 3 standalone components (`FileTree`, `CodeEditor`, `MarkdownEditor`) accept ALL data via props. Never call hooks internally. `CodeEditor` takes `content: string, onChange, language`. DataProvider only needed by pane wrappers. |
|
|
126
|
+
| Storage scoping | workspaceId in key (tenth pass) | Default key: `boring-ui-v2:layout:{workspaceId}`. Falls back to `boring-ui-v2:layout` if workspaceId not provided. Prevents data loss when multi-workspace arrives. |
|
|
127
|
+
| Nested shell guard | allowedPanels prop (tenth pass) | DockviewShell accepts optional `allowedPanels: string[]` to filter registry for nested instances. ChatLayout artifact surface uses this to prevent outer-shell panels from rendering inside. Outer shell has no restriction. |
|
|
128
|
+
|
|
129
|
+
## Architecture
|
|
130
|
+
|
|
131
|
+
### Package boundary
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
v2/packages/workspace/ # This package
|
|
135
|
+
src/
|
|
136
|
+
components/ # shadcn-wrapped building blocks
|
|
137
|
+
layouts/ # IDE, Chat-centered, layout primitives
|
|
138
|
+
panes/ # Built-in pane implementations
|
|
139
|
+
panels/ # Dockview panel wrappers (shadcn chrome)
|
|
140
|
+
persistence/ # Single-key localStorage engine
|
|
141
|
+
registry/ # Panel registry + dynamic loader
|
|
142
|
+
bridge/ # Agent-UI shared state (the coworking surface)
|
|
143
|
+
hooks/ # Layout, persistence, panel lifecycle hooks
|
|
144
|
+
theme/ # shadcn theme config + dockview overrides
|
|
145
|
+
index.ts # Public API
|
|
146
|
+
docs/
|
|
147
|
+
plans/ # This file
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Dependency graph
|
|
151
|
+
|
|
152
|
+
**Updated 2026-04-24 — fully inverted from v1.** Agent is the leaf. Workspace depends on agent (consumes `<ChatPanel />` as a pane). Core depends on workspace. Canonical graph lives in `packages/core/docs/CORE.md` §Dependency position (single fused doc replacing the old plans/ subfolder).
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
@boring/agent (leaf — ZERO internal deps; ships standalone CLI)
|
|
156
|
+
|
|
157
|
+
@boring/workspace (depends on agent)
|
|
158
|
+
├── @boring/agent (ChatPanel component + useAgentChat hook)
|
|
159
|
+
├── dockview-react (layout engine)
|
|
160
|
+
├── @codemirror/* (code editor)
|
|
161
|
+
├── shadcn/ui components (vendored in ui-shadcn/ — consumed by core)
|
|
162
|
+
└── tailwindcss (styling)
|
|
163
|
+
|
|
164
|
+
@boring/core (depends on workspace)
|
|
165
|
+
└── @boring/workspace (imports shadcn primitives from /ui-shadcn)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Standalone agent (`createAgentApp()` / `npx @boring/agent`) keeps **zero dependency on core**; the CLI must boot without DB/auth. Core apps mount agent via `registerAgentRoutes(app)` Fastify plugin — additive, not mandatory.
|
|
169
|
+
|
|
170
|
+
**Historical note**: sections below that mention `@boring/core` as a workspace dep (transport utils, config provider, hooks like `useTheme`/`useKeyboardShortcuts`) predate the inversion. In v2 these live in core. Sections that say "workspace never imports agent" are also outdated — workspace now consumes agent's `<ChatPanel />`. Read pre-2026-04-24 sections as v1 history, not v2 contract.
|
|
171
|
+
|
|
172
|
+
### Composability model
|
|
173
|
+
|
|
174
|
+
The workspace is a library, not a framework. Three tiers of usage — pick the one that
|
|
175
|
+
matches your app's needs. Each tier is a superset of the one below.
|
|
176
|
+
|
|
177
|
+
#### Tier 1: Preset layouts (slot overrides)
|
|
178
|
+
|
|
179
|
+
Preset layouts define a group arrangement with sensible defaults. Every slot is overridable
|
|
180
|
+
by panel ID. The preset is ~30 lines — it builds a `LayoutConfig` and passes it to `DockviewShell`.
|
|
181
|
+
|
|
182
|
+
```tsx
|
|
183
|
+
import { WorkspaceProvider, IdeLayout } from '@boring/workspace'
|
|
184
|
+
import { ChatPanel } from '@boring/agent'
|
|
185
|
+
import { MyCustomTree } from './MyCustomTree'
|
|
186
|
+
|
|
187
|
+
// Register custom panel, then reference by ID in layout slot
|
|
188
|
+
<WorkspaceProvider
|
|
189
|
+
panels={[
|
|
190
|
+
{ id: 'my-tree', component: MyCustomTree, title: 'Explorer' },
|
|
191
|
+
{ id: 'agent', component: ChatPanel, placement: 'right', hideHeader: true },
|
|
192
|
+
]}
|
|
193
|
+
>
|
|
194
|
+
<IdeLayout
|
|
195
|
+
sidebar="my-tree" // override: your component instead of built-in FileTree
|
|
196
|
+
center="empty" // default
|
|
197
|
+
right="agent" // default
|
|
198
|
+
/>
|
|
199
|
+
</WorkspaceProvider>
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**IdeLayout defaults**: `{ sidebar: 'filetree', center: 'empty', right: null }`
|
|
203
|
+
**ChatLayout defaults**: `{ nav: 'session-list', center: 'chat', sidebar: undefined, surface: undefined }`
|
|
204
|
+
|
|
205
|
+
#### Tier 2: DockviewShell (custom group arrangement)
|
|
206
|
+
|
|
207
|
+
Skip presets entirely. Define your own groups, positions, constraints.
|
|
208
|
+
|
|
209
|
+
```tsx
|
|
210
|
+
import { WorkspaceProvider, DockviewShell } from '@boring/workspace'
|
|
211
|
+
|
|
212
|
+
<WorkspaceProvider panels={[...]}>
|
|
213
|
+
<DockviewShell
|
|
214
|
+
layout={{
|
|
215
|
+
groups: [
|
|
216
|
+
{ id: 'left', position: 'left', locked: true, panel: 'filetree', constraints: { minWidth: 200 } },
|
|
217
|
+
{ id: 'center', position: 'center', panel: 'empty' },
|
|
218
|
+
{ id: 'bottom', position: 'bottom', panel: 'terminal', constraints: { maxHeight: 300 } },
|
|
219
|
+
{ id: 'right', position: 'right', panel: 'agent', hideHeader: true },
|
|
220
|
+
]
|
|
221
|
+
}}
|
|
222
|
+
/>
|
|
223
|
+
</WorkspaceProvider>
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
#### Tier 3: Standalone components (no dockview)
|
|
227
|
+
|
|
228
|
+
No layout engine at all. Import components and compose them in plain JSX.
|
|
229
|
+
No `WorkspaceProvider`, no `DataProvider`, no context — all data via props (tenth pass).
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
import { FileTree, CodeEditor } from '@boring/workspace'
|
|
233
|
+
|
|
234
|
+
<div className="flex h-screen">
|
|
235
|
+
<FileTree files={files} onSelect={setPath} />
|
|
236
|
+
<CodeEditor content={fileContent} onChange={setContent} language="typescript" />
|
|
237
|
+
</div>
|
|
238
|
+
// DataProvider is only needed by pane wrappers (CodeEditorPane, FileTreePane).
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
#### Core principle: groups are fixed, panels are dynamic
|
|
242
|
+
|
|
243
|
+
**Groups** (the layout skeleton) are declarative and fixed — defined in LayoutConfig.
|
|
244
|
+
**Panels** (the content) are dynamic — they come and go at runtime.
|
|
245
|
+
|
|
246
|
+
LayoutConfig describes "the room has 3 zones." Runtime API handles "put this file on the desk."
|
|
247
|
+
|
|
248
|
+
DockviewShell auto-manages:
|
|
249
|
+
- **Placeholders**: if a dynamic group has no panels, the placeholder panel shows automatically
|
|
250
|
+
- **Collapse**: collapsible groups shrink to `collapsedWidth` when collapsed, restore on expand
|
|
251
|
+
- **Panel count**: dynamic groups accept new panels via `useDockviewApi().addPanel()`
|
|
252
|
+
|
|
253
|
+
#### Layout config type
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
interface LayoutConfig {
|
|
257
|
+
version: string // e.g. '2.0'. Increment when group structure changes.
|
|
258
|
+
// On mismatch: call WorkspaceProviderProps.onLayoutVersionMismatch()
|
|
259
|
+
// (default: discard persisted layout, use new config)
|
|
260
|
+
groups: GroupConfig[]
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
interface GroupConfig {
|
|
264
|
+
id: string // unique group identifier
|
|
265
|
+
position: 'left' | 'center' | 'right' | 'bottom'
|
|
266
|
+
panel?: string // initial panel ID (from registry)
|
|
267
|
+
locked?: boolean // prevent closing/dragging out
|
|
268
|
+
hideHeader?: boolean // hide tab bar (for single-panel groups)
|
|
269
|
+
dynamic?: boolean // accepts panels at runtime (default: false)
|
|
270
|
+
placeholder?: string // panel shown when group is empty (requires dynamic: true)
|
|
271
|
+
collapsible?: boolean // can be collapsed (sidebar pattern)
|
|
272
|
+
collapsedWidth?: number // width when collapsed (e.g., 80 for icon-only sidebar)
|
|
273
|
+
constraints?: { // sizing — ONLY GroupConfig has constraints (not PanelConfig)
|
|
274
|
+
minWidth?: number
|
|
275
|
+
maxWidth?: number
|
|
276
|
+
minHeight?: number
|
|
277
|
+
maxHeight?: number
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**Only GroupConfig has sizing constraints.** PanelConfig describes behavior (icon, title,
|
|
283
|
+
filePatterns). GroupConfig describes layout (size, locked, collapsible). Clean separation —
|
|
284
|
+
panels don't dictate their container size.
|
|
285
|
+
|
|
286
|
+
#### Runtime API — `useDockviewApi()`
|
|
287
|
+
|
|
288
|
+
Exposed by DockviewShell for panes and hooks that need to manipulate the layout at runtime.
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
interface DockviewShellApi {
|
|
292
|
+
addPanel(groupId: string, config: { id: string, component: string, params?: Record<string, unknown> }): void
|
|
293
|
+
removePanel(panelId: string): void
|
|
294
|
+
activatePanel(panelId: string): void
|
|
295
|
+
updatePanelParams(panelId: string, params: Record<string, unknown>): void
|
|
296
|
+
movePanel(panelId: string, target: { groupId: string } | { direction: 'left' | 'right' | 'up' | 'down', referencePanelId: string }): void
|
|
297
|
+
getGroup(id: string): DockviewGroupApi | null
|
|
298
|
+
getActivePanel(): string | null
|
|
299
|
+
setGroupCollapsed(groupId: string, collapsed: boolean): void
|
|
300
|
+
|
|
301
|
+
/** Batch multiple operations into a single layout recalculation + persistence write.
|
|
302
|
+
* Defers onDidLayoutChange until the callback completes. */
|
|
303
|
+
batch(fn: () => void): void
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Panes access this via `useDockviewApi()` hook (available inside WorkspaceProvider).
|
|
308
|
+
Bridge commands (`openFile`, `openPanel`) use this internally.
|
|
309
|
+
|
|
310
|
+
Preset layouts are just functions that return a `LayoutConfig`:
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
// layouts/IdeLayout.tsx — ~30 lines, not 200+
|
|
314
|
+
function IdeLayout({ sidebar = 'filetree', center = 'empty', right }: IdeLayoutProps) {
|
|
315
|
+
const layout: LayoutConfig = {
|
|
316
|
+
groups: [
|
|
317
|
+
{ id: 'sidebar', position: 'left', panel: sidebar, locked: true,
|
|
318
|
+
collapsible: true, collapsedWidth: 0,
|
|
319
|
+
constraints: { minWidth: 200, maxWidth: 400 } },
|
|
320
|
+
{ id: 'center', position: 'center', panel: center,
|
|
321
|
+
dynamic: true, placeholder: 'empty' },
|
|
322
|
+
...(right ? [{ id: 'right', position: 'right' as const, panel: right, hideHeader: true,
|
|
323
|
+
constraints: { minWidth: 300 } }] : []),
|
|
324
|
+
]
|
|
325
|
+
}
|
|
326
|
+
return <DockviewShell layout={layout} />
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
#### PanelConfig — the extension contract
|
|
331
|
+
|
|
332
|
+
Every panel registered in the workspace follows this contract. This is what a developer
|
|
333
|
+
implements to add a new pane.
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
interface PanelConfig {
|
|
337
|
+
id: string // unique panel identifier
|
|
338
|
+
component: React.ComponentType<PaneProps> | (() => Promise<{ default: React.ComponentType<PaneProps> }>)
|
|
339
|
+
title?: string // display name in tab
|
|
340
|
+
icon?: React.ComponentType<{ size: number }> // tab icon (lucide-react compatible)
|
|
341
|
+
placement?: 'left' | 'center' | 'right' | 'bottom' // preferred initial position
|
|
342
|
+
essential?: boolean // cannot be closed by user
|
|
343
|
+
hideHeader?: boolean // hide tab bar when sole panel in group
|
|
344
|
+
requiresCapabilities?: string[] // only available when capabilities satisfied
|
|
345
|
+
filePatterns?: string[] // file globs this pane handles (e.g., ['*.csv', '*.parquet'])
|
|
346
|
+
|
|
347
|
+
// Lifecycle hooks — called by DockviewShell, run outside React render cycle.
|
|
348
|
+
// PanelChrome wires these automatically; pane components don't subscribe manually.
|
|
349
|
+
// Hooks receive PanelLifecycleApi (workspace-owned type), NOT raw DockviewPanelApi.
|
|
350
|
+
// This maintains dockview encapsulation — panel authors never import dockview types.
|
|
351
|
+
onActivate?: (api: PanelLifecycleApi) => void
|
|
352
|
+
onDeactivate?: (api: PanelLifecycleApi) => void
|
|
353
|
+
onClose?: (api: PanelLifecycleApi) => void | Promise<void> // return Promise to block close (e.g., call flushSave() then prompt if still dirty)
|
|
354
|
+
|
|
355
|
+
// State serialization for persistence. DockviewShell calls serializeState() during
|
|
356
|
+
// toJSON() and passes the result back via params.__restoredState on fromJSON().
|
|
357
|
+
// Enables panels to persist scroll position, cursor, expansion state across sessions.
|
|
358
|
+
// Max 4KB serialized per panel — PanelChrome enforces and logs warning if exceeded.
|
|
359
|
+
serializeState?: (panelId: string) => Record<string, JsonSerializable> | null
|
|
360
|
+
|
|
361
|
+
// NOTE: No constraints here. Sizing is GroupConfig's concern, not the panel's.
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Workspace-owned panel lifecycle API — wraps DockviewPanelApi.
|
|
365
|
+
// PanelChrome adapts dockview's API to this type before calling lifecycle hooks.
|
|
366
|
+
// Panel authors never import from dockview-react directly.
|
|
367
|
+
interface PanelLifecycleApi {
|
|
368
|
+
panelId: string
|
|
369
|
+
title: string
|
|
370
|
+
setTitle(title: string): void
|
|
371
|
+
close(): void
|
|
372
|
+
focus(): void
|
|
373
|
+
isActive: boolean
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Props injected into every pane component by the dockview wrapper
|
|
377
|
+
interface PaneProps {
|
|
378
|
+
panelId: string // dockview panel ID
|
|
379
|
+
params: Record<string, unknown> // params passed when opening (e.g., { path: '/src/main.ts' })
|
|
380
|
+
api: PanelLifecycleApi // workspace-owned panel API (not raw DockviewPanelApi)
|
|
381
|
+
bridge: WorkspaceBridge // workspace bridge for agent interaction
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
#### Panel authoring — typed object literals
|
|
386
|
+
|
|
387
|
+
No factory function needed. A typed object literal gives full TypeScript autocomplete:
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
import type { PanelConfig } from '@boring/workspace'
|
|
391
|
+
|
|
392
|
+
export const csvViewer: PanelConfig = {
|
|
393
|
+
id: 'csv-viewer',
|
|
394
|
+
component: () => import('./CsvViewer'), // lazy by default
|
|
395
|
+
title: 'CSV Viewer',
|
|
396
|
+
icon: TableIcon,
|
|
397
|
+
filePatterns: ['*.csv', '*.tsv'],
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Then in app shell:
|
|
401
|
+
<WorkspaceProvider panels={[csvViewer, imageViewer, agentPane]}>
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**File-type routing**: when a file is opened, the registry checks `filePatterns` on all
|
|
405
|
+
registered panels. First match wins. Built-in defaults:
|
|
406
|
+
|
|
407
|
+
| Pattern | Panel |
|
|
408
|
+
|---------|-------|
|
|
409
|
+
| `*.md`, `*.mdx` | `markdown-editor` |
|
|
410
|
+
| `*` (fallback) | `code-editor` |
|
|
411
|
+
|
|
412
|
+
Apps can override by registering a panel with more specific patterns:
|
|
413
|
+
|
|
414
|
+
```tsx
|
|
415
|
+
<WorkspaceProvider
|
|
416
|
+
panels={[
|
|
417
|
+
{ id: 'csv-viewer', component: CsvViewer, filePatterns: ['*.csv', '*.tsv'] },
|
|
418
|
+
{ id: 'image-viewer', component: ImageViewer, filePatterns: ['*.png', '*.jpg', '*.svg'] },
|
|
419
|
+
]}
|
|
420
|
+
/>
|
|
421
|
+
// Now opening a .csv file routes to CsvViewer instead of code-editor
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Agent-UI bridge (the "coworking surface")
|
|
425
|
+
|
|
426
|
+
**Recommended approach: shared reactive state store.**
|
|
427
|
+
|
|
428
|
+
The agent should feel like a co-user — seeing the same files, same open panels, same state. This means:
|
|
429
|
+
|
|
430
|
+
1. **Workspace state store** (Zustand) holds the canonical UI state:
|
|
431
|
+
- Open panels and their params (which file, which view mode)
|
|
432
|
+
- Active panel / active file
|
|
433
|
+
- Sidebar collapse state
|
|
434
|
+
- Dirty files map (which files have unsaved changes)
|
|
435
|
+
- Notifications / toasts
|
|
436
|
+
|
|
437
|
+
2. **Bridge API** — a thin command layer the agent calls:
|
|
438
|
+
```typescript
|
|
439
|
+
interface WorkspaceBridge {
|
|
440
|
+
// Read (agent sees what user sees)
|
|
441
|
+
getOpenPanels(): PanelState[]
|
|
442
|
+
getActiveFile(): string | null
|
|
443
|
+
getDirtyFiles(): string[] // files with unsaved changes
|
|
444
|
+
getVisibleFiles(): string[] // file paths currently visible in the tree
|
|
445
|
+
|
|
446
|
+
// Write (agent acts like a user) — all return Promise<CommandResult>
|
|
447
|
+
openFile(path: string, opts?: { mode?: 'view' | 'edit' | 'diff' }): Promise<CommandResult>
|
|
448
|
+
openPanel(config: DynamicPaneConfig): Promise<CommandResult>
|
|
449
|
+
closePanel(id: string): Promise<CommandResult>
|
|
450
|
+
expandToFile(path: string): Promise<CommandResult>
|
|
451
|
+
showNotification(msg: string, level?: 'info' | 'warn' | 'error'): Promise<CommandResult>
|
|
452
|
+
navigateToLine(file: string, line: number): Promise<CommandResult>
|
|
453
|
+
markDirty(path: string): void
|
|
454
|
+
markClean(path: string): void
|
|
455
|
+
|
|
456
|
+
// Subscribe — typed events with discriminated union
|
|
457
|
+
subscribe<K extends keyof BridgeEventMap>(
|
|
458
|
+
event: K,
|
|
459
|
+
handler: (data: BridgeEventMap[K]) => void
|
|
460
|
+
): Unsubscribe
|
|
461
|
+
|
|
462
|
+
// Selector-based subscription (fires only when selected slice changes)
|
|
463
|
+
select<T>(selector: (state: WorkspaceState) => T, handler: (value: T) => void): Unsubscribe
|
|
464
|
+
|
|
465
|
+
// Connectivity
|
|
466
|
+
onDisconnect(handler: () => void): Unsubscribe
|
|
467
|
+
onReconnect(handler: () => void): Unsubscribe
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
interface CommandResult {
|
|
471
|
+
seq: number
|
|
472
|
+
status: 'ok' | 'error'
|
|
473
|
+
error?: { code: string, message: string }
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Typed bridge events — exhaustive, no stringly typing
|
|
477
|
+
interface BridgeEventMap {
|
|
478
|
+
'panel:opened': { panelId: string; params: Record<string, unknown> }
|
|
479
|
+
'panel:closed': { panelId: string }
|
|
480
|
+
'panel:activated': { panelId: string; previousPanelId: string | null }
|
|
481
|
+
'file:opened': { path: string; mode: 'view' | 'edit' | 'diff' }
|
|
482
|
+
'file:saved': { path: string }
|
|
483
|
+
'file:dirty': { path: string; dirty: boolean }
|
|
484
|
+
'sidebar:toggled': { collapsed: boolean }
|
|
485
|
+
'notification:shown': { message: string; level: 'info' | 'warn' | 'error' }
|
|
486
|
+
'pane:error': { panelId: string; error: string; stack?: string }
|
|
487
|
+
}
|
|
488
|
+
// Apps extend via module augmentation:
|
|
489
|
+
// declare module '@boring/workspace' {
|
|
490
|
+
// interface BridgeEventMap { 'myapp:custom': { payload: string } }
|
|
491
|
+
// }
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
**Bridge state scope**: The bridge sends `openPanels`, `activePanel`, `activeFile`,
|
|
495
|
+
and `visibleFiles` (paths currently shown in the file tree). It does NOT send the full
|
|
496
|
+
file tree state — the agent queries files via its own filesystem tools. This eliminates
|
|
497
|
+
the bandwidth problem of broadcasting 10K+ file entries on every panel change.
|
|
498
|
+
|
|
499
|
+
3. **Transport**: **SSE + POST, command-based** (tenth pass). Workspace Zustand store is the authority. Agent posts commands via `POST /api/v1/ui/commands` → agent server validates and queues → SSE `GET /api/v1/ui/commands/next` delivers `event: command` to workspace → workspace executes locally → workspace `PUT /api/v1/ui/state` with `causedBy` field → agent reads via `get_ui_state` tool. All events carry `v:1` protocol version. Short-poll fallback (2s) for environments where SSE is unavailable. For browser-side agents (iframe/worker), **postMessage**. The bridge abstraction hides the transport.
|
|
500
|
+
|
|
501
|
+
4. **Server endpoint**: all UI bridge endpoints are hosted by `@boring/agent` (not workspace). Workspace is a client of `PUT /api/v1/ui/state` + SSE `GET /api/v1/ui/commands/next`. See `boring-ui-v2/packages/agent/docs/plans/agent-package-spec.md`.
|
|
502
|
+
|
|
503
|
+
### Server-side ownership
|
|
504
|
+
|
|
505
|
+
**Workspace v2 is frontend-only — no server code, no routes.** All backend routes are hosted by `@boring/agent`; workspace consumes them over HTTP.
|
|
506
|
+
|
|
507
|
+
| Routes | Owner | Notes |
|
|
508
|
+
|--------|-------|-------|
|
|
509
|
+
| `/api/v1/files` (GET/POST/DELETE) | **Agent** | Read/write/delete. Workspace consumes from file-tree + editor. |
|
|
510
|
+
| `/api/v1/tree` (GET) | **Agent** | Directory listing (lazy, per-dir). |
|
|
511
|
+
| `/api/v1/files/search` (GET) | **Agent** | Filename / glob search. Consumed by file-tree search input. |
|
|
512
|
+
| `/api/v1/stat` (GET) | **Agent** | File metadata. |
|
|
513
|
+
| `/api/v1/ui/state` (GET/PUT) | **Agent** | UI state KV (workspace writes layout state; agent reads via `get_ui_state` tool). |
|
|
514
|
+
| `/api/v1/ui/commands` (POST) | **Agent** | Agent posts UI command via `exec_ui` tool. |
|
|
515
|
+
| `/api/v1/ui/commands/next` (GET, SSE) | **Agent** | Workspace subscribes; receives commands; dispatches via Zustand store. |
|
|
516
|
+
| `/api/v1/agent/*` | **Agent** | Chat, sessions. |
|
|
517
|
+
| `/api/v1/git/*` | **Not in v1** | Git UI dropped; agent runs git via bash. Routes land when a UI needs them. |
|
|
518
|
+
| `/api/v1/workspaces/*` | **Cloud** (future) | Multi-workspace lifecycle. Not in v1. |
|
|
519
|
+
| Path validation + sandbox adapters (bwrap, directBash, nodeFs) | **Agent** | Moved in full from v1 workspace server. |
|
|
520
|
+
|
|
521
|
+
**Total v2 workspace server: 0 lines.** Workspace is a frontend library — installed into an app that also wires up `@boring/agent`'s backend. See `boring-ui-v2/packages/agent/docs/plans/agent-package-spec.md` for the full HTTP surface + rationale.
|
|
522
|
+
|
|
523
|
+
**Bridge transport (command-based, tenth pass)**: Agent posts commands via `POST /api/v1/ui/commands` → server validates and streams via SSE `GET /api/v1/ui/commands/next` (`event: command`) → workspace executes locally → workspace `PUT /api/v1/ui/state` with `causedBy` field. Workspace Zustand is the authority. All events carry `v:1` protocol version. No WebSocket.
|
|
524
|
+
|
|
525
|
+
### Dynamic panes (post-launch)
|
|
526
|
+
|
|
527
|
+
> Post-launch feature — see **Appendix I** below. Not part of v2 delivery.
|
|
528
|
+
> Prerequisite: Phases 1-3a must ship first. No current app uses dynamic panes.
|
|
529
|
+
|
|
530
|
+
## Phases
|
|
531
|
+
|
|
532
|
+
### Phase 1: Foundation (shadcn shell + dockview wrapper)
|
|
533
|
+
|
|
534
|
+
**Goal**: Empty workspace renders with shadcn-themed dockview. No panels, no data. Just the layout engine working with persistence.
|
|
535
|
+
|
|
536
|
+
**Tasks:**
|
|
537
|
+
|
|
538
|
+
1.1. **Project scaffold**
|
|
539
|
+
- `v2/packages/workspace/package.json` with deps: `react`, `dockview-react`, `tailwindcss`, `class-variance-authority`, `clsx`, `tailwind-merge`
|
|
540
|
+
- `tailwind.config.ts` with shadcn preset
|
|
541
|
+
- `tsconfig.json` (strict, paths aliases)
|
|
542
|
+
- CSS entry point with `@tailwind base/components/utilities` + shadcn CSS variables
|
|
543
|
+
|
|
544
|
+
1.2. **shadcn UI vendoring**
|
|
545
|
+
- Vendor core components: `Button`, `Tabs`, `Tooltip`, `DropdownMenu`, `Sheet`, `ResizablePanel`, `ScrollArea`, `Input`, `Badge`, `Separator`
|
|
546
|
+
- All under `@boring/ui/`
|
|
547
|
+
- Tailwind-only, no custom CSS classes
|
|
548
|
+
|
|
549
|
+
1.3. **Dockview integration layer**
|
|
550
|
+
- `DockviewShell` component: accepts `LayoutConfig` (groups with positions, constraints, initial panels), resolves panel IDs via registry, initializes dockview, manages lifecycle
|
|
551
|
+
- `LayoutConfig` type: `{ groups: GroupConfig[] }` — the composability primitive. Presets build these, apps can build custom ones.
|
|
552
|
+
- `PanelChrome` wrapper: shadcn header (title, icon, close button), body slot. Injects `PaneProps` into every panel component.
|
|
553
|
+
- `TabBar` wrapper: shadcn-styled tabs replacing dockview's default tab component
|
|
554
|
+
- `GroupChrome` wrapper: shadcn-styled group headers
|
|
555
|
+
- `dockview-reset.css`: strip dockview's default theme, let wrappers take over
|
|
556
|
+
- All visible chrome is tailwind classes on our wrapper components
|
|
557
|
+
|
|
558
|
+
1.4. **Persistence engine (Zustand persist middleware)**
|
|
559
|
+
- Zustand store with `persist` middleware, two partitions:
|
|
560
|
+
- `boring-ui-v2:layout:{workspaceId}` — `{ version: '2.0', layout: DockviewSerializedLayout, sidebar: CollapsedState, sizes: PanelSizes }`
|
|
561
|
+
- `boring-ui-v2:preferences` — `{ theme: 'light' | 'dark' }` (never reset)
|
|
562
|
+
- Layout partition: version mismatch → call `onLayoutVersionMismatch()` callback (default: reset to defaults). Apps can provide custom migration.
|
|
563
|
+
- `partialize` selects which state slices persist to which key
|
|
564
|
+
- Auto-hydration on mount via Zustand's `onRehydrateStorage`
|
|
565
|
+
- `useLayoutPersistence()` hook: auto-save on dockview `onDidLayoutChange` (**debounced 300ms**,
|
|
566
|
+
flush on `beforeunload`), auto-restore on mount. Critical: `onDidLayoutChange` fires per-pixel
|
|
567
|
+
during sash drags — raw `toJSON()` without debounce freezes the main thread.
|
|
568
|
+
- **Persistence hardening:**
|
|
569
|
+
- Zod schema validation on restore (reject negative dimensions, path separators in panel IDs, strings >1KB)
|
|
570
|
+
- `QuotaExceededError` handling: try/catch on `localStorage.setItem()`. On quota error, clear layout key
|
|
571
|
+
and retry once. If still failing, disable persistence for session + toast "Layout changes won't be saved."
|
|
572
|
+
- Cross-tab: listen to `window.addEventListener('storage', ...)` for layout key changes from other tabs.
|
|
573
|
+
Do NOT auto-apply foreign changes mid-session (jarring UX). Last-writer-wins, documented.
|
|
574
|
+
- Size budget: target <50KB serialized. Log warning if >100KB.
|
|
575
|
+
|
|
576
|
+
1.5. **Panel registry**
|
|
577
|
+
- `PanelRegistry` class: `register(id, config)`, `get(id)`, `list()`, `has(id)`, `resolve(filePattern)`
|
|
578
|
+
- Config shape: `PanelConfig` (see composability model — id, component, title, icon, placement, constraints, filePatterns, requiresCapabilities)
|
|
579
|
+
- `filePatterns` enables file-type routing: `registry.resolve('data.csv')` → first panel whose glob matches
|
|
580
|
+
- Lazy loading via `React.lazy()` for async components (PanelConfig accepts `() => Promise<...>`)
|
|
581
|
+
- `useRegistry()` hook + `RegistryProvider` context
|
|
582
|
+
- Built-in panels registered by default — **all lazy-loaded except `empty`**:
|
|
583
|
+
- `filetree`: `() => import('./panes/FileTreePane')` (~15KB gz with react-arborist)
|
|
584
|
+
- `code-editor`: `() => import('./panes/CodeEditorPane')` (~250KB gz with CodeMirror)
|
|
585
|
+
- `markdown-editor`: `() => import('./panes/MarkdownEditorPane')` (~300KB gz with tiptap)
|
|
586
|
+
- `data-catalog`: `() => import('./panes/DataCatalogPane')` (~2KB gz)
|
|
587
|
+
- `empty`: eagerly loaded (shown immediately on shell mount, <1KB)
|
|
588
|
+
- Initial bundle = dockview + shadcn + zustand + registry only (~120KB gz). Every pane is a
|
|
589
|
+
separate chunk loaded on first use. ChatLayout apps never download FileTree or editors.
|
|
590
|
+
- Apps add/replace panels via `WorkspaceProvider panels` prop — same PanelConfig contract
|
|
591
|
+
- App-registered panels with matching `filePatterns` override built-in defaults (more specific wins)
|
|
592
|
+
|
|
593
|
+
1.6. **DockviewShell lifecycle managers** (internal to DockviewShell, not exported as hooks)
|
|
594
|
+
- `wireGroupPlaceholder(api, groupId, placeholderId)` — add placeholder when group empties, remove when panel added
|
|
595
|
+
- `wireCollapsible(api, groupId, collapsedWidth, expandedConstraints)` — manage collapse/expand with constraint switching
|
|
596
|
+
- `useDockviewApi()` hook — exposes `DockviewShellApi` (addPanel, removePanel, activatePanel, updatePanelParams, movePanel, batch, getGroup, setGroupCollapsed) to panes
|
|
597
|
+
- These live inside DockviewShell. Presets don't need to call them — the shell reads `dynamic`, `placeholder`, `collapsible` from GroupConfig and wires automatically.
|
|
598
|
+
|
|
599
|
+
1.7. **Layout presets** (thin config wrappers — ~30 lines each)
|
|
600
|
+
- `IdeLayout`: builds a `LayoutConfig` with sidebar (left) + center (tabs) + optional right rail
|
|
601
|
+
- Props: `{ sidebar?: string, center?: string, right?: string }` — panel IDs, all overridable
|
|
602
|
+
- Defaults: `{ sidebar: 'filetree', center: 'empty', right: undefined }`
|
|
603
|
+
- `ChatLayout`: builds a `LayoutConfig` with nav rail (left) + chat stage (center) + surface (right)
|
|
604
|
+
- Props: `{ nav?: string, center?: string, surface?: string }`
|
|
605
|
+
- Defaults: `{ nav: 'session-list', center: 'chat', surface: undefined }`
|
|
606
|
+
- Both call `<DockviewShell layout={config} />` — they are not special, just convenience
|
|
607
|
+
- Apps that need custom arrangements skip presets and use `DockviewShell` directly with a `LayoutConfig`
|
|
608
|
+
|
|
609
|
+
**Deliverable**: A working workspace that renders an empty IDE layout with shadcn-themed dockview panels. Panels can be opened/closed/resized. Layout persists to localStorage and restores on reload.
|
|
610
|
+
|
|
611
|
+
### Phase 2: Built-in panes
|
|
612
|
+
|
|
613
|
+
**Goal**: Ship the 4 core panes — file tree, markdown viewer, data catalog, code editor.
|
|
614
|
+
|
|
615
|
+
**Tasks:**
|
|
616
|
+
|
|
617
|
+
2.1. **File tree pane**
|
|
618
|
+
- Virtualized tree using `react-arborist` (~15KB gz) — provides virtualized rendering,
|
|
619
|
+
keyboard navigation (arrow keys for expand/collapse/navigate), ARIA tree semantics
|
|
620
|
+
(`role="tree"` / `role="treeitem"` / `aria-expanded`), and drag-and-drop reordering.
|
|
621
|
+
O(visible) rendering, handles 10K+ files. Eliminates ~400 lines of custom tree logic
|
|
622
|
+
that would be needed with raw `react-window`.
|
|
623
|
+
- Collapsible directories, file icons, click-to-open
|
|
624
|
+
- Consumes file list from HTTP provider (`/api/v1/tree`, agent-hosted)
|
|
625
|
+
- Polling: `refetchInterval: 3000` (3s) for file list, paused during edits
|
|
626
|
+
- Search/filter input at top (shadcn `Input`) with debounced search (200ms).
|
|
627
|
+
Client-side filter for <5K files; falls back to server-side `/api/v1/files/search`
|
|
628
|
+
for larger workspaces via `/api/v1/files/search` (threshold configurable via WorkspaceProvider prop).
|
|
629
|
+
- Context menu (shadcn `DropdownMenu`): new file (`POST /api/v1/files`), new folder (`POST /api/v1/dirs`), rename (`POST /api/v1/files/move`), delete (`DELETE /api/v1/files`), copy path (client-only), open to side (bridge command)
|
|
630
|
+
- Drag-and-drop file moving: drag file/folder onto directory to move via `POST /api/v1/files/move` (same op as rename — path change only). Drop validation (can't drop into self/children). Visual drag-over feedback.
|
|
631
|
+
- Flat tree (no section system). Single root directory.
|
|
632
|
+
- No git status badges (git UI is agent responsibility)
|
|
633
|
+
- Used by both IdeLayout (sidebar) and ChatLayout (file browser in surface)
|
|
634
|
+
|
|
635
|
+
2.2. **Markdown editor pane**
|
|
636
|
+
- Port v1's tiptap editor: `packages/workspace/src/front/components/Editor.jsx` (912 lines)
|
|
637
|
+
- 10 extensions (reduced from 16): StarterKit, Underline, Link, Placeholder, TaskList+TaskItem, TextAlign, Highlight, Image (official, no resize), CodeBlockLowlight, Markdown. (DiffExtension dropped with diff mode.)
|
|
638
|
+
- **Dropped**: Table suite (4 packages), `tiptap-extension-resize-image` (third-party), FrontmatterEditor
|
|
639
|
+
- Toolbar: Bold, Italic, Underline, Strikethrough, H1/H2/H3, Bullet/Ordered/Task list, Quote, Code block, Link, Horizontal rule, Highlight, Image (URL prompt)
|
|
640
|
+
- Single mode: normal edit. Diff-vs-HEAD deferred to v1.x (git routes not in v1).
|
|
641
|
+
- Restyle with shadcn/tailwind (replace v1's custom CSS classes)
|
|
642
|
+
- Read/write via HTTP provider. Auto-save debounced 1000ms.
|
|
643
|
+
- Tab dirty state + external file change detection (same as code editor)
|
|
644
|
+
- Lazy-loaded: tiptap bundles (~650KB) only load on first markdown file open
|
|
645
|
+
- The component (`MarkdownEditor`) is standalone; the pane wraps it in dockview
|
|
646
|
+
|
|
647
|
+
2.3. **Data catalog pane**
|
|
648
|
+
- List of data sources / connections
|
|
649
|
+
- shadcn `Card` components for each source
|
|
650
|
+
- Click to preview data in center panel
|
|
651
|
+
- Minimal — similar to current 87-line implementation
|
|
652
|
+
|
|
653
|
+
2.4. **Code editor pane** (NEW — v1 uses react-simple-code-editor, v2 upgrades to CodeMirror 6)
|
|
654
|
+
- CodeMirror 6 integration (full rewrite, not a port)
|
|
655
|
+
- **Large file handling** (CM6 has NO worker-based tokenization — Lezer parses incrementally on main thread):
|
|
656
|
+
- **<1MB**: Full syntax highlighting, incremental Lezer parsing (default CM6 behavior)
|
|
657
|
+
- **≥1MB**: Read-only mode with syntax highlighting disabled. Show banner:
|
|
658
|
+
"Large file — editing disabled." + download link for files >10MB.
|
|
659
|
+
- Language support: JS/TS, Python, JSON, YAML, Markdown, SQL (all bundled initially; measure chunk size in Phase 2 — cut to 3 bundled + lazy rest only if total budget exceeded)
|
|
660
|
+
- Theme: wire CodeMirror theme to shadcn CSS variables via `EditorView.theme()`
|
|
661
|
+
- Read/write via HTTP provider
|
|
662
|
+
- Single mode: normal edit. Diff-vs-HEAD deferred to v1.x (git routes not in v1).
|
|
663
|
+
- Line numbers, word wrap toggle
|
|
664
|
+
- Tab dirty state: dot indicator on tab when unsaved changes exist
|
|
665
|
+
- Auto-save: debounced 1000ms after last keystroke
|
|
666
|
+
- External file change detection: if file changes on disk while editor is open, auto-sync when editor is not dirty. Suppress stale reads for 3s after save.
|
|
667
|
+
- Lazy-loaded: CodeMirror bundles (~250KB gz) only load on first code file open
|
|
668
|
+
|
|
669
|
+
2.4a. **Shared editor lifecycle hook** (`useEditorLifecycle`)
|
|
670
|
+
- Shared between CodeMirror and Tiptap editors (~100 LOC)
|
|
671
|
+
- Handles: dirty state tracking (bridge `markDirty`/`markClean`), auto-save debounce (1000ms),
|
|
672
|
+
external file change detection (auto-sync when not dirty, suppress stale reads 3s after save),
|
|
673
|
+
tab dirty indicator wiring
|
|
674
|
+
- Each editor provides an adapter: `{ isDirty: () => boolean, save: () => Promise<void>, getContent: () => string }`
|
|
675
|
+
- Eliminates ~80 LOC duplication between CodeEditorPane and MarkdownEditorPane
|
|
676
|
+
|
|
677
|
+
2.5. **Empty/placeholder pane**
|
|
678
|
+
- Shown when no file is open
|
|
679
|
+
- shadcn-styled welcome screen with keyboard shortcuts
|
|
680
|
+
- "Open file" action
|
|
681
|
+
|
|
682
|
+
2.6. **HTTP data provider**
|
|
683
|
+
- Typed fetch client for `/api/v1/files`, `/api/v1/tree`, `/api/v1/stat`, `/api/v1/files/search` (all agent-hosted). Git routes NOT in v1 — git UI is dropped per decisions table.
|
|
684
|
+
- React Query integration: `useFileContent(path)`, `useFileList(dir)`, `useFileWrite()`
|
|
685
|
+
- `DataProvider` context so panes access data without knowing the transport
|
|
686
|
+
- Error handling: 401 → redirect to auth, 404 → file not found UI
|
|
687
|
+
|
|
688
|
+
**Deliverable**: Functional IDE with file tree sidebar, code editor + markdown editor center, data catalog. Files load from server. Edits save back. Components also usable standalone (without dockview) for simple apps.
|
|
689
|
+
|
|
690
|
+
### Phase 3a: Agent integration — bridge + static pane
|
|
691
|
+
|
|
692
|
+
**Goal**: Agent pane renders in workspace. Agent can manipulate the UI through a shared state bridge.
|
|
693
|
+
|
|
694
|
+
**Tasks:**
|
|
695
|
+
|
|
696
|
+
3a.1. **Agent pane slot**
|
|
697
|
+
- **Updated 2026-04-24 — supersedes earlier "workspace does NOT import agent" line.** Workspace now imports `ChatPanel` from `@boring/agent` directly and registers it as a built-in pane. Matches the new dep chain `agent (leaf) ← workspace ← core`. App shells can still override by passing their own `ChatPanel` via `WorkspaceProvider`'s `panels` prop.
|
|
698
|
+
- Registered in panel registry with `placement: 'right'` (IDE) or `slot: 'chat'` (Chat layout)
|
|
699
|
+
- Lazy-loaded with Suspense wrapper
|
|
700
|
+
|
|
701
|
+
3a.2. **Workspace bridge — state store**
|
|
702
|
+
- Zustand store: `useWorkspaceStore()`
|
|
703
|
+
- State: `{ panels, activePanel, activeFile, sidebar, notifications, dirtyFiles }`
|
|
704
|
+
- `dirtyFiles` slice: `Map<string, { panelId: string, savedAt: number | null }>` — updated by
|
|
705
|
+
editor panes via `bridge.markDirty(path)` / `bridge.markClean(path)`. Exposed to agents via
|
|
706
|
+
`bridge.getDirtyFiles()`. Enables auto-save-before-commit workflows.
|
|
707
|
+
- Actions: `openFile`, `openPanel`, `closePanel`, `showNotification`, `navigateToLine`
|
|
708
|
+
- Subscribe API: `store.subscribe(selector, handler)`
|
|
709
|
+
- **Zustand selector discipline (enforced by API design, not ESLint):**
|
|
710
|
+
- `useWorkspaceStore()` is NOT exported from the public API
|
|
711
|
+
- Only atomic selector hooks are exported:
|
|
712
|
+
```typescript
|
|
713
|
+
export const useActiveFile = () => useWorkspaceStore(s => s.activeFile)
|
|
714
|
+
export const useActivePanel = () => useWorkspaceStore(s => s.activePanel)
|
|
715
|
+
export const useSidebarState = () => useWorkspaceStore(s => s.sidebar)
|
|
716
|
+
export const useOpenPanels = () => useWorkspaceStore(s => s.panels)
|
|
717
|
+
export const useDirtyFiles = () => useWorkspaceStore(s => s.dirtyFiles)
|
|
718
|
+
```
|
|
719
|
+
- Internal code CAN use `useWorkspaceStore(selector)` with selectors. External consumers cannot.
|
|
720
|
+
|
|
721
|
+
3a.3. **Workspace bridge — server endpoint (command-based, tenth pass)**
|
|
722
|
+
- Agent posts commands via `POST /api/v1/ui/commands` → server validates (Zod) → queues
|
|
723
|
+
- SSE `GET /api/v1/ui/commands/next` delivers `event: command` to workspace
|
|
724
|
+
- Workspace executes commands locally (Zustand store update)
|
|
725
|
+
- Workspace pushes state via `PUT /api/v1/ui/state` with `causedBy: 'user' | 'agent' | 'restore'`
|
|
726
|
+
- Agent reads state via `get_ui_state` tool (from server's cached copy)
|
|
727
|
+
- All events carry `v:1` protocol version field
|
|
728
|
+
- Workspace Zustand store is the authority — server is a relay and cache
|
|
729
|
+
- Matches existing chat stream pattern, ~20 LOC server
|
|
730
|
+
- **Short-poll fallback** (2s): `GET /api/v1/ui/commands/next?poll=true` + `PUT /api/v1/ui/state` for environments where SSE is unavailable
|
|
731
|
+
|
|
732
|
+
3a.4. **Chat-centered layout integration**
|
|
733
|
+
- Chat layout shell with agent chat in main area
|
|
734
|
+
- **Artifact surface = nested DockviewShell** (right panel, own state + persistence)
|
|
735
|
+
- `ArtifactSurfacePane` renders its own `<DockviewShell>` with `storageKey="v2:surface"` and `allowedPanels` guard (tenth pass)
|
|
736
|
+
- Follows v1's `SurfaceDockview` pattern: filtered registry via `allowedPanels`, gets layout via props, uses callbacks for state flow
|
|
737
|
+
- Sync suppression flag prevents feedback loops between outer and inner state
|
|
738
|
+
- Only rendered when artifacts exist (conditional mount)
|
|
739
|
+
- Session list UI (workspace renders, agent owns state — see resolved questions)
|
|
740
|
+
- **Chat hooks (simplified from v1's 8 → 2):**
|
|
741
|
+
- `useArtifactPanels()` (~50 lines) — open/close/track artifact panels in the surface dockview
|
|
742
|
+
- `useArtifactRouting()` (~30 lines) — optional convenience: maps tool names to panel types
|
|
743
|
+
- **Eliminated by bridge**: useShellStatePublisher (→ store subscriptions), useShellPersistence (→ Zustand persist), useSessionState/Store/ServerSessions (→ agent owns via bridge props), useToolBridge (→ agent calls bridge.openPanel() explicitly)
|
|
744
|
+
- NavRail props: `{ sessions, activeSessionId, onSwitch, onCreate, onNewChat, onToggleSurface, surfaceOpen }` — app shell passes user menu separately
|
|
745
|
+
|
|
746
|
+
**Deliverable**: Agent pane visible in workspace. Agent can open files, show artifacts, navigate. User sees agent actions in real-time.
|
|
747
|
+
|
|
748
|
+
### Phase 4: Polish & production hardening
|
|
749
|
+
|
|
750
|
+
**Goal**: Theme, responsive, keyboard shortcuts, error recovery.
|
|
751
|
+
|
|
752
|
+
**Tasks:**
|
|
753
|
+
|
|
754
|
+
4.1. **Theme system**
|
|
755
|
+
- Light/dark mode via shadcn CSS variables
|
|
756
|
+
- CodeMirror theme synced to workspace theme
|
|
757
|
+
- Dockview chrome follows theme automatically (via wrapper components)
|
|
758
|
+
- `useTheme()` hook + `ThemeProvider`
|
|
759
|
+
|
|
760
|
+
4.2. **Command Palette + Keyboard shortcuts**
|
|
761
|
+
- `Cmd+P` → **Command Palette** (shadcn `CommandDialog`)
|
|
762
|
+
- No prefix = file quick-open (fuzzy match)
|
|
763
|
+
- `>` prefix = filter registered commands
|
|
764
|
+
- Commands contributed via `registry.registerCommand({ id, title, run, shortcut? })`
|
|
765
|
+
- Built-in commands: Toggle Sidebar, Toggle Agent Panel, Save File, Close Tab
|
|
766
|
+
- `Cmd+B` → toggle sidebar
|
|
767
|
+
- `Cmd+\` → toggle agent panel
|
|
768
|
+
- `Cmd+S` → save active file
|
|
769
|
+
- `Cmd+W` → close active tab
|
|
770
|
+
- Panels and extensions register custom commands at runtime via the registry
|
|
771
|
+
|
|
772
|
+
4.3. **Responsive layout**
|
|
773
|
+
- Mobile: sidebar collapses to sheet (shadcn `Sheet`)
|
|
774
|
+
- Tablet: auto-collapse sidebar below breakpoint
|
|
775
|
+
- Minimum panel sizes enforced
|
|
776
|
+
|
|
777
|
+
4.4. **Error handling & recovery**
|
|
778
|
+
|
|
779
|
+
**Network errors:**
|
|
780
|
+
- HTTP fetch wrapper: auto-retry 3x with exponential backoff for 5xx / network errors
|
|
781
|
+
- 401/403: fire `onAuthError` callback (app shell handles redirect to login)
|
|
782
|
+
- Timeout: 10s default, configurable per route. Show toast on timeout.
|
|
783
|
+
- Offline detection: `navigator.onLine` + fetch probe. Show banner, disable writes.
|
|
784
|
+
|
|
785
|
+
**Corrupted state:**
|
|
786
|
+
- Zustand `onRehydrateStorage`: validate layout JSON against schema before applying
|
|
787
|
+
- Invalid layout (missing required fields, negative dimensions): reset to defaults + toast
|
|
788
|
+
- Corrupted dockview state (panel references non-existent component): strip unknown panels, keep valid ones
|
|
789
|
+
- Reset button in UI: user can force-reset layout from workspace menu
|
|
790
|
+
|
|
791
|
+
**Panel crashes:**
|
|
792
|
+
- React error boundary per pane: crashed pane shows error + retry button, doesn't kill workspace
|
|
793
|
+
- Error logged to console with panel ID and stack trace
|
|
794
|
+
- Bridge notified: `bridge.emit('pane:error', { panelId, error })` so agent can react
|
|
795
|
+
|
|
796
|
+
**Recovery hierarchy (2-tier — simplified from 3):**
|
|
797
|
+
1. Retry (panel error boundary — per-pane, doesn't kill workspace)
|
|
798
|
+
2. Full reset (corrupted layout → load default layout, lose customization)
|
|
799
|
+
No middle tier ("strip invalid panels") — either the layout is valid or it resets.
|
|
800
|
+
|
|
801
|
+
**Correlated failure handling**: Deferred to post-launch (ninth pass). For v2 launch,
|
|
802
|
+
individual pane error boundaries + bridge SSE reconnection banner are sufficient.
|
|
803
|
+
If correlated failures (backend crash killing all panes) become a real problem, add
|
|
804
|
+
`WorkspaceHealthMonitor` then. Bridge `onDisconnect` / `onReconnect` callbacks
|
|
805
|
+
still exposed on `WorkspaceBridge` for app-level handling.
|
|
806
|
+
|
|
807
|
+
4.5. **Testing (4-layer strategy)**
|
|
808
|
+
|
|
809
|
+
**Layer 1: Unit tests (fast, precise)**
|
|
810
|
+
- Hooks with mocked dockview API: `useSidebarLayout`, `usePanelSizing`, `usePanelActions`
|
|
811
|
+
- Registry: register, get, capability checks, available panes
|
|
812
|
+
- Persistence: Zustand store serialize/deserialize, version mismatch → callback (default reset)
|
|
813
|
+
- Bridge: command validation, rate limiter, state subscriptions
|
|
814
|
+
- Run: `vitest`, <5s total
|
|
815
|
+
|
|
816
|
+
**Layer 2: Storybook visual regression (design correctness)**
|
|
817
|
+
- Stories for each standalone component: FileTree, CodeEditor, MarkdownEditor, DataCatalog
|
|
818
|
+
- Stories for shadcn-wrapped dockview chrome: PanelChrome, TabBar, GroupChrome
|
|
819
|
+
- Stories for each pane in isolation (mocked data)
|
|
820
|
+
- Screenshot comparison via Chromatic or Percy
|
|
821
|
+
- Catches: CSS regressions, theme inconsistencies, responsive breakpoints
|
|
822
|
+
|
|
823
|
+
**Layer 3: Playwright E2E (golden paths)**
|
|
824
|
+
- Scripted scenarios: open file → edit → save, resize sidebar → persist → restore, open panel → close
|
|
825
|
+
- Chat layout: session switch, artifact open, agent message
|
|
826
|
+
- Bridge: agent opens file → workspace reflects, user action → bridge event fires
|
|
827
|
+
- **Bridge protocol E2E** (~50 LOC): Playwright opens workspace UI + a separate HTTP client
|
|
828
|
+
simulates the agent. Agent POSTs `openFile` command → SSE `event: command` delivers to workspace →
|
|
829
|
+
workspace applies locally (Zustand) → file panel appears in the UI. Tests the real SSE/polling path end-to-end.
|
|
830
|
+
- Run: CI, ~2 min
|
|
831
|
+
|
|
832
|
+
**Layer 4: Bombadil property-based exploration (unknown unknowns)**
|
|
833
|
+
- Framework: [Antithesis Bombadil](https://antithesishq.github.io/bombadil/) — autonomous
|
|
834
|
+
random exploration of the workspace UI, validating invariants after every action.
|
|
835
|
+
- **Properties (invariants that must always hold):**
|
|
836
|
+
- Layout has no overlapping panels (no negative widths/heights)
|
|
837
|
+
- Essential panels (filetree) cannot be closed
|
|
838
|
+
- Sidebar collapse/expand is reversible (collapse → expand = same width)
|
|
839
|
+
- Layout serialization round-trips: `toJSON() → fromJSON() → toJSON()` is idempotent
|
|
840
|
+
- No React error boundary triggers during normal exploration
|
|
841
|
+
- Bridge state matches visible UI (open panels list = actual rendered panels)
|
|
842
|
+
- Active file in bridge = file shown in editor
|
|
843
|
+
- **Actions (random sequences):**
|
|
844
|
+
- Open/close panels, resize sashes, collapse/expand sidebar
|
|
845
|
+
- Click files in tree, switch tabs, trigger keyboard shortcuts
|
|
846
|
+
- Send bridge commands (openFile, openPanel, closePanel, showNotification)
|
|
847
|
+
- Toggle theme, switch layouts (if toggle available)
|
|
848
|
+
- Write dynamic pane JSX, trigger hot-reload
|
|
849
|
+
- **Value**: Finds race conditions, state corruption, and edge cases that scripted tests
|
|
850
|
+
miss. Especially valuable for dockview (huge state space) and bridge (async commands).
|
|
851
|
+
- Run: Nightly CI or on-demand, 10-30 min exploration per run
|
|
852
|
+
|
|
853
|
+
**CI Pipeline (zero manual testing):**
|
|
854
|
+
```
|
|
855
|
+
On every PR:
|
|
856
|
+
1. vitest run # Layer 1: unit tests (~5s)
|
|
857
|
+
2. tsc --noEmit # Type check (~10s)
|
|
858
|
+
3. playwright test # Layer 3: E2E golden paths (~2 min)
|
|
859
|
+
4. storybook build + chromatic # Layer 2: visual regression (~3 min)
|
|
860
|
+
└── Total: ~6 min per PR
|
|
861
|
+
|
|
862
|
+
Nightly:
|
|
863
|
+
5. bombadil explore --duration 30m # Layer 4: property-based exploration
|
|
864
|
+
6. axe-core scan (via Storybook) # Accessibility audit
|
|
865
|
+
└── Reports to Slack/GitHub issue if failures found
|
|
866
|
+
|
|
867
|
+
On merge to main:
|
|
868
|
+
7. Full E2E suite (all browsers)
|
|
869
|
+
8. Bundle size check (fail if > budget)
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
No human testing required for any phase. Every feature is covered by at least one
|
|
873
|
+
automated layer before merge.
|
|
874
|
+
|
|
875
|
+
4.6. **Accessibility**
|
|
876
|
+
- WCAG 2.1 AA target (shadcn components are already AA-compliant)
|
|
877
|
+
- Keyboard navigation: Tab through panels, Escape to close, arrow keys in file tree
|
|
878
|
+
- Focus management: focus ring on active panel, focus trap in modals (shadcn `Dialog`)
|
|
879
|
+
- Screen reader: ARIA labels on panels (`aria-label="File tree"`, `role="tabpanel"`)
|
|
880
|
+
- axe-core automated scanning on all Storybook stories (catches 57% of WCAG issues)
|
|
881
|
+
- Manual screen reader testing deferred to post-launch (not blocking v2)
|
|
882
|
+
|
|
883
|
+
4.7. **Performance budgets**
|
|
884
|
+
|
|
885
|
+
| Metric | Target | Measured by |
|
|
886
|
+
|--------|--------|-------------|
|
|
887
|
+
| Initial JS (shell + registry, no panes) | <150KB gzipped | `vite build` chunk analysis |
|
|
888
|
+
| Total all-loaded ceiling | <800KB gzipped | sum of all workspace chunks |
|
|
889
|
+
| Shell render (empty layout) | <500ms TTI | Playwright `performance.mark()` |
|
|
890
|
+
| File tree (1000 files) | <200ms render | Vitest benchmark |
|
|
891
|
+
| First CodeMirror render | <500ms | Playwright timer |
|
|
892
|
+
| First tiptap render | <500ms | Playwright timer |
|
|
893
|
+
| Layout persistence round-trip | <50ms | Vitest benchmark |
|
|
894
|
+
| Bridge command latency | <100ms (SSE), <2s (short-poll fallback) | Playwright timer |
|
|
895
|
+
|
|
896
|
+
**Budget enforcement**: CI runs `vite build` and checks two numbers: initial chunk <150KB gz,
|
|
897
|
+
total workspace chunks <800KB gz. `import()` boundaries guarantee the initial bundle never
|
|
898
|
+
includes editor code. Per-chunk budgets are not tracked individually — if total is under 800KB,
|
|
899
|
+
the split between editors doesn't matter.
|
|
900
|
+
|
|
901
|
+
4.8. **i18n**: Deferred entirely. Plain English strings in JSX. No `t()` wrapper, no extraction
|
|
902
|
+
tooling. When i18n is needed, run a codemod (ast-grep or jscodeshift) to extract strings —
|
|
903
|
+
modern tooling makes this trivial on an existing codebase.
|
|
904
|
+
|
|
905
|
+
4.9. **Content Security Policy (CSP) compatibility**
|
|
906
|
+
|
|
907
|
+
The workspace must work under a strict CSP in hosted/multi-tenant deployments.
|
|
908
|
+
Target policy:
|
|
909
|
+
```
|
|
910
|
+
default-src 'self';
|
|
911
|
+
script-src 'self';
|
|
912
|
+
style-src 'self' 'unsafe-inline'; // Required: CodeMirror 6 style-mod injects <style> tags
|
|
913
|
+
connect-src 'self'; // SSE + POST are standard HTTP, no wss: needed
|
|
914
|
+
img-src 'self' data: blob:; // Required: inline images in markdown
|
|
915
|
+
font-src 'self';
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
**Known CSP constraints:**
|
|
919
|
+
- `style-src 'unsafe-inline'` required by CodeMirror 6's `style-mod` library (no nonce support upstream)
|
|
920
|
+
- Tiptap HTML paste: configure `HTMLSanitizer` extension to strip `<script>`, `on*` attributes,
|
|
921
|
+
`javascript:` URLs, and `<iframe>` on paste. Default tiptap does NOT sanitize HTML paste.
|
|
922
|
+
- Dynamic panes (POST-LAUNCH): esbuild returns JS from `'self'` origin, no `'unsafe-eval'` needed.
|
|
923
|
+
- `eval()` is never used. No `'unsafe-eval'` in script-src.
|
|
924
|
+
- **Test**: Playwright test that sets strict CSP header and verifies workspace loads and functions
|
|
925
|
+
(open file, edit, save, bridge command) without CSP violations.
|
|
926
|
+
|
|
927
|
+
4.10. **Test harness for consumer apps** (`@boring/workspace/testing`)
|
|
928
|
+
|
|
929
|
+
Apps that consume `@boring/workspace` need to test their custom panels and integrations
|
|
930
|
+
without standing up a real server or full provider tree. Workspace exports test utilities
|
|
931
|
+
as a separate entry point, tree-shaken from production builds:
|
|
932
|
+
|
|
933
|
+
```typescript
|
|
934
|
+
// @boring/workspace/testing
|
|
935
|
+
export { TestWorkspaceProvider } from './testing/TestWorkspaceProvider'
|
|
936
|
+
export { createMockBridge } from './testing/createMockBridge'
|
|
937
|
+
export { createMockRegistry } from './testing/createMockRegistry'
|
|
938
|
+
export { renderPane } from './testing/renderPane'
|
|
939
|
+
```
|
|
940
|
+
|
|
941
|
+
`renderPane()` wraps the component in `TestWorkspaceProvider` (mock registry, mock bridge,
|
|
942
|
+
mock data provider with fixture data, shadcn theme). Apps never assemble the provider tree manually.
|
|
943
|
+
|
|
944
|
+
`createMockBridge()` returns a `WorkspaceBridge` with all methods as `vi.fn()` stubs plus
|
|
945
|
+
optional state overrides. Supports `bridge.emit()` for simulating agent events in tests.
|
|
946
|
+
|
|
947
|
+
Child apps can run the full workspace test suite against their own integrations.
|
|
948
|
+
|
|
949
|
+
4.11. **Sample app** (`apps/workspace-playground`)
|
|
950
|
+
|
|
951
|
+
Minimal isolated test environment for the workspace package. No backend required.
|
|
952
|
+
|
|
953
|
+
```
|
|
954
|
+
apps/workspace-playground/
|
|
955
|
+
├── package.json # deps: @boring/workspace, @boring/core, vite, react
|
|
956
|
+
├── vite.config.ts
|
|
957
|
+
├── src/
|
|
958
|
+
│ ├── main.tsx
|
|
959
|
+
│ ├── App.tsx # IdeLayout with mock data provider
|
|
960
|
+
│ ├── mockProvider.ts # In-memory file data (~50 fixture files)
|
|
961
|
+
│ └── fixtures/ # .ts, .md, .json, .csv, .py sample files
|
|
962
|
+
```
|
|
963
|
+
|
|
964
|
+
- `pnpm --filter workspace-playground dev` to run
|
|
965
|
+
- Uses `WorkspaceProvider` with mock data (no `/api` proxy, no backend)
|
|
966
|
+
- Renders `IdeLayout` with built-in panels (filetree, editors, empty)
|
|
967
|
+
- Smoke test for the package: if the playground renders, the package works
|
|
968
|
+
- Also usable as a visual development environment for workspace contributors
|
|
969
|
+
|
|
970
|
+
## File structure (Phase 1 target)
|
|
971
|
+
|
|
972
|
+
```
|
|
973
|
+
v2/packages/workspace/
|
|
974
|
+
├── package.json
|
|
975
|
+
├── tsconfig.json
|
|
976
|
+
├── tailwind.config.ts
|
|
977
|
+
├── postcss.config.js
|
|
978
|
+
├── src/
|
|
979
|
+
│ ├── index.ts # Public API
|
|
980
|
+
│ ├── globals.css # Tailwind directives + shadcn CSS vars
|
|
981
|
+
│ ├── lib/
|
|
982
|
+
│ │ └── utils.ts # cn() helper (clsx + twMerge)
|
|
983
|
+
│ ├── components/ # Standalone React components (usable without dockview)
|
|
984
|
+
│ │ ├── FileTree.tsx # File browser — standalone, imported by FileTreePane
|
|
985
|
+
│ │ ├── CodeEditor.tsx # CodeMirror 6 wrapper — standalone
|
|
986
|
+
│ │ ├── MarkdownEditor.tsx # WYSIWYG markdown (Milkdown or tiptap) — standalone
|
|
987
|
+
│ │ ├── DataCatalog.tsx # Data source list — standalone
|
|
988
|
+
│ │ └── ui/ # Vendored shadcn components
|
|
989
|
+
│ │ ├── button.tsx
|
|
990
|
+
│ │ ├── tabs.tsx
|
|
991
|
+
│ │ ├── tooltip.tsx
|
|
992
|
+
│ │ ├── dropdown-menu.tsx
|
|
993
|
+
│ │ ├── sheet.tsx
|
|
994
|
+
│ │ ├── scroll-area.tsx
|
|
995
|
+
│ │ ├── input.tsx
|
|
996
|
+
│ │ ├── badge.tsx
|
|
997
|
+
│ │ ├── separator.tsx
|
|
998
|
+
│ │ └── resizable.tsx
|
|
999
|
+
│ ├── dock/ # Dockview integration (chrome + wrappers)
|
|
1000
|
+
│ │ ├── PanelChrome.tsx # Panel header + body wrapper (shadcn)
|
|
1001
|
+
│ │ ├── TabBar.tsx # shadcn-styled tab component
|
|
1002
|
+
│ │ ├── GroupChrome.tsx # Group header wrapper
|
|
1003
|
+
│ │ └── dockview-overrides.css # Minimal overrides for dockview internals (<200 lines)
|
|
1004
|
+
│ ├── layouts/
|
|
1005
|
+
│ │ ├── DockviewShell.tsx # Core dockview container — accepts LayoutConfig
|
|
1006
|
+
│ │ ├── IdeLayout.tsx # Preset: sidebar + center + right rail (~30 lines)
|
|
1007
|
+
│ │ ├── ChatLayout.tsx # Preset: nav rail + chat + surface (~30 lines)
|
|
1008
|
+
│ │ └── types.ts # LayoutConfig, GroupConfig, IdeLayoutProps, ChatLayoutProps
|
|
1009
|
+
│ ├── store/ # Single Zustand store (tenth pass — one store, partitioned persist)
|
|
1010
|
+
│ │ ├── index.ts # useWorkspaceStore (NOT exported). Persisted: layout, preferences. Ephemeral: bridge state.
|
|
1011
|
+
│ │ └── selectors.ts # Atomic hooks: useActiveFile, useActivePanel, useSidebarState, etc.
|
|
1012
|
+
│ ├── persistence/
|
|
1013
|
+
│ │ └── useLayoutPersistence.ts # Auto-save/restore hook (wires dockview events to store)
|
|
1014
|
+
│ ├── registry/
|
|
1015
|
+
│ │ ├── PanelRegistry.ts # Register/get/list/resolve panels
|
|
1016
|
+
│ │ ├── CommandRegistry.ts # Register/get commands (tenth pass — separate from panels)
|
|
1017
|
+
│ │ ├── RegistryProvider.tsx # React context
|
|
1018
|
+
│ │ ├── types.ts # PanelConfig, PaneProps, CommandConfig, PanelLifecycleApi
|
|
1019
|
+
│ │ └── dynamicLoader.ts # Hot-load agent panes (Phase 3b, see Appendix I)
|
|
1020
|
+
│ ├── panes/ # Dockview pane wrappers (component + panel params)
|
|
1021
|
+
│ │ ├── FileTreePane.tsx # Wraps FileTree + dockview params
|
|
1022
|
+
│ │ ├── MarkdownEditorPane.tsx # Wraps MarkdownEditor + file load/save
|
|
1023
|
+
│ │ ├── CodeEditorPane.tsx # Wraps CodeEditor + file load/save
|
|
1024
|
+
│ │ ├── DataCatalogPane.tsx # Wraps DataCatalog
|
|
1025
|
+
│ │ └── EmptyPane.tsx # Placeholder
|
|
1026
|
+
│ ├── bridge/ # Agent-UI bridge (Phase 3) — reads/writes the single store
|
|
1027
|
+
│ │ ├── commands.ts # Command definitions (client-side types + dispatcher)
|
|
1028
|
+
│ │ ├── client.ts # SSE subscriber + PUT helpers hitting @boring/agent
|
|
1029
|
+
│ │ └── useWorkspaceBridge.ts # Hook for panes to interact
|
|
1030
|
+
│ ├── hooks/
|
|
1031
|
+
│ │ ├── useDockLayout.ts # Sidebar discovery, collapse
|
|
1032
|
+
│ │ ├── useEditorLifecycle.ts # Shared editor lifecycle (dirty, auto-save, external change)
|
|
1033
|
+
│ │ ├── usePanelActions.ts # Open/close/activate
|
|
1034
|
+
│ │ ├── useFileData.ts # React Query file hooks
|
|
1035
|
+
│ │ └── useTheme.ts # Light/dark mode
|
|
1036
|
+
│ └── theme/
|
|
1037
|
+
│ ├── codemirror-theme.ts # CM6 theme from shadcn vars
|
|
1038
|
+
│ └── tokens.ts # Design token constants
|
|
1039
|
+
├── docs/
|
|
1040
|
+
│ └── plans/
|
|
1041
|
+
│ └── WORKSPACE_V2_PLAN.md # This file
|
|
1042
|
+
└── __tests__/
|
|
1043
|
+
├── persistence.test.ts
|
|
1044
|
+
├── registry.test.ts
|
|
1045
|
+
└── bridge.test.ts
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
## Resolved questions
|
|
1049
|
+
|
|
1050
|
+
| Question | Answer |
|
|
1051
|
+
|----------|--------|
|
|
1052
|
+
| Agent pane folder (DEFERRED) | `/workspace/panes/` — visible in workspace root when dynamic panes ship (Appendix I, post-v2). |
|
|
1053
|
+
| Dynamic pane API (DEFERRED) | **Full bridge access.** Pane receives `{ theme, data, panelId, bridge: WorkspaceBridge }`. Agent panes can orchestrate the workspace. Not in v2 scope — see Appendix I. |
|
|
1054
|
+
| Chat session ownership | **Agent owns session state** (CRUD, messages, active session). **Workspace owns session list UI** — renders `{ id, title, updatedAt }[]` from agent, calls agent's `switchSession()`/`createSession()`. Clean data/view split. |
|
|
1055
|
+
| Monorepo tooling | **pnpm workspaces + Vite.** Already familiar from v1. Minimal config. |
|
|
1056
|
+
|
|
1057
|
+
### Dynamic pane props contract
|
|
1058
|
+
|
|
1059
|
+
```typescript
|
|
1060
|
+
interface DynamicPaneProps {
|
|
1061
|
+
theme: 'light' | 'dark'
|
|
1062
|
+
data: unknown // passed by agent when opening the panel
|
|
1063
|
+
panelId: string
|
|
1064
|
+
bridge: WorkspaceBridge // full workspace API
|
|
1065
|
+
}
|
|
1066
|
+
```
|
|
1067
|
+
|
|
1068
|
+
### Session list ownership boundary
|
|
1069
|
+
|
|
1070
|
+
```
|
|
1071
|
+
@boring/agent (owns state):
|
|
1072
|
+
sessions: Session[]
|
|
1073
|
+
activeSessionId: string
|
|
1074
|
+
switchSession(id: string): void
|
|
1075
|
+
createSession(): Session
|
|
1076
|
+
deleteSession(id: string): void
|
|
1077
|
+
|
|
1078
|
+
@boring/workspace (owns UI):
|
|
1079
|
+
<SessionList
|
|
1080
|
+
sessions={agent.sessions}
|
|
1081
|
+
activeId={agent.activeSessionId}
|
|
1082
|
+
onSwitch={agent.switchSession}
|
|
1083
|
+
onCreate={agent.createSession}
|
|
1084
|
+
onDelete={agent.deleteSession}
|
|
1085
|
+
/>
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
Workspace never knows what a "session" contains (messages, AI state). It just renders a list with titles and handles the chrome (active indicator, create button, context menu).
|
|
1089
|
+
|
|
1090
|
+
## v1 → v2 reference map
|
|
1091
|
+
|
|
1092
|
+
Guide for the implementing agent. For each v2 area, the v1 file(s) to study for patterns, logic to port, and lessons learned. All paths relative to repo root.
|
|
1093
|
+
|
|
1094
|
+
### Phase 1: Foundation
|
|
1095
|
+
|
|
1096
|
+
| v2 target | v1 reference | What to learn |
|
|
1097
|
+
|-----------|-------------|---------------|
|
|
1098
|
+
| `DockviewShell.tsx` | `packages/workspace/src/front/layouts/ide/IdeLayout.jsx` (lines 1–80, 400–500) | Dockview initialization: `<DockviewReact>` setup, `onReady` callback, component map registration, `api.fromJSON()` for restore. **Simplify**: v1 is 1718 lines — most is panel lifecycle that belongs in hooks, not the shell. |
|
|
1099
|
+
| `PanelChrome.tsx` | `packages/workspace/src/front/components/DockTab.jsx` (143 lines) | Tab rendering with file icons, close button, active state. Replace all custom CSS classes with shadcn/tailwind. |
|
|
1100
|
+
| `dockview-reset.css` | `packages/workspace/src/front/layouts/ide/IdeLayout.jsx` — look for `.dockview-*` overrides in imported CSS | Identify which dockview classes need zeroing out. Also check `node_modules/dockview-react/dist/styles/dockview.css` for the full default theme. |
|
|
1101
|
+
| `store/index.ts` | `packages/workspace/src/front/persistence/LayoutManager.js` (618 lines) | Single Zustand store (tenth pass). Layout save/restore logic: `dockApi.toJSON()` serialization, localStorage read/write, validation. Persisted: layout, preferences. Ephemeral: bridge state. **Drop**: version migration system, `lastKnownGoodLayout` backup, multi-key storage. Keep: `toJSON()`/`fromJSON()` round-trip, validation. |
|
|
1102
|
+
| `PanelRegistry.ts` | `packages/workspace/src/front/registry/panes.jsx` (484 lines) | Registration pattern: `register(id, { component, title, icon, placement, constraints, requires })`. Lazy loading via `React.lazy()`. Capability gating pattern. **Drop**: `requiresRouters`, `requiresFeatures` (legacy). Keep: `requiresCapabilities` for optional runtime gating. |
|
|
1103
|
+
| `IdeLayout.tsx` | `packages/workspace/src/front/layouts/ide/IdeLayout.jsx` (full file, 1718 lines) | v1 is 1718 lines because layout logic, hooks, and panel lifecycle are inlined. **v2 approach**: IdeLayout is ~30 lines — it builds a `LayoutConfig` and passes it to `DockviewShell`. All the heavy logic lives in DockviewShell and shared hooks. Study v1 for the *group arrangement* (sidebar left locked, center tabs, right rail) and constraints, then express as a `LayoutConfig`. |
|
|
1104
|
+
| `ChatLayout.tsx` | `packages/workspace/src/front/layouts/chat/ChatCenteredWorkspace.jsx` (434 lines) | Same simplification as IdeLayout. v2 ChatLayout is ~30 lines building a `LayoutConfig` (nav rail left, chat center, surface right). Study v1 for group arrangement + `SurfaceShell.jsx` (544 lines) for the artifact surface dockview. |
|
|
1105
|
+
| `useDockLayout.ts` | `packages/workspace/src/front/hooks/useDockLayout.js` (337 lines) | Sidebar discovery (`findSidebarGroup()`), collapse toggle (`toggleSidebar()`), center group tracking. Panel activation. Group constraint management. |
|
|
1106
|
+
| `usePanelActions.ts` | `packages/workspace/src/front/hooks/usePanelActions.js` (16 KB) | Open/close/activate panel logic. File-to-panel routing. Editor tab management. **Heavy file** — extract only the core open/close/activate pattern, leave file-specific logic to panes. |
|
|
1107
|
+
|
|
1108
|
+
### Phase 2: Built-in panes
|
|
1109
|
+
|
|
1110
|
+
| v2 target | v1 reference | What to learn |
|
|
1111
|
+
|-----------|-------------|---------------|
|
|
1112
|
+
| `FileTreePane.tsx` | `packages/workspace/src/front/components/FileTree.jsx` (1129 lines) + `packages/workspace/src/front/panels/FileTreePanel.jsx` (344 lines) | Tree rendering with sections (files, data catalog). Expand/collapse state. Click-to-open dispatching. Context menu (new/rename/delete). **Rewrite**: replace custom CSS with shadcn Tree/Accordion. Use CodeMirror file icons or lucide. |
|
|
1113
|
+
| `MarkdownEditor.tsx` + `MarkdownEditorPane.tsx` | `packages/workspace/src/front/components/Editor.jsx` (912 lines) | Port directly. Keep tiptap. Restyle with shadcn/tailwind. Port: toolbar, frontmatter section, task lists, tables, code blocks with syntax highlighting. |
|
|
1114
|
+
| `CodeEditorPane.tsx` | `packages/workspace/src/front/components/CodeEditor.jsx` (176 lines, uses `react-simple-code-editor` + Prism) + `packages/workspace/src/front/panels/EditorPanel.jsx` (417 lines) | File loading (`useFileContent()`), save (`useFileWrite()`), dirty state (`isDirty`, `isSaving`), external change detection, editor mode switching (normal/diff). **Full rewrite**: `react-simple-code-editor` → CodeMirror 6. Port the file load/save/dirty pattern, rewrite the editor internals. |
|
|
1115
|
+
| `DataCatalogPane.tsx` | `packages/workspace/src/front/panels/DataCatalogPanel.jsx` (87 lines) | Simple list of data sources. Minimal. Port almost directly, just swap CSS to shadcn Card/Badge. |
|
|
1116
|
+
| `EmptyPane.tsx` | `packages/workspace/src/front/panels/EmptyPanel.jsx` (44 lines) | Placeholder with welcome message. Trivial to rewrite with shadcn. |
|
|
1117
|
+
| `useFileData.ts` | `packages/workspace/src/front/providers/data/queries.js` (10 KB) + `packages/workspace/src/front/providers/data/httpProvider.js` (8.4 KB) | React Query hooks: `useFileContent(path)`, `useFileList(dir)`, `useFileWrite()`, `useFileSearch()`. Query key patterns. HTTP provider with auth header injection, 401 retry. **Port**: the query hooks + HTTP client. **Drop**: `useGitStatus` (git UI dropped in v2), lightningFs, isomorphicGit, pyodide providers (all moved to agent or dropped). |
|
|
1118
|
+
|
|
1119
|
+
### Phase 3: Agent integration
|
|
1120
|
+
|
|
1121
|
+
| v2 target | v1 reference | What to learn |
|
|
1122
|
+
|-----------|-------------|---------------|
|
|
1123
|
+
| `bridge/client.ts` + `bridge/commands.ts` | `packages/workspace/src/server/services/uiStateImpl.ts` (201 lines) + `packages/workspace/src/front/utils/frontendState.js` (2.6 KB) | Current UI state bridge: panel snapshots, command queuing, client-scoped state. v2: bridge reads/writes the single Zustand store (tenth pass). SSE client in `bridge/client.ts`, command dispatcher in `bridge/commands.ts`. Study the state shape: `{ openPanels, activeFile, commands }`. |
|
|
1124
|
+
| `bridge/client.ts` | `packages/workspace/src/server/http/uiStateRoutes.ts` (289 lines) | Current HTTP endpoints: `GET /ui/state`, `POST /ui/command`, `GET /ui/panels`. **Replace** with command-based bridge (tenth pass): SSE `event: command` stream + `PUT /api/v1/ui/state` with `causedBy` + POST commands. Port the command vocabulary (openPanel, navigateFile, showNotification). |
|
|
1125
|
+
| `dynamicLoader.ts` | No direct v1 equivalent | New for v2. Reference: Vite's `import.meta.glob()` for dynamic imports. React `lazy()` + `Suspense` pattern from v1's pane registry (`panes.jsx` lines 20-50). Error boundary pattern from `PanelErrorBoundary.jsx` (58 lines). |
|
|
1126
|
+
| Agent pane slot | `packages/workspace/src/front/registry/panes.jsx` — agent pane registration (lines 440-470) | How agent panel is registered with capability gating (`requiresCapabilities: ['agent.chat']`). Lazy loading pattern. |
|
|
1127
|
+
| Session list UI | `packages/workspace/src/front/layouts/chat/ChatCenteredWorkspace.jsx` (lines 50-120) + `packages/workspace/src/front/layouts/chat/NavRail.jsx` (133 lines) | Session switcher in nav rail. Session create/switch callbacks. Active session indicator. **Port the UI**, not the state management (agent owns state in v2). |
|
|
1128
|
+
| Tool → artifact routing | `packages/workspace/src/front/layouts/chat/utils/toolArtifactBridge.js` (6.4 KB) + `packages/workspace/src/front/layouts/chat/hooks/useToolBridge.js` (5.3 KB) | How tool results become panel openers. Map of tool names to panel types. **Simplify**: in v2 the agent explicitly calls `bridge.openPanel()` instead of workspace inferring panel type from tool results. Keep as optional convenience. |
|
|
1129
|
+
|
|
1130
|
+
### Phase 4: Polish
|
|
1131
|
+
|
|
1132
|
+
| v2 target | v1 reference | What to learn |
|
|
1133
|
+
|-----------|-------------|---------------|
|
|
1134
|
+
| Theme system | `packages/core/src/front/hooks/useTheme.jsx` | Light/dark toggle, localStorage persistence, system preference detection. Port directly, wire to shadcn CSS vars. |
|
|
1135
|
+
| Keyboard shortcuts | `packages/core/src/front/hooks/useKeyboardShortcuts.js` | Shortcut registration pattern. v1 uses a custom hook. Consider `cmdk` or shadcn `CommandDialog` for `Cmd+P` file picker. |
|
|
1136
|
+
| Responsive layout | `packages/workspace/src/front/hooks/useResponsiveSidebarCollapse.js` (1.5 KB) | Auto-collapse sidebar below breakpoint. Simple — port the breakpoint logic, use shadcn `Sheet` for mobile sidebar. |
|
|
1137
|
+
| Panel error boundary | `packages/workspace/src/front/components/PanelErrorBoundary.jsx` (58 lines) | Error boundary wrapper with retry button. Port directly, style with shadcn. Critical for dynamic pane safety. |
|
|
1138
|
+
|
|
1139
|
+
### Files explicitly NOT to port
|
|
1140
|
+
|
|
1141
|
+
| v1 file | Reason |
|
|
1142
|
+
|---------|--------|
|
|
1143
|
+
| `components/Terminal.jsx` (578 lines) | Terminal/PTY no longer used in any app. Dead code. |
|
|
1144
|
+
| `panels/TerminalPanel.jsx` (384 lines) | Same — terminal not used |
|
|
1145
|
+
| `panels/ShellTerminalPanel.jsx` (298 lines) | Same — terminal not used |
|
|
1146
|
+
| `components/SyncStatusFooter.jsx` (489 lines) | Git sync UI — agent responsibility |
|
|
1147
|
+
| `hooks/useAutoSync.js` | Git auto-sync — agent responsibility |
|
|
1148
|
+
| `hooks/useLightningFsGitBootstrap.js` (8.5 KB) | Browser git — dropped (HTTP only) |
|
|
1149
|
+
| `providers/data/lightningFsProvider.js` | Browser FS — dropped |
|
|
1150
|
+
| `providers/data/isomorphicGitProvider.js` | Browser git — dropped |
|
|
1151
|
+
| `providers/data/pyodideRunner.js` | Browser Python — dropped |
|
|
1152
|
+
| `hooks/useGitHubConnection.js` | Auth flow — agent/core responsibility |
|
|
1153
|
+
| `hooks/useFrontendStatePersist.js` | Replaced by bridge in v2 |
|
|
1154
|
+
| `components/FrontmatterEditor.jsx` | YAML editing — not in v2 scope |
|
|
1155
|
+
| `chrome/UserMenu.jsx` | App chrome — belongs to app shell, not workspace |
|
|
1156
|
+
| `server/adapters/*` (nodeFs, directBash, bwrap) | Filesystem/sandbox — moved to agent |
|
|
1157
|
+
| `server/http/fileRoutes.ts`, `execRoutes.ts`, `gitRoutes.ts` | Backend routes — moved to agent |
|
|
1158
|
+
| `server/jobs/execJob.ts` | Exec job manager — moved to agent |
|
|
1159
|
+
| `server/workspace/paths.ts`, `helpers.ts` | Path validation — moved to agent |
|
|
1160
|
+
| All `*.d.ts`, `*.d.ts.map`, `*.js` build artifacts | v1 build artifacts, not source |
|
|
1161
|
+
|
|
1162
|
+
## Risks & mitigations
|
|
1163
|
+
|
|
1164
|
+
### Risk 1: Dockview wrapping complexity
|
|
1165
|
+
|
|
1166
|
+
**Risk**: Dockview injects DOM for tabs, drag-drop ghosts, drop targets, sash handles, floating
|
|
1167
|
+
groups. Not all of it can be wrapped in shadcn React components.
|
|
1168
|
+
|
|
1169
|
+
**Mitigation**: Best-effort wrap + single override file. Same approach as v1.
|
|
1170
|
+
|
|
1171
|
+
v1 solved this with:
|
|
1172
|
+
- Custom React tab components passed via `tabComponents`/`defaultTabComponent` props → full control over tab chrome
|
|
1173
|
+
- `.dockview-theme-abyss` CSS class (~200 lines in `packages/core/src/front/styles/base.css` lines 1519-1730) overriding `--dv-*` CSS variables and styling sash handles, active groups, content containers
|
|
1174
|
+
- Dockview's own CSS variable system (`--dv-tab-*`, `--dv-tabs-container-*`) mapped to design tokens
|
|
1175
|
+
|
|
1176
|
+
**v2 approach**: Same pattern, but map `--dv-*` variables to shadcn's `--primary`, `--background`, `--border`, etc. instead of custom tokens. Custom tab/header React components use tailwind classes. One `dockview-overrides.css` file (<200 lines) for sash handles, drop targets, and drag ghosts that can't be wrapped in React.
|
|
1177
|
+
|
|
1178
|
+
**v1 files to study**:
|
|
1179
|
+
- `packages/core/src/front/styles/base.css` lines 1519-1730 (dockview theme)
|
|
1180
|
+
- `packages/workspace/src/front/components/DockTab.jsx` (custom tab component)
|
|
1181
|
+
- `IdeLayout.jsx` lines 697-712 (DockviewReact props for custom components)
|
|
1182
|
+
|
|
1183
|
+
### Risk 2: Circular dependency (workspace ↔ agent)
|
|
1184
|
+
|
|
1185
|
+
**Updated 2026-04-24 — superseded by the inverted dep chain.** This section's original "workspace never imports agent" rule is no longer the contract. Read the current rule in §Dependency graph (top of this file) and `packages/core/docs/CORE.md` §Dependency position.
|
|
1186
|
+
|
|
1187
|
+
Current rule:
|
|
1188
|
+
|
|
1189
|
+
- **Workspace imports `ChatPanel` directly from `@boring/agent`** as a built-in pane. Agent stays the leaf with zero knowledge of workspace or core. The `panels` prop override still works for apps that want to inject a custom chat panel.
|
|
1190
|
+
- **Agent never imports from workspace.** Agent does not depend on `WorkspaceBridge` as a workspace import; the bridge types live in core, and agent consumes them only when embedded via `registerAgentRoutes` (not in its standalone CLI build).
|
|
1191
|
+
|
|
1192
|
+
```tsx
|
|
1193
|
+
// Default (workspace uses agent's ChatPanel as a built-in pane — no app-shell wiring needed):
|
|
1194
|
+
import { IdeLayout } from '@boring/workspace'
|
|
1195
|
+
|
|
1196
|
+
<IdeLayout /> // ChatPanel is included automatically
|
|
1197
|
+
|
|
1198
|
+
// Override (apps can inject a different chat panel):
|
|
1199
|
+
import { IdeLayout } from '@boring/workspace'
|
|
1200
|
+
import { MyCustomChat } from './my-chat'
|
|
1201
|
+
|
|
1202
|
+
<IdeLayout panels={[{ id: 'agent', component: MyCustomChat, placement: 'right' }]} />
|
|
1203
|
+
```
|
|
1204
|
+
|
|
1205
|
+
### Risk 3: Third-party CSS overrides
|
|
1206
|
+
|
|
1207
|
+
**Risk**: "Full shadcn, zero custom CSS" is aspirational. Dockview and CodeMirror inject their own styles.
|
|
1208
|
+
|
|
1209
|
+
**Mitigation**: One `vendor-overrides.css` file, budget <200 lines. Same as v1 pattern.
|
|
1210
|
+
|
|
1211
|
+
v1 solved this with a single `base.css` containing all vendor overrides scoped by parent class:
|
|
1212
|
+
- `.dockview-theme-abyss .dv-tab` for dockview
|
|
1213
|
+
- `.editor-content .tiptap` for tiptap
|
|
1214
|
+
- Xterm themed programmatically via `getComputedStyle()` reading CSS vars
|
|
1215
|
+
|
|
1216
|
+
**v2 approach**: Same pattern with shadcn CSS vars as the source of truth.
|
|
1217
|
+
- `dockview-overrides.css` — map `--dv-*` vars to shadcn vars, style sash/drop targets (~150 lines)
|
|
1218
|
+
- CodeMirror 6 — themed via `EditorView.theme()` API reading shadcn CSS vars at runtime (same approach as v1's xterm theming). Zero CSS override needed.
|
|
1219
|
+
- Everything else: pure tailwind/shadcn
|
|
1220
|
+
|
|
1221
|
+
### Risk 4: Persistence resets during development
|
|
1222
|
+
|
|
1223
|
+
**Risk**: Single localStorage key with no migration means devs lose layout on every schema change during Phase 1-2.
|
|
1224
|
+
|
|
1225
|
+
**Mitigation**: Accepted. Schema changes are rare after Phase 1 stabilizes. Simple version check (version !== current → reset) is already in the plan. No migration system needed.
|
|
1226
|
+
|
|
1227
|
+
### Risk 5: Dynamic pane production viability — OUT OF SCOPE
|
|
1228
|
+
|
|
1229
|
+
Dynamic panes moved to post-launch. See **Appendix I** below.
|
|
1230
|
+
Decision when it's time: server-side esbuild transform with two-layer validation.
|
|
1231
|
+
|
|
1232
|
+
### Risk 6: Phase 3 scope — SIMPLIFIED
|
|
1233
|
+
|
|
1234
|
+
**Risk**: Phase 3 was originally bridge + dynamic panes + chat layout + session UI.
|
|
1235
|
+
|
|
1236
|
+
**Mitigation**: Dynamic panes removed from v2. Phase 3 is now: bridge + static agent pane +
|
|
1237
|
+
chat layout integration + session list UI. This is one coherent unit (agent needs the bridge,
|
|
1238
|
+
both layouts need the agent pane, chat layout needs session list).
|
|
1239
|
+
|
|
1240
|
+
### Risk 7: Zustand hydration + dockview initialization race condition (CRITICAL)
|
|
1241
|
+
|
|
1242
|
+
**Risk**: v1 has a known timing issue. Layout persistence reads from localStorage (sync), then dockview
|
|
1243
|
+
mounts and calls `onReady` (async, next render cycle). If the user switches projects quickly, `panelSizesRef`
|
|
1244
|
+
may still hold the OLD project's sizes when `applyInitialSizes()` runs for the NEW project's panels.
|
|
1245
|
+
|
|
1246
|
+
v1's IdeLayout.jsx mitigates this with `layoutChromeHydratedPrefix` (line 127) — a flag that gates
|
|
1247
|
+
panel building. But the flag is set BEFORE panels fully load, creating a window where stale refs leak.
|
|
1248
|
+
|
|
1249
|
+
**v2 approach**: Zustand persist middleware `onRehydrateStorage` callback fires AFTER hydration completes.
|
|
1250
|
+
Gate dockview `onReady` behind a `hydrationComplete` flag from the store. Sequence:
|
|
1251
|
+
|
|
1252
|
+
```
|
|
1253
|
+
1. WorkspaceProvider mounts → Zustand persist starts hydrating from localStorage
|
|
1254
|
+
2. onRehydrateStorage fires → set hydrationComplete = true
|
|
1255
|
+
3. DockviewShell mounts → onReady fires → check hydrationComplete before calling fromJSON()
|
|
1256
|
+
4. If not hydrated yet → queue the onReady callback, execute after hydration
|
|
1257
|
+
```
|
|
1258
|
+
|
|
1259
|
+
This is cleaner than v1's ref-based approach. Zustand gives us a proper lifecycle hook.
|
|
1260
|
+
|
|
1261
|
+
### Risk 8: Dual editor keybinding conflicts (tiptap + CodeMirror)
|
|
1262
|
+
|
|
1263
|
+
**Risk**: v2 ships both tiptap (markdown WYSIWYG) and CodeMirror 6 (code editing). Both register
|
|
1264
|
+
global-ish keyboard handlers. When both editors exist in the same dockview layout:
|
|
1265
|
+
- Ctrl+B → tiptap bold? CodeMirror fold? dockview toggle sidebar?
|
|
1266
|
+
- Ctrl+Z → which editor's undo stack?
|
|
1267
|
+
- Tab → tiptap list indent? CodeMirror tab insert?
|
|
1268
|
+
|
|
1269
|
+
**Mitigation**: Only the *focused* editor should capture keystrokes. Both tiptap and CodeMirror
|
|
1270
|
+
already scope their handlers to their DOM container. But dockview's panel activation system
|
|
1271
|
+
(`onDidActivePanelChange`) must coordinate:
|
|
1272
|
+
|
|
1273
|
+
1. When a panel activates, disable keyboard listeners on the previously active editor panel
|
|
1274
|
+
2. tiptap: `editor.setEditable(false)` on blur, `true` on focus
|
|
1275
|
+
3. CodeMirror: `EditorView.dispatch({ effects: readOnly.reconfigure(...) })` on blur/focus
|
|
1276
|
+
4. Workspace-level shortcuts (Cmd+P, Cmd+B sidebar) registered on the workspace container,
|
|
1277
|
+
not on individual editors — they always win via `event.stopPropagation()` guard
|
|
1278
|
+
|
|
1279
|
+
**v1 file to study**: `packages/core/src/front/hooks/useKeyboardShortcuts.js` — already handles
|
|
1280
|
+
shortcut priority. Port this pattern.
|
|
1281
|
+
|
|
1282
|
+
### Risk 9: FileTree hidden context dependency blocks standalone usage
|
|
1283
|
+
|
|
1284
|
+
**Risk**: The plan exports `FileTree` as a standalone component for apps like `minimal`. But v1's
|
|
1285
|
+
`FileTree.jsx` calls `useDataProvider()` internally (line 55) — this throws if no `DataContext.Provider`
|
|
1286
|
+
wraps the component. Users who import `FileTree` standalone get a runtime crash with no clear error.
|
|
1287
|
+
|
|
1288
|
+
**Mitigation**: Two-tier component API (no middle "Connected" layer):
|
|
1289
|
+
|
|
1290
|
+
```tsx
|
|
1291
|
+
// Standalone (Tier 3 — prop-based, no context needed):
|
|
1292
|
+
<FileTree
|
|
1293
|
+
files={files}
|
|
1294
|
+
onSelect={handleSelect}
|
|
1295
|
+
onExpand={handleExpand}
|
|
1296
|
+
/>
|
|
1297
|
+
|
|
1298
|
+
// Pane (Tier 1/2 — dockview wrapper, reads from context):
|
|
1299
|
+
<FileTreePane /> // internally calls useFileData(), wired to bridge
|
|
1300
|
+
```
|
|
1301
|
+
|
|
1302
|
+
The pane wrapper (`FileTreePane`) reads from context internally — it IS the connected variant.
|
|
1303
|
+
No separate `ConnectedFileTree`. Two layers: `FileTree` (pure props) and `FileTreePane` (context-aware dockview wrapper).
|
|
1304
|
+
|
|
1305
|
+
Same pattern for all components: `CodeEditor` (standalone) + `CodeEditorPane` (wrapper).
|
|
1306
|
+
|
|
1307
|
+
### Risk 10: ChatLayout / IdeLayout code duplication (~400 LOC)
|
|
1308
|
+
|
|
1309
|
+
**Risk**: Both layouts implement left sidebar discovery, panel sizing constraints, and collapse/expand
|
|
1310
|
+
logic independently. v1's ChatCenteredWorkspace uses dockview for its artifact surface (`SurfaceShell.jsx`
|
|
1311
|
+
with `SurfaceDockview`). That's ~400 lines of duplicated sidebar + sizing code.
|
|
1312
|
+
|
|
1313
|
+
**Simplification**: Extract shared hooks before building v2 layouts:
|
|
1314
|
+
|
|
1315
|
+
| Shared hook | Extracted from | Used by |
|
|
1316
|
+
|-------------|----------------|---------|
|
|
1317
|
+
| `useSidebarLayout()` | `useDockLayout.js` lines 59-172 | IdeLayout, ChatLayout |
|
|
1318
|
+
| `usePanelSizing()` | `IdeLayout.jsx` lines 623-680 | IdeLayout, ChatLayout |
|
|
1319
|
+
| `usePanelConstraints()` | `IdeLayout.jsx` lines 210-280 | IdeLayout, ChatLayout |
|
|
1320
|
+
|
|
1321
|
+
Both are preset configs over DockviewShell (different group arrangements — chat has nav rail + surface,
|
|
1322
|
+
IDE has sidebar + editor tabs). DockviewShell handles all shared concerns (persistence, constraints,
|
|
1323
|
+
placeholders, collapse). Presets are ~30 lines each.
|
|
1324
|
+
|
|
1325
|
+
v2 architecture:
|
|
1326
|
+
```
|
|
1327
|
+
DockviewShell (handles groups, constraints, placeholders, collapse internally)
|
|
1328
|
+
├── wireGroupPlaceholder() (dynamic groups auto-manage empty state)
|
|
1329
|
+
├── wireCollapsible() (sidebar collapse/expand with constraint switching)
|
|
1330
|
+
├── useDockviewApi() (runtime API exposed to panes: addPanel, removePanel, etc.)
|
|
1331
|
+
├── IdeLayout → LayoutConfig (sidebar + center[dynamic] + right rail)
|
|
1332
|
+
└── ChatLayout → LayoutConfig (nav rail + center + surface[nested DockviewShell])
|
|
1333
|
+
```
|
|
1334
|
+
|
|
1335
|
+
### Risk 11: Panel ID not found in registry (seventh-pass)
|
|
1336
|
+
|
|
1337
|
+
**Risk**: `<IdeLayout sidebar="typo" />` passes a panel ID that isn't registered. DockviewShell
|
|
1338
|
+
calls `api.addPanel()` with an unknown component — silent failure or crash.
|
|
1339
|
+
|
|
1340
|
+
**Mitigation**: DockviewShell validates all panel IDs against the registry in `onReady` before
|
|
1341
|
+
calling `api.addPanel()`. Unknown ID → console.error with available IDs, skip the panel.
|
|
1342
|
+
In dev mode, throw to catch typos early.
|
|
1343
|
+
|
|
1344
|
+
### Risk 12: Persisted layout shadows new LayoutConfig (seventh-pass)
|
|
1345
|
+
|
|
1346
|
+
**Risk**: User's localStorage has a saved layout. App ships a new LayoutConfig with different
|
|
1347
|
+
groups. Persisted layout wins → user never sees the new arrangement.
|
|
1348
|
+
|
|
1349
|
+
**Mitigation**: LayoutConfig includes a `version: string` field (e.g., `'2.0'`). Persisted layout stores the
|
|
1350
|
+
config version it was created from. On restore, if versions differ → call `onLayoutVersionMismatch()` callback
|
|
1351
|
+
(defaults to reset). Apps can provide custom migration logic. See tenth-pass decisions.
|
|
1352
|
+
|
|
1353
|
+
### Risk 13: filePatterns routing precedence (seventh-pass)
|
|
1354
|
+
|
|
1355
|
+
**Risk**: Multiple panels register overlapping file patterns. `*.ts` vs `*.test.ts` vs `*` —
|
|
1356
|
+
which wins? App-registered vs built-in — who takes priority?
|
|
1357
|
+
|
|
1358
|
+
**Mitigation**: Explicit precedence rules:
|
|
1359
|
+
1. App-registered panels always checked before built-in panels
|
|
1360
|
+
2. Within each group: longest suffix match wins (`*.test.ts` > `*.ts` > `*`)
|
|
1361
|
+
3. If tied: first registered wins (registration order is deterministic)
|
|
1362
|
+
4. `bridge.openFile(path, { panel: 'code-editor' })` with explicit panel ID bypasses routing
|
|
1363
|
+
|
|
1364
|
+
### Risk 14: Bridge command lifecycle — async but returns void (seventh-pass)
|
|
1365
|
+
|
|
1366
|
+
**Risk**: `bridge.openFile('/file.ts')` returns void but is async internally (needs HTTP fetch +
|
|
1367
|
+
dockview panel creation). Agent calls openFile then immediately reads `getOpenPanels()` —
|
|
1368
|
+
file isn't there yet.
|
|
1369
|
+
|
|
1370
|
+
**Mitigation**: Bridge commands return `Promise<CommandResult>` (matches agent's bridge contract). Agent can await if it needs confirmation:
|
|
1371
|
+
```typescript
|
|
1372
|
+
const { seq, status } = await bridge.openFile('/file.ts')
|
|
1373
|
+
const panels = bridge.getOpenPanels() // now includes the file
|
|
1374
|
+
```
|
|
1375
|
+
Fire-and-forget usage still works — just don't `await`.
|
|
1376
|
+
|
|
1377
|
+
### Risk 15: Dockview single-maintainer dependency (ninth-pass)
|
|
1378
|
+
|
|
1379
|
+
**Risk**: Dockview is maintained by one person (mathuo). The plan deeply couples to its API
|
|
1380
|
+
(`toJSON`/`fromJSON`, `DockviewApi`, group locking, header hiding). If the project goes
|
|
1381
|
+
unmaintained, there's no migration path.
|
|
1382
|
+
|
|
1383
|
+
**Mitigation**: Accepted. DockviewShell encapsulates all dockview interaction — no pane imports
|
|
1384
|
+
from `dockview-react` directly. All pane interaction goes through `useDockviewApi()` (our
|
|
1385
|
+
abstraction). If dockview dies, only DockviewShell internals (~500 LOC) need rewriting. No
|
|
1386
|
+
additional adapter layer (YAGNI — dockview is actively maintained, 3K+ GitHub stars).
|
|
1387
|
+
|
|
1388
|
+
### Risk 16: Two editor stacks = duplicate maintenance (ninth-pass)
|
|
1389
|
+
|
|
1390
|
+
**Risk**: Tiptap (markdown) + CodeMirror (code) both need dirty tracking, auto-save, external
|
|
1391
|
+
file change detection, theme wiring. Maintaining the same lifecycle logic in two places.
|
|
1392
|
+
|
|
1393
|
+
**Mitigation**: Shared `useEditorLifecycle()` hook (~100 LOC) extracted in Phase 2.4a. Both
|
|
1394
|
+
editors provide an adapter interface: `{ isDirty, save, getContent }`. Hook handles dirty state,
|
|
1395
|
+
debounced auto-save, external change detection, and bridge `markDirty`/`markClean` calls.
|
|
1396
|
+
|
|
1397
|
+
---
|
|
1398
|
+
|
|
1399
|
+
## Implementation Reference (added in sixth-pass audit)
|
|
1400
|
+
|
|
1401
|
+
Everything below was derived from a line-by-line audit of v1 code against the v2 plan.
|
|
1402
|
+
An implementing agent should be able to start coding from this section alone.
|
|
1403
|
+
|
|
1404
|
+
### A. Public API — exact `index.ts` exports
|
|
1405
|
+
|
|
1406
|
+
```typescript
|
|
1407
|
+
// v2/packages/workspace/src/index.ts
|
|
1408
|
+
|
|
1409
|
+
// Layout shells
|
|
1410
|
+
export { IdeLayout } from './layouts/IdeLayout'
|
|
1411
|
+
export { ChatLayout } from './layouts/ChatLayout'
|
|
1412
|
+
export { DockviewShell } from './layouts/DockviewShell'
|
|
1413
|
+
|
|
1414
|
+
// Standalone components (usable WITHOUT WorkspaceProvider or dockview)
|
|
1415
|
+
export { FileTree } from './components/FileTree'
|
|
1416
|
+
export { CodeEditor } from './components/CodeEditor'
|
|
1417
|
+
export { MarkdownEditor } from './components/MarkdownEditor'
|
|
1418
|
+
export { DataCatalog } from './components/DataCatalog'
|
|
1419
|
+
|
|
1420
|
+
// Dockview pane wrappers (require WorkspaceProvider context)
|
|
1421
|
+
export { FileTreePane } from './panes/FileTreePane'
|
|
1422
|
+
export { CodeEditorPane } from './panes/CodeEditorPane'
|
|
1423
|
+
export { MarkdownEditorPane } from './panes/MarkdownEditorPane'
|
|
1424
|
+
export { DataCatalogPane } from './panes/DataCatalogPane'
|
|
1425
|
+
export { EmptyPane } from './panes/EmptyPane'
|
|
1426
|
+
|
|
1427
|
+
// Registry & panel management
|
|
1428
|
+
export { PanelRegistry } from './registry/PanelRegistry'
|
|
1429
|
+
export { CommandRegistry, useCommandRegistry } from './registry/CommandRegistry'
|
|
1430
|
+
export { RegistryProvider, useRegistry } from './registry/RegistryProvider'
|
|
1431
|
+
|
|
1432
|
+
// Bridge (agent-facing API)
|
|
1433
|
+
export { useWorkspaceBridge } from './bridge/useWorkspaceBridge'
|
|
1434
|
+
|
|
1435
|
+
// Persistence
|
|
1436
|
+
export { useLayoutPersistence } from './persistence/useLayoutPersistence'
|
|
1437
|
+
|
|
1438
|
+
// Runtime layout API (panels are dynamic)
|
|
1439
|
+
export { useDockviewApi } from './layouts/DockviewShell'
|
|
1440
|
+
|
|
1441
|
+
// Hooks
|
|
1442
|
+
export { usePanelActions } from './hooks/usePanelActions'
|
|
1443
|
+
|
|
1444
|
+
// Provider (wraps layout + registry + bridge + theme + data)
|
|
1445
|
+
export { WorkspaceProvider } from './WorkspaceProvider'
|
|
1446
|
+
|
|
1447
|
+
// Data hooks
|
|
1448
|
+
export { DataProvider, useFileData, useFileContent, useFileList, useFileWrite } from './providers/DataProvider'
|
|
1449
|
+
|
|
1450
|
+
// Theme
|
|
1451
|
+
export { ThemeProvider, useTheme } from './theme/ThemeProvider'
|
|
1452
|
+
|
|
1453
|
+
// Utilities
|
|
1454
|
+
export { getFileIcon } from './utils/fileIcons'
|
|
1455
|
+
|
|
1456
|
+
// Atomic selector hooks (useWorkspaceStore is NOT exported — only these hooks)
|
|
1457
|
+
export { useActiveFile, useActivePanel, useSidebarState, useOpenPanels, useDirtyFiles } from './store/selectors'
|
|
1458
|
+
|
|
1459
|
+
// Layout config (the composability primitive)
|
|
1460
|
+
export type { LayoutConfig, GroupConfig } from './layouts/types'
|
|
1461
|
+
|
|
1462
|
+
// Types
|
|
1463
|
+
export type { WorkspaceBridge, PanelState, CommandResult, BridgeEventMap } from './bridge/types'
|
|
1464
|
+
export type { PanelConfig, PaneProps, PanelRegistryType, CommandConfig } from './registry/types'
|
|
1465
|
+
export type { PanelLifecycleApi } from './registry/types'
|
|
1466
|
+
export type { DockviewShellApi } from './layouts/DockviewShell'
|
|
1467
|
+
// NOTE: WorkspaceStoreState is NOT exported — store is internal. Use atomic hooks.
|
|
1468
|
+
export type { WorkspaceProviderProps } from './WorkspaceProvider'
|
|
1469
|
+
export type { IdeLayoutProps, ChatLayoutProps, DockviewShellProps } from './layouts/types'
|
|
1470
|
+
```
|
|
1471
|
+
|
|
1472
|
+
**Test harness exports** (`@boring/workspace/testing` — separate entry point, tree-shaken from production):
|
|
1473
|
+
```typescript
|
|
1474
|
+
export { TestWorkspaceProvider } from './testing/TestWorkspaceProvider'
|
|
1475
|
+
export { createMockBridge } from './testing/createMockBridge'
|
|
1476
|
+
export { createMockRegistry } from './testing/createMockRegistry'
|
|
1477
|
+
export { renderPane } from './testing/renderPane'
|
|
1478
|
+
```
|
|
1479
|
+
|
|
1480
|
+
**Server exports**: None. Workspace v2 is frontend-only. The ~52 route registrars, service
|
|
1481
|
+
interfaces, and adapter types from v1 now live in `@boring/agent`.
|
|
1482
|
+
|
|
1483
|
+
**Minimum viable for `minimal` / `custom-layout` apps:**
|
|
1484
|
+
```
|
|
1485
|
+
FileTree, CodeEditor, MarkdownEditor (standalone components)
|
|
1486
|
+
DataProvider, useFileData, useFileContent (React Query hooks)
|
|
1487
|
+
// Plus @boring/core/ui: Button, Tabs, Input, Badge, Separator
|
|
1488
|
+
```
|
|
1489
|
+
|
|
1490
|
+
### B. WorkspaceProvider — full props specification
|
|
1491
|
+
|
|
1492
|
+
```typescript
|
|
1493
|
+
interface WorkspaceProviderProps {
|
|
1494
|
+
children: React.ReactNode
|
|
1495
|
+
|
|
1496
|
+
// --- Registry ---
|
|
1497
|
+
panels?: PanelConfig[] // Additional panels beyond built-ins (e.g., ChatPanel)
|
|
1498
|
+
// Built-ins (filetree, editor, empty) auto-registered
|
|
1499
|
+
|
|
1500
|
+
// --- Capabilities (for panel registry filtering) ---
|
|
1501
|
+
capabilities?: Record<string, boolean> // e.g. { 'agent.chat': true, 'workspace.files': true }
|
|
1502
|
+
|
|
1503
|
+
// --- Data layer ---
|
|
1504
|
+
apiBaseUrl?: string // Base URL for HTTP data provider. Default: '' (same origin)
|
|
1505
|
+
authHeaders?: Record<string, string> // Injected into every HTTP request (e.g., { Authorization: 'Bearer ...' })
|
|
1506
|
+
|
|
1507
|
+
// --- Theme ---
|
|
1508
|
+
defaultTheme?: 'light' | 'dark' // Initial theme. Default: 'light'
|
|
1509
|
+
onThemeChange?: (theme: 'light' | 'dark') => void
|
|
1510
|
+
|
|
1511
|
+
// --- Persistence ---
|
|
1512
|
+
workspaceId?: string // Scopes persistence key: 'boring-ui-v2:layout:{workspaceId}'
|
|
1513
|
+
// Omit for single-workspace apps (falls back to 'boring-ui-v2:layout')
|
|
1514
|
+
storageKey?: string // Full override for persistence key (takes precedence over workspaceId)
|
|
1515
|
+
persistenceEnabled?: boolean // Default: true. Set false for tests / ephemeral mode
|
|
1516
|
+
|
|
1517
|
+
// --- Layout version migration (tenth pass) ---
|
|
1518
|
+
onLayoutVersionMismatch?: (persisted: string, current: string, layout: unknown) => SerializedLayout | null
|
|
1519
|
+
// Called when persisted layout version !== LayoutConfig.version
|
|
1520
|
+
// Return migrated layout or null (= reset to defaults)
|
|
1521
|
+
// Default: () => null (reset)
|
|
1522
|
+
|
|
1523
|
+
// --- Bridge (Phase 3) ---
|
|
1524
|
+
bridgeEndpoint?: string // SSE endpoint for agent→workspace commands. Default: '/api/v1/ui/commands/next'
|
|
1525
|
+
// Set null to disable bridge (standalone mode)
|
|
1526
|
+
|
|
1527
|
+
// --- Error callbacks ---
|
|
1528
|
+
onLayoutError?: (error: Error) => void // Invalid/corrupted layout detected
|
|
1529
|
+
onAuthError?: (statusCode: number) => void // 401/403 from data provider
|
|
1530
|
+
}
|
|
1531
|
+
```
|
|
1532
|
+
|
|
1533
|
+
**Contexts provided (each accessible via hook):**
|
|
1534
|
+
|
|
1535
|
+
| Context | Hook | What it provides |
|
|
1536
|
+
|---------|------|-----------------|
|
|
1537
|
+
| `WorkspaceStoreContext` | `useWorkspaceStore()` | Zustand store — layout state, active panel, sidebar, file tree |
|
|
1538
|
+
| `WorkspaceBridgeContext` | `useWorkspaceBridge()` | Imperative commands — `openFile()`, `openPanel()`, etc. |
|
|
1539
|
+
| `RegistryContext` | `useRegistry()` | Panel registry — `get()`, `list()`, `has()`, `getComponents()` |
|
|
1540
|
+
| `ThemeContext` | `useTheme()` | Theme state — `{ theme, setTheme }` |
|
|
1541
|
+
| `DataProviderContext` | `useFileData()` | React Query hooks — `useFileContent()`, `useFileList()`, `useFileWrite()` |
|
|
1542
|
+
|
|
1543
|
+
**Usage patterns (three tiers of composability):**
|
|
1544
|
+
|
|
1545
|
+
```tsx
|
|
1546
|
+
// TIER 1: Preset layout with slot overrides
|
|
1547
|
+
import { WorkspaceProvider, IdeLayout } from '@boring/workspace'
|
|
1548
|
+
import { ChatPanel } from '@boring/agent'
|
|
1549
|
+
import { MyCustomTree } from './MyCustomTree'
|
|
1550
|
+
|
|
1551
|
+
function App() {
|
|
1552
|
+
return (
|
|
1553
|
+
<WorkspaceProvider
|
|
1554
|
+
panels={[
|
|
1555
|
+
{ id: 'agent', component: ChatPanel, placement: 'right', hideHeader: true },
|
|
1556
|
+
{ id: 'my-tree', component: MyCustomTree, title: 'Explorer' },
|
|
1557
|
+
]}
|
|
1558
|
+
capabilities={{ 'workspace.files': true, 'agent.chat': true }}
|
|
1559
|
+
onAuthError={(code) => redirectToLogin()}
|
|
1560
|
+
>
|
|
1561
|
+
<IdeLayout sidebar="my-tree" right="agent" />
|
|
1562
|
+
</WorkspaceProvider>
|
|
1563
|
+
)
|
|
1564
|
+
}
|
|
1565
|
+
```
|
|
1566
|
+
|
|
1567
|
+
```tsx
|
|
1568
|
+
// TIER 2: Custom layout via DockviewShell
|
|
1569
|
+
import { WorkspaceProvider, DockviewShell } from '@boring/workspace'
|
|
1570
|
+
|
|
1571
|
+
function App() {
|
|
1572
|
+
return (
|
|
1573
|
+
<WorkspaceProvider panels={[...]} capabilities={...}>
|
|
1574
|
+
<DockviewShell layout={{
|
|
1575
|
+
groups: [
|
|
1576
|
+
{ id: 'nav', position: 'left', panel: 'filetree', locked: true,
|
|
1577
|
+
collapsible: true, collapsedWidth: 0, constraints: { minWidth: 200, maxWidth: 350 } },
|
|
1578
|
+
{ id: 'main', position: 'center', panel: 'empty',
|
|
1579
|
+
dynamic: true, placeholder: 'empty' },
|
|
1580
|
+
{ id: 'console', position: 'bottom', panel: 'terminal',
|
|
1581
|
+
dynamic: true, constraints: { maxHeight: 300 } },
|
|
1582
|
+
{ id: 'ai', position: 'right', panel: 'agent', hideHeader: true },
|
|
1583
|
+
]
|
|
1584
|
+
}} />
|
|
1585
|
+
</WorkspaceProvider>
|
|
1586
|
+
)
|
|
1587
|
+
}
|
|
1588
|
+
```
|
|
1589
|
+
|
|
1590
|
+
```tsx
|
|
1591
|
+
// TIER 3: Standalone components (no dockview, no WorkspaceProvider, no context)
|
|
1592
|
+
// All data via props — zero context dependency (tenth pass)
|
|
1593
|
+
import { FileTree, CodeEditor } from '@boring/workspace'
|
|
1594
|
+
|
|
1595
|
+
function App() {
|
|
1596
|
+
const [content, setContent] = useState('')
|
|
1597
|
+
return (
|
|
1598
|
+
<div className="flex h-screen">
|
|
1599
|
+
<FileTree files={files} onSelect={setPath} />
|
|
1600
|
+
<CodeEditor content={content} onChange={setContent} language="typescript" />
|
|
1601
|
+
</div>
|
|
1602
|
+
)
|
|
1603
|
+
}
|
|
1604
|
+
// Note: DataProvider is only needed by pane wrappers (FileTreePane, CodeEditorPane).
|
|
1605
|
+
// Standalone components accept all data via props.
|
|
1606
|
+
```
|
|
1607
|
+
|
|
1608
|
+
### C. @boring/core dependency contract — ARCHIVED 2026-04-24
|
|
1609
|
+
|
|
1610
|
+
**This section is archival.** It was written when the planned v2 dep graph was `workspace → core`. The actual v2 dep graph is inverted: `agent (leaf) ← workspace ← core`. Workspace does NOT import from core; core imports from workspace (specifically `@boring/ui`). Canonical graph: `packages/core/docs/CORE.md` §Dependency position.
|
|
1611
|
+
|
|
1612
|
+
The list below was intended to constrain what workspace could pull from core. It's preserved only so that if we ever invert the graph again, we have a record of what v1 workspace actually used. Any mention of `@boring/core` as a workspace dep below is historical, not a contract.
|
|
1613
|
+
|
|
1614
|
+
---
|
|
1615
|
+
|
|
1616
|
+
Every import workspace v2 makes from `@boring/core`. No new dependencies added beyond v1's.
|
|
1617
|
+
|
|
1618
|
+
**HTTP transport (`@boring/core/front`):**
|
|
1619
|
+
- `apiFetchJson(url, opts)` — fetch wrapper with auth header injection
|
|
1620
|
+
- `buildApiUrl(path)` — resolve relative paths to absolute API URLs
|
|
1621
|
+
- `getHttpErrorDetail(error)` — parse HTTP error payloads
|
|
1622
|
+
- `routes` — constant map of API endpoint paths
|
|
1623
|
+
|
|
1624
|
+
**UI utilities (`@boring/core/front`):**
|
|
1625
|
+
- `cn()` — tailwind class merger (clsx + twMerge)
|
|
1626
|
+
- `copyTextToClipboard(text)` — clipboard API wrapper
|
|
1627
|
+
- `formatShortcut(shortcut)` — platform-aware shortcut notation (Cmd vs Ctrl)
|
|
1628
|
+
- `useViewportBreakpoint()` — responsive breakpoint hook
|
|
1629
|
+
|
|
1630
|
+
**Keyboard shortcuts (`@boring/core/front`):**
|
|
1631
|
+
- `useKeyboardShortcuts(shortcuts)` — global shortcut registration hook
|
|
1632
|
+
- `DEFAULT_SHORTCUTS` — predefined Cmd+P, Cmd+S, etc.
|
|
1633
|
+
|
|
1634
|
+
**Accessibility (`@boring/core/front`):**
|
|
1635
|
+
- `announceToScreenReader(message)` — ARIA live region
|
|
1636
|
+
- `trapFocus(containerEl)` — focus trap for modals
|
|
1637
|
+
- `useReducedMotion()` — respect prefers-reduced-motion
|
|
1638
|
+
|
|
1639
|
+
**Theme (`@boring/core/front`):**
|
|
1640
|
+
- `useTheme()` — get/set light/dark mode
|
|
1641
|
+
- `ThemeToggle` — toggle button component
|
|
1642
|
+
|
|
1643
|
+
**Configuration (`@boring/core/front`):**
|
|
1644
|
+
- `useConfig()` — read app config (storage prefix, feature flags)
|
|
1645
|
+
- `useCapabilities()` — read server capabilities
|
|
1646
|
+
|
|
1647
|
+
**Design tokens (`@boring/core/front`):**
|
|
1648
|
+
- `ICON_SIZE_INLINE`, `ICON_SIZE_TOOLBAR`, `ICON_SIZE_ACTIVITY`
|
|
1649
|
+
|
|
1650
|
+
**UI components (`@boring/core/ui` — shadcn, 13 vendored):**
|
|
1651
|
+
- `Button`, `Input`, `Badge`, `Separator`, `Tabs`, `Tooltip`, `DropdownMenu`
|
|
1652
|
+
- `Select`, `Dialog`, `Avatar`, `Switch`, `Label`, `Textarea`
|
|
1653
|
+
- **v2 imports these, does NOT re-vendor** — apps import from core directly
|
|
1654
|
+
|
|
1655
|
+
**Server (`@boring/core/server`):**
|
|
1656
|
+
- `createAuthHook` — auth middleware factory
|
|
1657
|
+
- `ServerConfig` type
|
|
1658
|
+
|
|
1659
|
+
**Shared (`@boring/core/shared`):**
|
|
1660
|
+
- `CapabilitiesResponse` type
|
|
1661
|
+
- Git types (`GitStatus`, `GitChange`, etc.)
|
|
1662
|
+
|
|
1663
|
+
### D. Bridge protocol specification
|
|
1664
|
+
|
|
1665
|
+
**v1 status**: HTTP polling only (750ms interval). No WebSocket.
|
|
1666
|
+
|
|
1667
|
+
**v2 transport**: **SSE + POST** — matches the existing chat stream pattern. No WebSocket.
|
|
1668
|
+
|
|
1669
|
+
**State scope**: Bridge sends `openPanels`, `activePanel`, `activeFile`, `visibleFiles` (paths
|
|
1670
|
+
shown in file tree), and `dirtyFiles`. NO full file tree state — agent queries files via its own
|
|
1671
|
+
tools. This eliminates the bandwidth problem of broadcasting 10K+ entries on every panel change.
|
|
1672
|
+
|
|
1673
|
+
#### Endpoints
|
|
1674
|
+
|
|
1675
|
+
```
|
|
1676
|
+
GET /api/v1/ui/commands/next — SSE stream (agent → workspace commands)
|
|
1677
|
+
POST /api/v1/ui/commands — agent sends commands
|
|
1678
|
+
PUT /api/v1/ui/state — workspace publishes UI state (workspace → agent)
|
|
1679
|
+
GET /api/v1/ui/state/latest — one-shot full state snapshot (for reconnection or polling)
|
|
1680
|
+
```
|
|
1681
|
+
|
|
1682
|
+
#### Data flow (tenth pass — command-based, workspace-authoritative)
|
|
1683
|
+
|
|
1684
|
+
```
|
|
1685
|
+
Agent POST {kind:'openFile', params:{path:'/foo.ts'}}
|
|
1686
|
+
→ agent server validates (Zod per-kind schema)
|
|
1687
|
+
→ agent server queues command
|
|
1688
|
+
→ SSE delivers: event: command data: {v:1, kind:'openFile', params:{...}}
|
|
1689
|
+
→ workspace executes locally (Zustand store update)
|
|
1690
|
+
→ workspace PUT /api/v1/ui/state with {v:1, causedBy:'agent', ...}
|
|
1691
|
+
→ agent reads state via get_ui_state tool
|
|
1692
|
+
|
|
1693
|
+
User clicks file in tree:
|
|
1694
|
+
→ workspace executes locally (Zustand store update)
|
|
1695
|
+
→ workspace PUT /api/v1/ui/state with {v:1, causedBy:'user', ...}
|
|
1696
|
+
→ agent reads state via get_ui_state tool
|
|
1697
|
+
```
|
|
1698
|
+
|
|
1699
|
+
**Authority**: Workspace Zustand store is the source of truth. Agent server is a relay for commands and a cache for state that agents can query. The server NEVER modifies UI state — it only validates and forwards commands.
|
|
1700
|
+
|
|
1701
|
+
#### SSE command stream (`GET /api/v1/ui/commands/next`)
|
|
1702
|
+
|
|
1703
|
+
```
|
|
1704
|
+
event: command
|
|
1705
|
+
data: {"v":1,"kind":"openFile","params":{"path":"/src/main.ts","mode":"edit"}}
|
|
1706
|
+
|
|
1707
|
+
event: command
|
|
1708
|
+
data: {"v":1,"kind":"showNotification","params":{"msg":"File saved","level":"info"}}
|
|
1709
|
+
|
|
1710
|
+
event: error
|
|
1711
|
+
data: {"v":1,"code":"invalid_command","message":"Unknown panel ID"}
|
|
1712
|
+
|
|
1713
|
+
event: heartbeat
|
|
1714
|
+
data: {}
|
|
1715
|
+
```
|
|
1716
|
+
|
|
1717
|
+
- On connect: server sends `event: init` with full state snapshot (for reconciliation).
|
|
1718
|
+
- Subsequent events are `event: command` — individual commands the workspace executes.
|
|
1719
|
+
- Browser `EventSource` auto-reconnects on disconnect (built-in).
|
|
1720
|
+
- Server sends `event: heartbeat` every 30s to keep the connection alive through proxies.
|
|
1721
|
+
- All events carry `v:1` protocol version. On version mismatch, workspace shows warning banner.
|
|
1722
|
+
- ~20 LOC server — same pattern as `GET /api/v1/agent/chat/:sessionId/:turnId`.
|
|
1723
|
+
|
|
1724
|
+
#### State publishing (`PUT /api/v1/ui/state`)
|
|
1725
|
+
|
|
1726
|
+
Workspace pushes its current UI state to the agent server after every meaningful state change.
|
|
1727
|
+
Agent reads this state via the `get_ui_state` tool (from the agent's own cached copy).
|
|
1728
|
+
|
|
1729
|
+
```typescript
|
|
1730
|
+
// PUT body:
|
|
1731
|
+
interface UIStatePut {
|
|
1732
|
+
v: 1 // protocol version
|
|
1733
|
+
causedBy: 'user' | 'agent' | 'restore' // prevents agent echo loops
|
|
1734
|
+
openPanels: PanelState[]
|
|
1735
|
+
activePanel: string | null
|
|
1736
|
+
activeFile: string | null
|
|
1737
|
+
visibleFiles: string[] // file paths shown in tree
|
|
1738
|
+
dirtyFiles: string[] // files with unsaved changes
|
|
1739
|
+
}
|
|
1740
|
+
```
|
|
1741
|
+
|
|
1742
|
+
- Debounced client-side (100ms) to coalesce rapid UI changes (e.g., clicking through files).
|
|
1743
|
+
- `causedBy` allows agents to distinguish user actions from their own command results.
|
|
1744
|
+
- 204 No Content response. No validation beyond JSON parsing — workspace is the authority.
|
|
1745
|
+
|
|
1746
|
+
#### Command format (`POST /api/v1/ui/commands`)
|
|
1747
|
+
|
|
1748
|
+
```typescript
|
|
1749
|
+
interface BridgeCommand {
|
|
1750
|
+
kind: 'openFile' | 'openPanel' | 'closePanel' | 'showNotification'
|
|
1751
|
+
| 'navigateToLine' | 'expandToFile'
|
|
1752
|
+
params: Record<string, unknown>
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// Response:
|
|
1756
|
+
interface CommandResult {
|
|
1757
|
+
seq: number
|
|
1758
|
+
status: 'ok' | 'error'
|
|
1759
|
+
error?: { code: string, message: string }
|
|
1760
|
+
}
|
|
1761
|
+
```
|
|
1762
|
+
|
|
1763
|
+
#### Bridge command validation (SECURITY-CRITICAL)
|
|
1764
|
+
|
|
1765
|
+
Every bridge command passes through a per-kind Zod validator before execution.
|
|
1766
|
+
Unknown kinds are rejected. Per-kind param schemas enforced server-side:
|
|
1767
|
+
|
|
1768
|
+
| Kind | Params | Constraints |
|
|
1769
|
+
|------|--------|-------------|
|
|
1770
|
+
| `openFile` | `path: string, mode?: 'view'\|'edit'\|'diff'` | path max 1024 chars, validated by `paths.ts` |
|
|
1771
|
+
| `openPanel` | `id: string, component: string, params?: Record<string, JsonSerializable>` | id alphanum+dash max 64, component must exist in registry, params max 16KB |
|
|
1772
|
+
| `closePanel` | `id: string` | must be open, must not be essential |
|
|
1773
|
+
| `showNotification` | `msg: string, level?: 'info'\|'warn'\|'error'` | msg max 500 chars, plain text only (NO HTML) |
|
|
1774
|
+
| `navigateToLine` | `file: string, line: number` | file validated by `paths.ts`, line positive integer |
|
|
1775
|
+
| `expandToFile` | `path: string` | validated by `paths.ts` |
|
|
1776
|
+
|
|
1777
|
+
**Rate limiting**: Deferred to post-launch. Bridge commands are infrequent (~1 per agent turn).
|
|
1778
|
+
Max 50 open panels per workspace enforced (hard cap, not rate).
|
|
1779
|
+
|
|
1780
|
+
**Path parameters** (`openFile`, `navigateToLine`, `expandToFile`): MUST go through same
|
|
1781
|
+
`validatePath()` + `assertRealPathWithinWorkspace()` as `fileRoutes.ts`. Bridge MUST NOT
|
|
1782
|
+
shortcircuit to direct file reads.
|
|
1783
|
+
|
|
1784
|
+
**Text parameters** (`showNotification` msg, panel titles): rendered as React text nodes
|
|
1785
|
+
(`{msg}`), NEVER via `dangerouslySetInnerHTML`. Truncated to max length server-side.
|
|
1786
|
+
|
|
1787
|
+
#### Authentication
|
|
1788
|
+
|
|
1789
|
+
Standard HTTP auth on both endpoints:
|
|
1790
|
+
- SSE stream: `Authorization: Bearer <token>` header (or session cookie)
|
|
1791
|
+
- POST commands: same `Authorization: Bearer <token>` header
|
|
1792
|
+
- No special ticket/upgrade mechanism needed (SSE is standard HTTP, not WebSocket)
|
|
1793
|
+
- 401/403 → SSE stream closes, client shows reconnection banner
|
|
1794
|
+
|
|
1795
|
+
#### Reconnection
|
|
1796
|
+
|
|
1797
|
+
SSE `EventSource` auto-reconnects with browser-default retry interval (~3s).
|
|
1798
|
+
On reconnect, server sends `event: init` with cached state snapshot for reconciliation.
|
|
1799
|
+
Workspace compares with its Zustand store and resolves any conflicts (workspace wins — it's the authority).
|
|
1800
|
+
|
|
1801
|
+
**Consistency guarantees:**
|
|
1802
|
+
- **At-most-once command delivery**: Commands are fire-and-forget during disconnect. Agent
|
|
1803
|
+
re-evaluates state and re-issues if needed.
|
|
1804
|
+
- **Server restart**: All bridge state is in-memory. Restart = clean slate. On reconnect,
|
|
1805
|
+
workspace re-PUTs its current state to the server.
|
|
1806
|
+
- **Idempotency**: `openPanel` with duplicate ID activates the existing panel (default
|
|
1807
|
+
`prefer_existing: true`). Makes accidental double-sends harmless.
|
|
1808
|
+
|
|
1809
|
+
#### Short-poll fallback (for environments where SSE is unavailable)
|
|
1810
|
+
|
|
1811
|
+
- `GET /api/v1/ui/state/latest` — poll every 2s for cached state (for reconciliation)
|
|
1812
|
+
- `POST /api/v1/ui/commands` — enqueue command (same endpoint as SSE mode)
|
|
1813
|
+
- `PUT /api/v1/ui/state` — workspace pushes state (same endpoint as SSE mode)
|
|
1814
|
+
- Workspace polls `GET /api/v1/ui/commands/next?poll=true` for pending commands (returns batch, not SSE)
|
|
1815
|
+
- Same JSON format, just over standard request/response
|
|
1816
|
+
|
|
1817
|
+
#### v1 REST bridge endpoints — DROPPED
|
|
1818
|
+
|
|
1819
|
+
v2 is a hard cut. No v1 bridge consumers. The v1 REST endpoints (`GET /ui/state`, `POST /ui/commands`,
|
|
1820
|
+
etc.) are not carried forward. SSE + POST is the only transport.
|
|
1821
|
+
|
|
1822
|
+
### E. DockviewShell — configuration specification
|
|
1823
|
+
|
|
1824
|
+
Two responsibilities:
|
|
1825
|
+
|
|
1826
|
+
1. **Initial layout from LayoutConfig** — groups are fixed, declarative
|
|
1827
|
+
2. **Runtime panel manipulation via `useDockviewApi()`** — panels are dynamic, imperative
|
|
1828
|
+
|
|
1829
|
+
DockviewShell auto-manages: placeholder lifecycle for dynamic groups, sidebar collapse/expand,
|
|
1830
|
+
and group constraint enforcement. Preset layouts (`IdeLayout`, `ChatLayout`) are thin wrappers
|
|
1831
|
+
that build a `LayoutConfig`. Supports nesting (a pane can render its own DockviewShell).
|
|
1832
|
+
|
|
1833
|
+
Derived from v1 `IdeLayout.jsx` lines 1–500, 697–712 and dockview 4.13.1 API.
|
|
1834
|
+
|
|
1835
|
+
```typescript
|
|
1836
|
+
interface DockviewShellProps {
|
|
1837
|
+
// Layout configuration (groups are fixed, declarative)
|
|
1838
|
+
layout: LayoutConfig
|
|
1839
|
+
|
|
1840
|
+
// Custom tab appearance (shadcn-styled)
|
|
1841
|
+
tabComponent?: React.ComponentType<DockviewDefaultTabProps>
|
|
1842
|
+
rightHeaderActions?: React.ComponentType<any>
|
|
1843
|
+
|
|
1844
|
+
// Persisted state override (takes precedence over layout config on restore)
|
|
1845
|
+
persistedLayout?: SerializedDockviewLayout
|
|
1846
|
+
|
|
1847
|
+
// Lifecycle
|
|
1848
|
+
onReady?: (api: DockviewApi) => void
|
|
1849
|
+
onLayoutChange?: (layout: SerializedDockviewLayout) => void
|
|
1850
|
+
onDidDrop?: (event: DockviewDropEvent) => void
|
|
1851
|
+
|
|
1852
|
+
// Persistence key (for nested dockview — each instance gets its own)
|
|
1853
|
+
storageKey?: string // default: uses WorkspaceProvider's key
|
|
1854
|
+
|
|
1855
|
+
// Panel guard for nested shells (tenth pass)
|
|
1856
|
+
allowedPanels?: string[] // filter registry to only these panel IDs
|
|
1857
|
+
// undefined = full registry access (default for root shell)
|
|
1858
|
+
|
|
1859
|
+
// Options
|
|
1860
|
+
className?: string
|
|
1861
|
+
}
|
|
1862
|
+
```
|
|
1863
|
+
|
|
1864
|
+
**Runtime API exposed via hook (panels are dynamic, imperative):**
|
|
1865
|
+
|
|
1866
|
+
See §Architecture > Runtime API (`useDockviewApi()`) for the full `DockviewShellApi` type
|
|
1867
|
+
(8 methods: `addPanel`, `removePanel`, `activatePanel`, `updatePanelParams`, `movePanel`,
|
|
1868
|
+
`batch`, `getGroup`, `getActivePanel`, `setGroupCollapsed`). Defined once there — not
|
|
1869
|
+
duplicated here to avoid spec drift.
|
|
1870
|
+
|
|
1871
|
+
**DockviewShell internals:**
|
|
1872
|
+
```tsx
|
|
1873
|
+
function DockviewShell({ layout, persistedLayout, storageKey, ...props }: DockviewShellProps) {
|
|
1874
|
+
const registry = useRegistry()
|
|
1875
|
+
const hydrationComplete = useWorkspaceStore(s => s.hydrationComplete)
|
|
1876
|
+
const apiRef = useRef<DockviewApi | null>(null)
|
|
1877
|
+
|
|
1878
|
+
const components = useMemo(() => registry.getComponents(), [registry])
|
|
1879
|
+
const shellApi = useMemo(() => createShellApi(apiRef), [])
|
|
1880
|
+
|
|
1881
|
+
return (
|
|
1882
|
+
<DockviewApiContext.Provider value={shellApi}>
|
|
1883
|
+
<DockviewReact
|
|
1884
|
+
className={props.className}
|
|
1885
|
+
components={components}
|
|
1886
|
+
defaultTabComponent={props.tabComponent ?? ShadcnDockTab}
|
|
1887
|
+
rightHeaderActionsComponent={props.rightHeaderActions}
|
|
1888
|
+
onReady={(event) => {
|
|
1889
|
+
if (!hydrationComplete) {
|
|
1890
|
+
pendingOnReady.current = event
|
|
1891
|
+
return
|
|
1892
|
+
}
|
|
1893
|
+
handleReady(event, layout, persistedLayout)
|
|
1894
|
+
}}
|
|
1895
|
+
onDidDrop={props.onDidDrop}
|
|
1896
|
+
/>
|
|
1897
|
+
</DockviewApiContext.Provider>
|
|
1898
|
+
)
|
|
1899
|
+
}
|
|
1900
|
+
```
|
|
1901
|
+
|
|
1902
|
+
**`onReady` callback — builds groups, applies constraints, wires lifecycle:**
|
|
1903
|
+
```typescript
|
|
1904
|
+
function handleReady(event, layout, persistedLayout?) {
|
|
1905
|
+
const api = event.api
|
|
1906
|
+
|
|
1907
|
+
// 1. Restore: persisted layout wins, else build from LayoutConfig
|
|
1908
|
+
if (persistedLayout) {
|
|
1909
|
+
api.fromJSON(persistedLayout)
|
|
1910
|
+
} else {
|
|
1911
|
+
for (const group of layout.groups) {
|
|
1912
|
+
if (group.panel) {
|
|
1913
|
+
api.addPanel({
|
|
1914
|
+
id: group.panel, component: group.panel,
|
|
1915
|
+
position: { direction: positionToDirection(group.position) },
|
|
1916
|
+
})
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// 2. Apply group properties (groups are fixed, config is authoritative)
|
|
1922
|
+
for (const group of layout.groups) {
|
|
1923
|
+
const panel = api.getPanel(group.panel ?? group.id)
|
|
1924
|
+
if (!panel?.group) continue
|
|
1925
|
+
if (group.locked) panel.group.locked = true
|
|
1926
|
+
if (group.hideHeader) panel.group.header.hidden = true
|
|
1927
|
+
if (group.constraints) {
|
|
1928
|
+
panel.group.api.setConstraints({
|
|
1929
|
+
minimumWidth: group.constraints.minWidth,
|
|
1930
|
+
maximumWidth: group.constraints.maxWidth,
|
|
1931
|
+
})
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
// 3. Wire placeholder lifecycle for dynamic groups
|
|
1936
|
+
for (const group of layout.groups) {
|
|
1937
|
+
if (group.dynamic && group.placeholder) {
|
|
1938
|
+
wireGroupPlaceholder(api, group.id, group.placeholder)
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
// 4. Wire collapsible sidebar behavior
|
|
1943
|
+
for (const group of layout.groups) {
|
|
1944
|
+
if (group.collapsible) {
|
|
1945
|
+
wireCollapsible(api, group.id, group.collapsedWidth ?? 0, group.constraints)
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
// 5. Wire layout change listener for persistence (DEBOUNCED)
|
|
1950
|
+
// onDidLayoutChange fires on every pixel of sash drag — dozens/sec.
|
|
1951
|
+
// toJSON() serializes 5-15KB. Debounce to 300ms to avoid hammering localStorage.
|
|
1952
|
+
const debouncedPersist = debounce(() => {
|
|
1953
|
+
props.onLayoutChange?.(api.toJSON())
|
|
1954
|
+
}, 300)
|
|
1955
|
+
api.onDidLayoutChange(debouncedPersist)
|
|
1956
|
+
window.addEventListener('beforeunload', () => debouncedPersist.flush())
|
|
1957
|
+
|
|
1958
|
+
// 6. Store API ref
|
|
1959
|
+
apiRef.current = api
|
|
1960
|
+
}
|
|
1961
|
+
```
|
|
1962
|
+
|
|
1963
|
+
**How presets use DockviewShell:**
|
|
1964
|
+
```typescript
|
|
1965
|
+
// layouts/IdeLayout.tsx — ~30 lines
|
|
1966
|
+
export function IdeLayout({ sidebar = 'filetree', center = 'empty', right }: IdeLayoutProps) {
|
|
1967
|
+
return <DockviewShell layout={{
|
|
1968
|
+
groups: [
|
|
1969
|
+
{ id: 'sidebar', position: 'left', panel: sidebar, locked: true,
|
|
1970
|
+
collapsible: true, collapsedWidth: 0,
|
|
1971
|
+
constraints: { minWidth: 200, maxWidth: 400 } },
|
|
1972
|
+
{ id: 'center', position: 'center', panel: center,
|
|
1973
|
+
dynamic: true, placeholder: 'empty' },
|
|
1974
|
+
...(right ? [{ id: 'right', position: 'right' as const, panel: right,
|
|
1975
|
+
hideHeader: true, constraints: { minWidth: 300 } }] : []),
|
|
1976
|
+
]
|
|
1977
|
+
}} />
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
// layouts/ChatLayout.tsx — ~30 lines
|
|
1981
|
+
export function ChatLayout({ nav = 'session-list', center = 'chat', surface, sidebar }: ChatLayoutProps) {
|
|
1982
|
+
return <DockviewShell layout={{
|
|
1983
|
+
groups: [
|
|
1984
|
+
{ id: 'nav', position: 'left', panel: nav, locked: true, hideHeader: true,
|
|
1985
|
+
constraints: { minWidth: 60, maxWidth: 60 } },
|
|
1986
|
+
{ id: 'center', position: 'center', panel: center },
|
|
1987
|
+
...(sidebar ? [{ id: 'sidebar', position: 'left' as const, panel: sidebar,
|
|
1988
|
+
collapsible: true, collapsedWidth: 0, constraints: { minWidth: 200, maxWidth: 350 } }] : []),
|
|
1989
|
+
...(surface ? [{ id: 'surface', position: 'right' as const, panel: surface,
|
|
1990
|
+
dynamic: true, placeholder: 'empty' }] : []),
|
|
1991
|
+
]
|
|
1992
|
+
}} />
|
|
1993
|
+
}
|
|
1994
|
+
// NOTE: ChatLayout supports an optional file tree sidebar (sidebar='filetree')
|
|
1995
|
+
```
|
|
1996
|
+
|
|
1997
|
+
**Nested DockviewShell (ChatLayout artifact surface):**
|
|
1998
|
+
```typescript
|
|
1999
|
+
// panes/ArtifactSurfacePane.tsx — renders inside ChatLayout's surface group
|
|
2000
|
+
function ArtifactSurfacePane({ artifacts, onSelectArtifact, onCloseArtifact }) {
|
|
2001
|
+
return (
|
|
2002
|
+
<DockviewShell
|
|
2003
|
+
layout={{
|
|
2004
|
+
groups: [{ id: 'artifacts', position: 'center', dynamic: true, placeholder: 'empty' }]
|
|
2005
|
+
}}
|
|
2006
|
+
storageKey="boring-ui-v2:surface" // own persistence, independent of outer shell
|
|
2007
|
+
allowedPanels={['code-editor', 'markdown-editor', 'csv-viewer', 'empty']} // tenth pass: guard against outer-shell panels
|
|
2008
|
+
/>
|
|
2009
|
+
)
|
|
2010
|
+
}
|
|
2011
|
+
// Filters outer panel registry to allowed IDs. Has own API, own state, own lifecycle.
|
|
2012
|
+
// v1 pattern: SurfaceDockview uses syncingRef to prevent feedback loops.
|
|
2013
|
+
```
|
|
2014
|
+
|
|
2015
|
+
**Nested shell — minimal isolation (ninth pass simplification):**
|
|
2016
|
+
|
|
2017
|
+
Nesting works naturally because dockview instances are independent. No formal isolation
|
|
2018
|
+
protocol needed for v2 — only one consumer (ChatLayout artifact surface) exists.
|
|
2019
|
+
|
|
2020
|
+
1. **Persistence**: Each DockviewShell with a unique `storageKey` creates its own Zustand
|
|
2021
|
+
persist partition. Root uses `boring-ui-v2:layout`, nested surface uses `boring-ui-v2:surface`.
|
|
2022
|
+
|
|
2023
|
+
2. **API scoping**: `useDockviewApi()` returns the API for the nearest ancestor DockviewShell,
|
|
2024
|
+
so panes inside the nested shell automatically interact with the correct instance.
|
|
2025
|
+
|
|
2026
|
+
3. **No panel ID namespacing** — panel IDs are plain strings. If two shells happen to have
|
|
2027
|
+
panels with the same ID, they are independent (different dockview instances).
|
|
2028
|
+
|
|
2029
|
+
4. **No bridge routing** — bridge commands always target the root shell. Nested shell panels
|
|
2030
|
+
are managed directly by the component that renders the nested DockviewShell.
|
|
2031
|
+
|
|
2032
|
+
If multi-shell routing is needed post-launch (e.g., agent opening artifacts in the surface
|
|
2033
|
+
from outside), add `shell` parameter to bridge commands at that point.
|
|
2034
|
+
|
|
2035
|
+
### F. shadcn component inventory — all phases
|
|
2036
|
+
|
|
2037
|
+
| Component | Phase | Used by |
|
|
2038
|
+
|-----------|-------|---------|
|
|
2039
|
+
| `Button` | 1 | Everywhere (toolbar, dialogs, actions) |
|
|
2040
|
+
| `Tabs` | 1 | TabBar (dockview tab chrome) |
|
|
2041
|
+
| `Tooltip` | 1 | Icon buttons, toolbar items |
|
|
2042
|
+
| `DropdownMenu` | 1 | Context menus, panel menu |
|
|
2043
|
+
| `Sheet` | 1 | Mobile sidebar overlay |
|
|
2044
|
+
| `ScrollArea` | 1 | File tree scroll container |
|
|
2045
|
+
| `Input` | 1 | File tree search, rename input |
|
|
2046
|
+
| `Badge` | 1 | Tab dirty indicator, panel labels |
|
|
2047
|
+
| `Separator` | 1 | Toolbar dividers, panel borders |
|
|
2048
|
+
| `Dialog` | 1 | Confirm actions, modals |
|
|
2049
|
+
| `Card` | 2 | Data catalog entries |
|
|
2050
|
+
| `Checkbox` | 2 | Task list items (tiptap markdown) |
|
|
2051
|
+
| `AlertDialog` | 2 | Confirm file delete |
|
|
2052
|
+
| `Command` / `CommandDialog` | 4 | Cmd+P file quick-open |
|
|
2053
|
+
| `Popover` | 4 | Inline context actions |
|
|
2054
|
+
| `Select` | 4 | Language selector (code editor) |
|
|
2055
|
+
| `Label` | 4 | Form labels |
|
|
2056
|
+
|
|
2057
|
+
**Total: 17 shadcn components** across all phases.
|
|
2058
|
+
|
|
2059
|
+
**Not vendored by workspace** (available from `@boring/core/ui`):
|
|
2060
|
+
`Avatar`, `Switch`, `Textarea` — only needed by app shells, not workspace itself.
|
|
2061
|
+
|
|
2062
|
+
### G. Phase dependency DAG & parallelization
|
|
2063
|
+
|
|
2064
|
+
```
|
|
2065
|
+
Phase 1 (Foundation) — Critical path: ~14 days (1 agent), ~11 days (2 agents)
|
|
2066
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
2067
|
+
|
|
2068
|
+
1.1 Scaffold ─────┬──→ 1.2 Vendor shadcn ──→ 1.3 Dockview wrappers ──┐
|
|
2069
|
+
(2-3d) │ (1-2d) (3-4d) │
|
|
2070
|
+
│ │
|
|
2071
|
+
├──→ 1.4 Persistence (Zustand) ─────────────────────┤
|
|
2072
|
+
│ (2-3d, parallel with 1.3) │
|
|
2073
|
+
│ │
|
|
2074
|
+
└──→ 1.5 Panel registry ──────────────────────────→ │
|
|
2075
|
+
(2d, parallel, doesn't block) │
|
|
2076
|
+
↓
|
|
2077
|
+
1.6 Shared hooks ──→ 1.7 Layout shells
|
|
2078
|
+
(2-3d) (3-4d)
|
|
2079
|
+
|
|
2080
|
+
|
|
2081
|
+
Phase 2 (Panes) — after Phase 1 complete. ~16-22 days (1 agent), ~7 days (3 agents)
|
|
2082
|
+
═══════════════════════════════════════════════════════════════════════════════════════
|
|
2083
|
+
|
|
2084
|
+
┌── 2.6 HTTP data provider (2-3d) ───────────────────┐
|
|
2085
|
+
│ │
|
|
2086
|
+
├── 2.1 File tree pane (3-5d) ───────────────────────→ │ (all parallel)
|
|
2087
|
+
├── 2.2 Markdown editor pane (4-6d, port from v1) ──→ │
|
|
2088
|
+
├── 2.4 Code editor pane (6-8d, FULL REWRITE) ──────→ │
|
|
2089
|
+
├── 2.3 Data catalog pane (1-2d) ────────────────────→ │
|
|
2090
|
+
└── 2.5 Empty pane (0.5d) ───────────────────────────→ │
|
|
2091
|
+
|
|
2092
|
+
Longest task: 2.4 CodeMirror 6 integration (6-8 days)
|
|
2093
|
+
|
|
2094
|
+
|
|
2095
|
+
Phase 3a (Agent integration) — after Phase 1+2. ~9-13 days (1 agent), ~6 days (2 agents)
|
|
2096
|
+
═════════════════════════════════════════════════════════════════════════════════════════════
|
|
2097
|
+
|
|
2098
|
+
3a.1 Agent pane slot (0.5d) ──┐
|
|
2099
|
+
│
|
|
2100
|
+
3a.2 Bridge state store (2-3d)─┤──→ 3a.3 Bridge server endpoint (3-4d) ──→ 3a.4 Chat layout (4-5d)
|
|
2101
|
+
│
|
|
2102
|
+
└──→ (parallel)
|
|
2103
|
+
|
|
2104
|
+
|
|
2105
|
+
Phase 4 (Polish) — all tasks independent, can overlap with Phase 3a
|
|
2106
|
+
════════════════════════════════════════════════════════════════════
|
|
2107
|
+
|
|
2108
|
+
4.1 Theme (2d) │ 4.2 Shortcuts (2d) │ 4.3 Responsive (2d) │ 4.4 Error handling (2d)
|
|
2109
|
+
4.5 Testing CI (3d) │ 4.6 Accessibility (2d) │ 4.7 Perf budgets (1d) │ 4.8 i18n (deferred)
|
|
2110
|
+
|
|
2111
|
+
|
|
2112
|
+
TOTAL ESTIMATES:
|
|
2113
|
+
1 agent, sequential: ~6-8 weeks
|
|
2114
|
+
2 agents, coordinated: ~4-5 weeks
|
|
2115
|
+
3 agents, parallel: ~3-4 weeks
|
|
2116
|
+
|
|
2117
|
+
KEY BLOCKERS:
|
|
2118
|
+
1. Phase 1 must complete before Phase 2/3a start
|
|
2119
|
+
2. CodeMirror 6 (2.4) is the longest single task (6-8 days)
|
|
2120
|
+
3. Dockview wrappers (1.3) are second longest (3-4 days)
|
|
2121
|
+
4. Bridge SSE+POST (3a.3) is NEW code (no v1 reference) — higher risk
|
|
2122
|
+
```
|
|
2123
|
+
|
|
2124
|
+
### H. Risk mitigation — implementation patterns
|
|
2125
|
+
|
|
2126
|
+
#### Risk 7: Zustand hydration race (pseudo-code)
|
|
2127
|
+
|
|
2128
|
+
```typescript
|
|
2129
|
+
// store/index.ts — single store, partitioned persist (tenth pass)
|
|
2130
|
+
export const useWorkspaceStore = create<WorkspaceStoreState>()(
|
|
2131
|
+
persist(
|
|
2132
|
+
(set, get) => ({
|
|
2133
|
+
hydrationComplete: false,
|
|
2134
|
+
layout: null,
|
|
2135
|
+
// ... other state
|
|
2136
|
+
}),
|
|
2137
|
+
{
|
|
2138
|
+
name: 'boring-ui-v2:layout',
|
|
2139
|
+
onRehydrateStorage: () => (state) => {
|
|
2140
|
+
// Fires AFTER hydration completes
|
|
2141
|
+
state?.setHydrationComplete(true)
|
|
2142
|
+
},
|
|
2143
|
+
}
|
|
2144
|
+
)
|
|
2145
|
+
)
|
|
2146
|
+
|
|
2147
|
+
// layouts/DockviewShell.tsx
|
|
2148
|
+
function DockviewShell(props: DockviewShellProps) {
|
|
2149
|
+
const hydrationComplete = useWorkspaceStore(s => s.hydrationComplete)
|
|
2150
|
+
const pendingOnReady = useRef<DockviewReadyEvent | null>(null)
|
|
2151
|
+
|
|
2152
|
+
const handleReady = useCallback((event: DockviewReadyEvent) => {
|
|
2153
|
+
if (!hydrationComplete) {
|
|
2154
|
+
pendingOnReady.current = event
|
|
2155
|
+
return
|
|
2156
|
+
}
|
|
2157
|
+
initializeDockview(event)
|
|
2158
|
+
}, [hydrationComplete])
|
|
2159
|
+
|
|
2160
|
+
// Execute queued onReady when hydration completes
|
|
2161
|
+
useEffect(() => {
|
|
2162
|
+
if (hydrationComplete && pendingOnReady.current) {
|
|
2163
|
+
initializeDockview(pendingOnReady.current)
|
|
2164
|
+
pendingOnReady.current = null
|
|
2165
|
+
}
|
|
2166
|
+
}, [hydrationComplete])
|
|
2167
|
+
|
|
2168
|
+
if (!hydrationComplete) return <LoadingSkeleton />
|
|
2169
|
+
|
|
2170
|
+
return <DockviewReact onReady={handleReady} ... />
|
|
2171
|
+
}
|
|
2172
|
+
```
|
|
2173
|
+
|
|
2174
|
+
#### Risk 8: Editor keybinding isolation
|
|
2175
|
+
|
|
2176
|
+
```typescript
|
|
2177
|
+
// hooks/useEditorFocusManagement.ts
|
|
2178
|
+
function useEditorFocusManagement(api: DockviewApi) {
|
|
2179
|
+
useEffect(() => {
|
|
2180
|
+
const disposable = api.onDidActivePanelChange((event) => {
|
|
2181
|
+
const prevPanel = event.previous
|
|
2182
|
+
const nextPanel = event.panel
|
|
2183
|
+
|
|
2184
|
+
// Deactivate previous editor
|
|
2185
|
+
if (prevPanel?.params?.editorRef) {
|
|
2186
|
+
const editor = prevPanel.params.editorRef
|
|
2187
|
+
if (editor.type === 'tiptap') editor.setEditable(false)
|
|
2188
|
+
if (editor.type === 'codemirror') {
|
|
2189
|
+
editor.dispatch({ effects: readOnlyCompartment.reconfigure(EditorState.readOnly.of(true)) })
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
// Activate new editor
|
|
2194
|
+
if (nextPanel?.params?.editorRef) {
|
|
2195
|
+
const editor = nextPanel.params.editorRef
|
|
2196
|
+
if (editor.type === 'tiptap') editor.setEditable(true)
|
|
2197
|
+
if (editor.type === 'codemirror') {
|
|
2198
|
+
editor.dispatch({ effects: readOnlyCompartment.reconfigure(EditorState.readOnly.of(false)) })
|
|
2199
|
+
editor.focus()
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
})
|
|
2203
|
+
return () => disposable.dispose()
|
|
2204
|
+
}, [api])
|
|
2205
|
+
}
|
|
2206
|
+
```
|
|
2207
|
+
|
|
2208
|
+
#### Risk 9: FileTree standalone vs connected
|
|
2209
|
+
|
|
2210
|
+
```typescript
|
|
2211
|
+
// components/FileTree.tsx — STANDALONE (props-based, zero context dependency)
|
|
2212
|
+
interface FileTreeProps {
|
|
2213
|
+
files: FileEntry[]
|
|
2214
|
+
selectedPath?: string
|
|
2215
|
+
expandedDirs?: string[]
|
|
2216
|
+
searchQuery?: string
|
|
2217
|
+
onSelect?: (path: string) => void
|
|
2218
|
+
onExpand?: (dir: string) => void
|
|
2219
|
+
onCollapse?: (dir: string) => void
|
|
2220
|
+
onContextMenu?: (path: string, action: string) => void
|
|
2221
|
+
onDragDrop?: (source: string, target: string) => void
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
export function FileTree(props: FileTreeProps) { /* pure render */ }
|
|
2225
|
+
|
|
2226
|
+
// panes/FileTreePane.tsx — the pane wrapper IS the connected variant (no separate ConnectedFileTree)
|
|
2227
|
+
export function FileTreePane({ params, bridge }: PaneProps) {
|
|
2228
|
+
const { files, expandedDirs } = useFileData(params.dir as string)
|
|
2229
|
+
return (
|
|
2230
|
+
<FileTree
|
|
2231
|
+
files={files}
|
|
2232
|
+
expandedDirs={expandedDirs}
|
|
2233
|
+
onSelect={(path) => bridge.openFile(path)}
|
|
2234
|
+
onExpand={(dir) => bridge.expandToFile(dir)}
|
|
2235
|
+
/>
|
|
2236
|
+
)
|
|
2237
|
+
}
|
|
2238
|
+
```
|
|
2239
|
+
|
|
2240
|
+
---
|
|
2241
|
+
|
|
2242
|
+
## Appendix I: Dynamic Panes — Agent-Generated UI at Runtime
|
|
2243
|
+
|
|
2244
|
+
**Status: POST-LAUNCH** — Not part of the v2 workspace delivery.
|
|
2245
|
+
Prerequisite: v2 Phases 1-3a must ship first (layouts, panes, bridge). Dynamic panes build
|
|
2246
|
+
on top of the bridge infrastructure. No current app uses this feature — it will be validated
|
|
2247
|
+
against real agent workflows before committing to build.
|
|
2248
|
+
|
|
2249
|
+
This covers how agents write React components that the workspace hot-loads as panels.
|
|
2250
|
+
|
|
2251
|
+
### Model
|
|
2252
|
+
|
|
2253
|
+
Agents write JSX files. Workspace hot-loads them. Error boundary provides the feedback loop.
|
|
2254
|
+
|
|
2255
|
+
```
|
|
2256
|
+
Agent writes .jsx → Workspace hot-loads → Renders in panel
|
|
2257
|
+
↓ (crash?)
|
|
2258
|
+
Error boundary catches
|
|
2259
|
+
↓
|
|
2260
|
+
Error piped to agent via bridge
|
|
2261
|
+
↓
|
|
2262
|
+
Agent fixes file
|
|
2263
|
+
↓
|
|
2264
|
+
Workspace reloads automatically
|
|
2265
|
+
```
|
|
2266
|
+
|
|
2267
|
+
### File location
|
|
2268
|
+
|
|
2269
|
+
Agent-generated panes live at `/workspace/panes/` — visible in the workspace root.
|
|
2270
|
+
Users can inspect and edit them.
|
|
2271
|
+
|
|
2272
|
+
### Loading mechanism
|
|
2273
|
+
|
|
2274
|
+
1. Agent writes file via its file tools (or bridge)
|
|
2275
|
+
2. Agent calls `bridge.openPanel({ id: 'chart-viz', source: '/workspace/panes/chart-visualization.jsx' })`
|
|
2276
|
+
3. Workspace calls `dynamic import(source)` wrapped in `React.lazy()`
|
|
2277
|
+
4. Component renders inside error boundary + Suspense wrapper
|
|
2278
|
+
5. Pane receives full props: `{ theme, data, panelId, bridge }`
|
|
2279
|
+
|
|
2280
|
+
### Props contract
|
|
2281
|
+
|
|
2282
|
+
```typescript
|
|
2283
|
+
interface DynamicPaneProps {
|
|
2284
|
+
theme: 'light' | 'dark'
|
|
2285
|
+
data: unknown // passed by agent when opening the panel
|
|
2286
|
+
panelId: string
|
|
2287
|
+
bridge: WorkspaceBridge // full workspace API
|
|
2288
|
+
}
|
|
2289
|
+
```
|
|
2290
|
+
|
|
2291
|
+
Full bridge access — agent-generated panes can read workspace state, open files,
|
|
2292
|
+
open other panels, show notifications, navigate. Same power as built-in panes.
|
|
2293
|
+
|
|
2294
|
+
### Component kit (`@boring/workspace/blocks`)
|
|
2295
|
+
|
|
2296
|
+
A convenience library of pre-themed shadcn building blocks. Not a constraint —
|
|
2297
|
+
agents can use raw HTML/divs if they want, but the kit is faster and consistent.
|
|
2298
|
+
|
|
2299
|
+
**Blocks:**
|
|
2300
|
+
- **Layout**: `Card`, `Grid`, `Tabs`, `Accordion`, `ScrollArea`, `Separator`
|
|
2301
|
+
- **Data display**: `DataTable`, `KeyValue`, `Metric`, `CodeBlock`, `MarkdownView`, `JsonViewer`
|
|
2302
|
+
- **Charts**: `BarChart`, `LineChart`, `PieChart`, `ScatterChart` (thin wrappers over recharts)
|
|
2303
|
+
- **Forms**: `Form`, `Input`, `Select`, `Checkbox`, `Slider`, `Button`
|
|
2304
|
+
- **Feedback**: `Badge`, `Alert`, `Spinner`, `EmptyState`
|
|
2305
|
+
|
|
2306
|
+
All blocks inherit shadcn CSS variables, are tree-shakeable, have zero business logic,
|
|
2307
|
+
and accept standard React props + `className` for tailwind overrides.
|
|
2308
|
+
|
|
2309
|
+
### Runtime boundary
|
|
2310
|
+
|
|
2311
|
+
- Dynamic panes run in **main thread** (not iframe) for full React context access
|
|
2312
|
+
- **Error boundary** catches crashes — panel shows error + stack trace, doesn't kill workspace
|
|
2313
|
+
- **Hot-reload**: workspace watches `/workspace/panes/` for file changes (via polling or
|
|
2314
|
+
bridge notification), invalidates module cache, re-renders
|
|
2315
|
+
|
|
2316
|
+
### Transform approach: server-side esbuild (DECIDED)
|
|
2317
|
+
|
|
2318
|
+
**Problem**: In production, there's no Vite dev server. The browser receives raw `.jsx` it can't execute.
|
|
2319
|
+
|
|
2320
|
+
**Decision**: Agent-generated JSX goes through a server transform endpoint before the browser
|
|
2321
|
+
ever sees it. Two-layer validation catches errors before they reach the UI.
|
|
2322
|
+
|
|
2323
|
+
```
|
|
2324
|
+
Agent writes .jsx to /workspace/panes/chart.jsx
|
|
2325
|
+
↓
|
|
2326
|
+
Server: GET /api/v1/ui/pane?source=/workspace/panes/chart.jsx
|
|
2327
|
+
↓
|
|
2328
|
+
Layer 1 — Prevalidation (esbuild transform):
|
|
2329
|
+
✅ JSX syntax valid?
|
|
2330
|
+
✅ Imports resolve? (@boring/workspace/blocks → bundled module URL)
|
|
2331
|
+
✅ No dangerous patterns? (optional AST check)
|
|
2332
|
+
↓ fails → 400 response with error details → agent fixes without UI disruption
|
|
2333
|
+
↓ passes → returns transformed JS (browser-ready ESM)
|
|
2334
|
+
↓
|
|
2335
|
+
Browser: dynamic import(transformed_url)
|
|
2336
|
+
↓
|
|
2337
|
+
Layer 2 — Runtime validation (error boundary):
|
|
2338
|
+
↓ render crash → error boundary catches → pipes error to agent → agent fixes → auto-reload
|
|
2339
|
+
↓ renders OK → panel visible to user
|
|
2340
|
+
```
|
|
2341
|
+
|
|
2342
|
+
**Why server-side esbuild**: Works identically in dev and prod, resolves bare specifiers
|
|
2343
|
+
at transform time (no import maps needed), prevalidation is free, ~10ms per file, no
|
|
2344
|
+
runtime dependency added to client bundle.
|
|
2345
|
+
|
|
2346
|
+
**Import resolution**: esbuild rewrites bare specifiers to URLs pointing at bundled modules
|
|
2347
|
+
using the Vite build manifest (`@boring/workspace/blocks` → `/assets/workspace-blocks-[hash].js`).
|
|
2348
|
+
|
|
2349
|
+
**Caching**: Transformed JS cached by content hash. Same source → same hash → browser cache hit.
|
|
2350
|
+
|
|
2351
|
+
### Bridge safety layer (for dynamic panes)
|
|
2352
|
+
|
|
2353
|
+
- **Rate limiter**: max 10 bridge mutations per second per pane. Excess dropped + logged.
|
|
2354
|
+
- **Command validation**: bridge actions go through validator (e.g., can't close essential panels).
|
|
2355
|
+
- **Undo stack**: bridge records last N state snapshots. `bridge.undo()` available for recovery.
|
|
2356
|
+
- **Kill switch**: if a dynamic pane triggers > 50 errors in 10 seconds, workspace auto-closes it.
|
|
2357
|
+
|
|
2358
|
+
### Implementation phases (Phase 3b)
|
|
2359
|
+
|
|
2360
|
+
| Task | Description |
|
|
2361
|
+
|------|-------------|
|
|
2362
|
+
| 3b.1 | Server-side esbuild transform endpoint (~100 LOC) |
|
|
2363
|
+
| 3b.2 | Dynamic pane loader (`registry/dynamicLoader.ts`) |
|
|
2364
|
+
| 3b.3 | Component kit (`blocks/`) — vendored shadcn + data blocks |
|
|
2365
|
+
| 3b.4 | File watcher + hot-reload |
|
|
2366
|
+
| 3b.5 | Error feedback loop (error → agent → fix → reload) |
|
|
2367
|
+
| 3b.6 | Bridge safety layer (rate limiter, validator, kill switch) |
|
|
2368
|
+
|
|
2369
|
+
### v1 references for dynamic panes
|
|
2370
|
+
|
|
2371
|
+
| v2 target | v1 reference | Notes |
|
|
2372
|
+
|-----------|-------------|-------|
|
|
2373
|
+
| Dynamic loader | No direct v1 equivalent | New. Reference Vite's `import.meta.glob()` pattern. |
|
|
2374
|
+
| Error boundary | `PanelErrorBoundary.jsx` (58 lines) | Port and extend with error-to-agent piping. |
|
|
2375
|
+
| Component kit | `core/design-system/ui/*.jsx` | Already vendored shadcn. Move relevant to blocks export. |
|
|
2376
|
+
| File watching | `providers/data/queries.js` — polling pattern | React Query polling. Adapt for pane directory. |
|
|
2377
|
+
| Bridge error events | `server/services/uiStateImpl.ts` | v1 queues commands. v2 uses reactive events. |
|
|
2378
|
+
|
|
2379
|
+
---
|
|
2380
|
+
|
|
2381
|
+
## Appendix J: Gap Analysis — v1 Features vs v2 Plan
|
|
2382
|
+
|
|
2383
|
+
What breaks, what's dropped intentionally, what needs rethinking. All drop/keep decisions
|
|
2384
|
+
are already reflected in the Decisions table and Phase tasks above — this appendix provides
|
|
2385
|
+
the detailed rationale and per-app compatibility analysis.
|
|
2386
|
+
|
|
2387
|
+
### Per-app v2 compatibility
|
|
2388
|
+
|
|
2389
|
+
| App | v2 compatibility | Key gaps |
|
|
2390
|
+
|-----|-----------------|----------|
|
|
2391
|
+
| **minimal** | ~95% | FileTree needs prop-based variant (solved by Tier 3 standalone) |
|
|
2392
|
+
| **custom-layout** | ~90% | Same as minimal — 3-column CSS grid + standalone components |
|
|
2393
|
+
| **chat** | ~20% (by design) | 80% is agent-package code. Workspace provides layout shell + artifact surface. |
|
|
2394
|
+
| **agent-backend** | ~15% (by design) | Layout toggle dropped (app implements). Capability fetching is app shell's job. |
|
|
2395
|
+
| **ide** | ~5% (by design) | 95% is app-shell code. Auth, routing, cloud features are not workspace's concern. |
|
|
2396
|
+
| **agent-frontend** | DROPPED (permanent) | Offline mode (LightningFS, isomorphic-git, Pyodide) permanently dropped. No DataProvider abstraction — HTTP-only is the final architecture. |
|
|
2397
|
+
|
|
2398
|
+
Low compatibility percentages are expected and correct — they reflect the boundary shift
|
|
2399
|
+
where auth, routing, cloud, and agent logic move to their proper owners (app shell, `@boring/agent`, `@boring/cloud`).
|
|
2400
|
+
|
|
2401
|
+
**Ninth-pass gap notes:**
|
|
2402
|
+
- **Terminal/PTY**: No longer used in any app. Dead code in v1 — not a migration concern.
|
|
2403
|
+
- **Git sidebar**: Dropped entirely. Agent owns all git UI. File tree is files-only.
|
|
2404
|
+
- **Tool-to-artifact bridge**: Agent must explicitly call `bridge.openPanel()` — workspace
|
|
2405
|
+
no longer infers panel type from tool results. Requires agent-side update.
|
|
2406
|
+
- **Layout toggle (IDE ↔ Chat)**: App implements conditional rendering. No workspace support.
|
|
2407
|
+
- **Offline/local-first**: Permanently gone. No path to re-add without DataProvider abstraction.
|
|
2408
|
+
|
|
2409
|
+
### Custom events inventory (v1 → v2 replacement)
|
|
2410
|
+
|
|
2411
|
+
v1 uses `window.dispatchEvent(new CustomEvent(...))` for loose coupling. v2's bridge replaces most:
|
|
2412
|
+
|
|
2413
|
+
| Event | v2 replacement |
|
|
2414
|
+
|-------|----------------|
|
|
2415
|
+
| `boring-ui:user-settings-open` | App shell responsibility |
|
|
2416
|
+
| `boring-ui:sync-interval-changed` | Dropped (agent owns sync) |
|
|
2417
|
+
| `boring-ui:agent-prompt` | `bridge.sendMessage()` |
|
|
2418
|
+
| `boring-ui:shell-state` | `store.subscribe()` |
|
|
2419
|
+
| `theme-toggle-request` | `useTheme()` hook |
|
|
2420
|
+
| `bui:openFile` | `bridge.openFile()` |
|
|
2421
|
+
| PI session events (4) | Agent package responsibility |
|
|
2422
|
+
|
|
2423
|
+
### localStorage key inventory (namespace collision avoidance)
|
|
2424
|
+
|
|
2425
|
+
v1 scatters state across 12+ keys. v2 consolidates to `boring-ui-v2:layout` + `boring-ui-v2:preferences`.
|
|
2426
|
+
Know v1 patterns to avoid collisions during coexistence:
|
|
2427
|
+
|
|
2428
|
+
| v1 key pattern | v2 status |
|
|
2429
|
+
|----------------|-----------|
|
|
2430
|
+
| `boring-ui:{prefix}:{projectRoot}:layout` | → `boring-ui-v2:layout` |
|
|
2431
|
+
| `boring-ui:{prefix}:{projectRoot}:tabs` | Dropped (derived from layout) |
|
|
2432
|
+
| `boring-ui:{prefix}:{projectRoot}:lastKnownGoodLayout` | Dropped |
|
|
2433
|
+
| `boring-ui-theme` / `kurt-web-theme` | → `boring-ui-v2:preferences` |
|
|
2434
|
+
| `boring-ui:chat-sessions:v1` | Agent package responsibility |
|
|
2435
|
+
| `boring-ui:terminal-*` | Dropped (terminal → agent) |
|
|
2436
|
+
|
|
2437
|
+
### IDE boot flow boundary (app shell vs workspace)
|
|
2438
|
+
|
|
2439
|
+
```
|
|
2440
|
+
main.jsx → App.jsx → useCapabilities → useWorkspaceAuth → useWorkspaceRouter
|
|
2441
|
+
→ useDataProviderScope → useResolvedCapabilities → PageRouter
|
|
2442
|
+
→ IdeLayout OR ChatCenteredWorkspace
|
|
2443
|
+
```
|
|
2444
|
+
|
|
2445
|
+
| Step | v2 owner |
|
|
2446
|
+
|------|----------|
|
|
2447
|
+
| Capabilities fetch | App shell |
|
|
2448
|
+
| Auth flow (OIDC, session) | App shell |
|
|
2449
|
+
| Workspace routing | App shell |
|
|
2450
|
+
| Data provider scope | Workspace (`WorkspaceProvider`) |
|
|
2451
|
+
| Resolved capabilities | Workspace (registry filters) |
|
|
2452
|
+
| Layout selection | App shell |
|
|
2453
|
+
| Layout rendering | Workspace |
|
|
2454
|
+
|
|
2455
|
+
### CSS audit: v1 breakpoints to parameterize
|
|
2456
|
+
|
|
2457
|
+
v1's `base.css` has hardcoded pixel breakpoints. v2 parameterizes all via tailwind config:
|
|
2458
|
+
|
|
2459
|
+
| Breakpoint | v1 purpose | v2 approach |
|
|
2460
|
+
|------------|-----------|-------------|
|
|
2461
|
+
| `56px` | Sidebar collapse width | `collapsedWidth` in GroupConfig |
|
|
2462
|
+
| `420px` | Surface min-width | `constraints.minWidth` in GroupConfig |
|
|
2463
|
+
| `1180px` | Compact layout trigger | tailwind `lg` breakpoint |
|
|
2464
|
+
| `960px` | Mobile layout trigger | tailwind `md` breakpoint |
|
|
2465
|
+
|
|
2466
|
+
### Tiptap extension inventory (detailed)
|
|
2467
|
+
|
|
2468
|
+
| Extension | Package | v2 status |
|
|
2469
|
+
|-----------|---------|-----------|
|
|
2470
|
+
| StarterKit | `@tiptap/starter-kit` | **KEEP** |
|
|
2471
|
+
| Underline | `@tiptap/extension-underline` | **KEEP** |
|
|
2472
|
+
| Link | `@tiptap/extension-link` | **KEEP** |
|
|
2473
|
+
| Placeholder | `@tiptap/extension-placeholder` | **KEEP** |
|
|
2474
|
+
| TaskList + TaskItem | `@tiptap/extension-task-list`, `-task-item` | **KEEP** |
|
|
2475
|
+
| TextAlign | `@tiptap/extension-text-align` | **KEEP** |
|
|
2476
|
+
| Highlight | `@tiptap/extension-highlight` | **KEEP** |
|
|
2477
|
+
| Image | `@tiptap/extension-image` | **REPLACE** (official, no resize handles) |
|
|
2478
|
+
| CodeBlockLowlight | `@tiptap/extension-code-block-lowlight` | **KEEP** |
|
|
2479
|
+
| Markdown | `@tiptap/markdown` | **KEEP** |
|
|
2480
|
+
| Custom DiffExtension | Inline in Editor.jsx | **DEFER** (tenth pass: dropped with diff mode. Returns when git diff viewing ships.) |
|
|
2481
|
+
| ~~Table suite~~ | `@tiptap/extension-table*` (4 packages) | **DROP** |
|
|
2482
|
+
| ~~ImageResize~~ | `tiptap-extension-resize-image` | **DROP** |
|
|
2483
|
+
|
|
2484
|
+
**Result: 10 extensions (down from 16). Dropped 6 packages (including DiffExtension — deferred with diff mode).**
|
|
2485
|
+
|
|
2486
|
+
### Deep import anti-pattern (v1 → v2 fix)
|
|
2487
|
+
|
|
2488
|
+
v1 apps use 11+ deep imports bypassing the public API. v2's `index.ts` exports everything
|
|
2489
|
+
apps need — zero deep imports required (see §A. Public API).
|