@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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +94 -0
  3. package/dist/CodeEditor-DQqOn4xz.js +266 -0
  4. package/dist/CommandPalette-aM61U-b0.js +5229 -0
  5. package/dist/FileTree-DRq_bfue.js +245 -0
  6. package/dist/MarkdownEditor-DjiHxnRv.js +349 -0
  7. package/dist/WorkspaceLoadingState-By0dZoPD.js +568 -0
  8. package/dist/agent-tool-NvxKfist.d.ts +28 -0
  9. package/dist/app-front.d.ts +485 -0
  10. package/dist/app-front.js +452 -0
  11. package/dist/app-server.d.ts +53 -0
  12. package/dist/app-server.js +769 -0
  13. package/dist/bootstrapServer-BRUqUpVW.d.ts +66 -0
  14. package/dist/boring-workspace.css +1 -0
  15. package/dist/charts.d.ts +114 -0
  16. package/dist/charts.js +143 -0
  17. package/dist/events.d.ts +178 -0
  18. package/dist/events.js +88 -0
  19. package/dist/explorer-DtLUnuah.d.ts +129 -0
  20. package/dist/panel-DnvDNQac.js +6 -0
  21. package/dist/server.d.ts +84 -0
  22. package/dist/server.js +811 -0
  23. package/dist/shared.d.ts +113 -0
  24. package/dist/shared.js +11 -0
  25. package/dist/testing-e2e.d.ts +68 -0
  26. package/dist/testing-e2e.js +45 -0
  27. package/dist/testing.d.ts +464 -0
  28. package/dist/testing.js +10984 -0
  29. package/dist/utils-B6yFEsav.js +8 -0
  30. package/dist/workspace.css +5780 -0
  31. package/dist/workspace.d.ts +2119 -0
  32. package/dist/workspace.js +1884 -0
  33. package/docs/INTERFACES.md +58 -0
  34. package/docs/PLUGIN_STRUCTURE.md +162 -0
  35. package/docs/README.md +19 -0
  36. package/docs/bridge.md +135 -0
  37. package/docs/panels.md +102 -0
  38. package/docs/plans/GENERIC_EXPLORER_PLUGIN_PLAN.md +455 -0
  39. package/docs/plans/MACRO_PLUGIN_GENERIC_HELPERS_AUDIT.md +962 -0
  40. package/docs/plans/PLUGIN_OUTPUTS_ISOLATION_PLAN.md +301 -0
  41. package/docs/plans/README.md +9 -0
  42. package/docs/plans/UI_BRIDGE_OWNERSHIP_REFACTOR.md +303 -0
  43. package/docs/plans/archive/CODE_OWNERSHIP_CLEANUP_PLAN.md +387 -0
  44. package/docs/plans/archive/COMMAND_PALETTE_REGISTRY.md +814 -0
  45. package/docs/plans/archive/DECLARATIVE_LAYOUT_MIGRATION.md +277 -0
  46. package/docs/plans/archive/PLUGIN_MODEL.md +3674 -0
  47. package/docs/plans/archive/SRC_FOLDER_REORG_PLAN.md +307 -0
  48. package/docs/plans/archive/UNIFIED_EVENT_BUS.md +647 -0
  49. package/docs/plans/archive/WORKSPACE_V2_PLAN.md +2489 -0
  50. package/docs/plugins.md +158 -0
  51. package/package.json +164 -0
@@ -0,0 +1,301 @@
1
+ # Plugin outputs and workspace isolation plan
2
+
3
+ **Status:** implemented migration plan
4
+ **Owners:** workspace
5
+ **Last updated:** 2026-04-30
6
+
7
+ ## Goal
8
+
9
+ Make plugins the owners of user-facing workspace capabilities, while
10
+ keeping `@boring/workspace` as the host substrate.
11
+
12
+ The immediate correction is that a left workbench tab should not be a
13
+ hardcoded workbench behavior or an implicit `PanelConfig.placement`.
14
+ It should be an explicit plugin output. The same rule applies to file
15
+ search, file hooks, file open behavior, and file routing: those belong to
16
+ the filesystem plugin, with the workspace only providing generic
17
+ registries, hosts, event transport, and UI manipulation contracts.
18
+
19
+ ## Problems Addressed
20
+
21
+ 1. **Left tabs are too implicit.** Plugins currently express a workbench
22
+ left tab as a normal panel with `placement: "left-tab"`. That makes
23
+ the host infer semantics from a layout hint instead of consuming an
24
+ intentional plugin output.
25
+
26
+ 2. **The left pane knew filesystem details.**
27
+ `WorkbenchLeftPane` imported and rendered the filesystem file tree
28
+ directly. It also had built-in handling for the Data tab. That meant
29
+ `excludeDefaults` and plugin composition could not fully own the visible
30
+ left-tab set.
31
+
32
+ 3. **Filesystem data lived in `front/`.** `front/data` contained
33
+ filesystem-specific client methods, hooks, event invalidation, and SSE
34
+ subscription logic. Those are not generic workspace frontend
35
+ primitives; they are filesystem plugin runtime.
36
+
37
+ 4. **Event domains were not plugin-owned.** The event bus is generic
38
+ workspace substrate, but event names such as `file:changed`
39
+ make filesystem behavior look like workspace core behavior. Plugin
40
+ events must be keyed by their owning plugin id, for example
41
+ `filesystem:file.changed`.
42
+
43
+ 5. **Artifact routing hardcoded file plugin behavior.** The artifact
44
+ surface knew about `code-editor`, `markdown-editor`,
45
+ `csv-viewer`, and `empty-file-panel` fallback behavior. File type
46
+ routing should be contributed by whichever plugin handles files.
47
+
48
+ 6. **Workspace imported agent types.** The workspace shared/plugin
49
+ layer imports `@boring/agent` types and validation. That violates the
50
+ package invariant that the app shell wires agent and workspace
51
+ together.
52
+
53
+ ## Target contract
54
+
55
+ Introduce a first-class plugin output model. The exact names can be
56
+ adjusted during implementation, but the shape should be discriminated and
57
+ explicit:
58
+
59
+ ```ts
60
+ type PluginOutput =
61
+ | LeftTabOutput
62
+ | CenterPanelOutput
63
+ | CatalogOutput
64
+ | CommandOutput
65
+ | BindingOutput
66
+ | SurfaceResolverOutput;
67
+
68
+ interface LeftTabOutput {
69
+ type: "left-tab";
70
+ id: string;
71
+ title: string;
72
+ icon?: ReactNode;
73
+ component: ComponentType<LeftTabProps>;
74
+ }
75
+
76
+ interface LeftTabProps {
77
+ query: string;
78
+ rootDir?: string;
79
+ bridge: WorkspaceBridge;
80
+ }
81
+ ```
82
+
83
+ The host may keep existing `panels`, `commands`, `catalogs`, and
84
+ `bindings` arrays as temporary compatibility sugar, but plugin bootstrap
85
+ should normalize everything into explicit outputs before registering
86
+ with the lower-level registries.
87
+
88
+ ## Ownership boundaries
89
+
90
+ Workspace core owns:
91
+
92
+ - plugin bootstrap and validation
93
+ - command, catalog, panel, output, and surface-resolver registries
94
+ - `DockviewShell` and generic panel hosting
95
+ - generic left-tab host chrome
96
+ - command palette shell
97
+ - event bus transport and workspace-owned event contracts
98
+ - generic data explorer primitives, if they remain domain-neutral
99
+ - superseded for explorer panes by `GENERIC_EXPLORER_PLUGIN_PLAN.md`: generic explorer becomes a workspace-owned feature plugin once it owns pane/output contracts
100
+ - the generic resolver dispatch loop: given an open request, ask registered
101
+ resolvers in precedence order and open the returned panel config
102
+
103
+ Filesystem plugin owns:
104
+
105
+ - Files left-tab output
106
+ - Files catalog output
107
+ - filesystem client and React hooks
108
+ - file event namespace and constants (`filesystem:file.*`)
109
+ - file event stream and cache invalidation binding
110
+ - file editor panels and fallback panels
111
+ - the filesystem surface resolver: path/file requests, extension or glob
112
+ matching, mapping paths to `code-editor` / `markdown-editor` / `csv-viewer`
113
+ / fallback panels, and open-file behavior through the workspace UI command
114
+ contract
115
+
116
+ Static data / domain plugins own:
117
+
118
+ - their own left-tab outputs
119
+ - their own catalogs
120
+ - their own surface resolvers for their resource types, e.g. series,
121
+ datasets, SQL results, images, notebooks, dashboards, or app-specific
122
+ artifacts
123
+ - their own data source adapters
124
+
125
+ The app shell owns:
126
+
127
+ - the actual chat panel dependency
128
+ - agent server composition
129
+ - app-specific plugins and runtime dependencies
130
+
131
+ ## Implementation plan
132
+
133
+ ### Phase 1: Add explicit outputs
134
+
135
+ - Add `PluginOutput` types under `packages/workspace/src/shared/plugins/`.
136
+ - Add validation for output IDs, output kinds, and renderable components.
137
+ - Update plugin bootstrap to normalize legacy contribution arrays into
138
+ outputs, then fan outputs into existing registries.
139
+ - Keep existing public fields for this pass so callers do not break
140
+ while the internals move.
141
+
142
+ Acceptance:
143
+
144
+ - Existing plugins still register.
145
+ - Tests cover duplicate output IDs and invalid output shapes.
146
+
147
+ ### Phase 2: Make the left pane a generic output host
148
+
149
+ - Replace `WorkbenchLeftPane` filesystem/Data special cases with a
150
+ generic host for `left-tab` outputs.
151
+ - Pass shared `LeftTabProps` into each tab: `query`, `rootDir`, and the
152
+ workspace bridge/UI contract.
153
+ - Move the Files tab declaration into `filesystemPlugin` as a
154
+ `left-tab` output.
155
+ - Move the Data tab declaration into data catalog plugin outputs.
156
+
157
+ Acceptance:
158
+
159
+ - No direct import from `front/chrome/workbench-left` to
160
+ `plugins/filesystemPlugin`.
161
+ - Files tab disappears when the filesystem default plugin is excluded.
162
+ - Data tab appears only when a data plugin contributes it.
163
+
164
+ ### Phase 3: Move filesystem hooks into the filesystem plugin
165
+
166
+ - Move the implementation of filesystem client, hooks, event stream, and
167
+ invalidation into `plugins/filesystemPlugin/front/data`.
168
+ - Delete `front/data`; do **not** keep compatibility re-export wrappers.
169
+ Filesystem data APIs are plugin-owned and must be imported from the
170
+ filesystem plugin package surface.
171
+ - Update filesystem plugin internals and all first-party consumers to import
172
+ from `plugins/filesystemPlugin/front/data`, not from `front/data`.
173
+ - Keep generic React Query provider behavior in workspace core only if it
174
+ is still domain-neutral after the move; otherwise let the filesystem plugin
175
+ own its provider/binding.
176
+
177
+ Acceptance:
178
+
179
+ - No tracked files remain under `packages/workspace/src/front/data/`.
180
+ - No first-party code imports `front/data`.
181
+ - Filesystem data APIs remain available through the filesystem plugin public
182
+ surface, not through `front/data` compatibility wrappers.
183
+ - File tree, file search, write, move, create directory, delete, and
184
+ invalidation tests still pass.
185
+
186
+ ### Phase 4: Make plugin events composable and plugin-keyed
187
+
188
+ - Split the event map into workspace-core events plus an augmentable
189
+ plugin event map.
190
+ - Key workspace-owned events with the workspace id:
191
+ `workspace:ui.command`, `workspace:editor.save.start`, and
192
+ `workspace:editor.save.end`.
193
+ - Let the filesystem plugin contribute typed event constants and payloads:
194
+ `filesystem:file.changed`, `filesystem:file.created`,
195
+ `filesystem:file.moved`, and `filesystem:file.deleted`.
196
+ - Replace bare event strings in emitters/subscribers with the owning
197
+ contract constants.
198
+
199
+ Acceptance:
200
+
201
+ - No production code emits or subscribes to bare `file:*` or `ui:command`
202
+ names.
203
+ - The generic event bus remains in `front/events`.
204
+ - Filesystem event names are defined by the filesystem plugin.
205
+ - Existing file invalidation, open-file, dock rename/delete, and UI command
206
+ tests still pass after migrating event names.
207
+
208
+ ### Phase 5: Move artifact routing out of artifact surface
209
+
210
+ Do **not** add a filesystem-specific `FileHandlerOutput` to the shared plugin
211
+ model. That would make the shared plugin contract know about paths, files,
212
+ globs, and extensions. Instead, add a generic `SurfaceResolverOutput`:
213
+ shared workspace knows only that a plugin can resolve an open request into an
214
+ `OpenPanelConfig`; each plugin owns the request kinds and mapping rules for
215
+ its domain.
216
+
217
+ Example shape:
218
+
219
+ ```ts
220
+ interface SurfaceOpenRequest {
221
+ kind: string;
222
+ target: string;
223
+ meta?: Record<string, unknown>;
224
+ }
225
+
226
+ interface SurfacePanelResolution {
227
+ component: string;
228
+ id?: string;
229
+ title?: string;
230
+ params?: Record<string, unknown>;
231
+ score?: number;
232
+ }
233
+
234
+ interface SurfaceResolverOutput {
235
+ type: "surface-resolver";
236
+ resolver: {
237
+ id: string;
238
+ resolve(request: SurfaceOpenRequest): SurfacePanelResolution | undefined;
239
+ };
240
+ }
241
+ ```
242
+
243
+ Mapping ownership:
244
+
245
+ - Workspace core owns the resolver registry and dispatch loop only:
246
+ collect resolver results and pick the best score.
247
+ - Filesystem plugin owns `workspace.open.path` requests and maps file paths to
248
+ its panels (`code-editor`, `markdown-editor`, `csv-viewer`, fallback, etc.).
249
+ - Data catalog plugin owns `data-catalog.open-row` requests and maps rows to
250
+ its visualization panel. Catalog search remains a separate catalog output.
251
+ - Data/domain plugins own their own resource requests, for example
252
+ `{ type: "series", id: "GDP" } -> chart panel` or
253
+ `{ type: "sql-result", id: "q1" } -> table panel`.
254
+ - The app shell owns plugin order. Later plugins should be able to override
255
+ earlier/default resolvers, so an app plugin can replace filesystem's CSV
256
+ mapping or add domain-specific routing without changing workspace core.
257
+
258
+ Make artifact surface ask the surface resolver registry for an open-panel
259
+ config instead of hardcoding filesystem panel IDs or file extensions.
260
+
261
+ Acceptance:
262
+
263
+ - Artifact surface has no hardcoded filesystem panel IDs, extension maps, or
264
+ fallback panel IDs.
265
+ - Shared plugin types do not mention file/path/glob-specific handler fields.
266
+ - Filesystem plugin contributes the resolver for file/path requests.
267
+ - Excluding filesystem defaults removes filesystem file routing.
268
+ - A host/plugin can override routing by registering a later resolver.
269
+
270
+ ### Phase 6: Remove workspace-to-agent imports
271
+
272
+ - Replace workspace shared usage of `@boring/agent` types with local
273
+ structural contracts or app-shell-provided adapters.
274
+ - Move agent-tool validation out of client-facing workspace shared code.
275
+ - Keep server-side composition in app-shell/server entry points only.
276
+
277
+ Acceptance:
278
+
279
+ - `rg '@boring/agent' packages/workspace/src` returns only allowed
280
+ app-shell/server adapter locations, or zero if the wrapper is moved out.
281
+ - Typecheck catches no regressions.
282
+
283
+ ## Test plan
284
+
285
+ - `pnpm --filter @boring/workspace typecheck`
286
+ - Workspace plugin tests for output validation and bootstrap fan-out.
287
+ - `WorkspaceProvider` tests for default plugin inclusion/exclusion.
288
+ - `WorkbenchLeftPane` tests for generic left-tab hosting.
289
+ - Filesystem plugin tests for Files tab, catalog search, and open-file
290
+ command dispatch.
291
+ - Artifact surface tests for surface resolver resolution and host override.
292
+ - Browser smoke: command palette Files search opens the selected file in
293
+ the workbench with Enter.
294
+
295
+ ## Non-goals for this pass
296
+
297
+ - No npm plugin loading changes.
298
+ - No plugin dependency graph.
299
+ - No route-as-plugin-output support.
300
+ - No deletion of compatibility wrapper files until the migration is
301
+ proven and explicitly approved.
@@ -0,0 +1,9 @@
1
+ # Workspace Plans
2
+
3
+ Active ownership records live in this folder.
4
+
5
+ - `GENERIC_EXPLORER_PLUGIN_PLAN.md` - generic explorer plugin shape and front/plugin ownership audit.
6
+ - `MACRO_PLUGIN_GENERIC_HELPERS_AUDIT.md` - macro plugin audit for reusable explorer/surface/event helper extraction.
7
+ - `PLUGIN_OUTPUTS_ISOLATION_PLAN.md` - plugin ownership and isolation plan.
8
+ - `UI_BRIDGE_OWNERSHIP_REFACTOR.md` - UI bridge ownership decision.
9
+ - `archive/` - superseded implementation plans kept for history.
@@ -0,0 +1,303 @@
1
+ # UI bridge ownership refactor — move UI tools out of `@boring/agent`
2
+
3
+ **Status:** implemented; kept as ownership record
4
+ **Owners:** workspace, agent
5
+ **Last updated:** 2026-05-01
6
+
7
+ > Naming note, 2026-05-01: this ownership record predates the workspace app
8
+ > split. Current public server composition is `createWorkspaceAgentServer` from
9
+ > `@boring/workspace/app/server`; older `createWorkspaceAgentApp` references
10
+ > below describe the same boundary before the front/server naming cleanup.
11
+ >
12
+ > Current shared contracts live in `packages/workspace/src/shared/ui-bridge.ts`.
13
+ > Current server wiring lives in
14
+ > `packages/workspace/src/app/server/createWorkspaceAgentServer.ts` and
15
+ > `packages/workspace/src/server/ui-control`.
16
+
17
+ ## Problem
18
+
19
+ `@boring/agent` currently owns four things that are conceptually workspace concerns:
20
+
21
+ 1. **`UiBridge` interface + `UiState` / `UiCommand` types** in `src/shared/ui-bridge.ts`. The discriminated union in `UiCommand` (`openFile` / `openPanel` / `closePanel` / `navigateToLine` / `expandToFile` / `showNotification`) describes operations on a workspace, not on an LLM harness.
22
+ 2. **`createInMemoryBridge()`** in `src/server/ui-bridge/createInMemoryBridge.ts`. The bridge is the message queue between "frontend pushed UI state" and "agent dispatched UI command" — both endpoints of the bridge are workspace concerns.
23
+ 3. **`uiRoutes` plugin** in `src/server/http/routes/ui.ts`. Serves `/api/v1/ui/*` (PUT state, POST commands, SSE drain). Today this is registered inside `createAgentApp`.
24
+ 4. **`createGetUiStateTool` / `createExecUiTool`** in `src/server/catalog/standardCatalog.ts`. The `standardCatalog` factory takes a `uiBridge?: UiBridge` parameter and conditionally appends the two tools. `createAgentApp` constructs an in-memory bridge and passes it.
25
+
26
+ Symptoms:
27
+
28
+ - Standalone `@boring/agent` (CLI mode, no workspace) ships UI tools and HTTP routes the harness can never fulfill — wasted bundle, misleading capability surface for non-workspace consumers.
29
+ - `standardCatalog`'s `uiBridge?` branch is the only place where the catalog has a knob; everything else is fixed. Asymmetric.
30
+ - The agent package's "shared" types are mixed: `AgentTool` (truly generic) sits next to `UiCommand` (workspace-specific). Future readers can't tell which is which.
31
+ - Adding a workspace-specific tool today (e.g., `query_data_catalog`, `focus_chart`) means either bolting it into agent's `standardCatalog` or threading it through `extraTools` from the app shell. Workspace doesn't currently have a clean place to define server-side tool factories.
32
+
33
+ ## Goal
34
+
35
+ `@boring/agent` becomes a pure tool harness. It knows nothing about UI bridges, UI state shapes, or workspace-specific commands. It exposes:
36
+
37
+ - A generic `AgentTool` interface
38
+ - `createAgentApp(opts)` — boots Fastify with the LLM loop, chat persistence, file/bash/edit/read/write/find/grep tools, and `/api/v1/agent/*` routes
39
+ - An `extraTools?: AgentTool[]` option as the only seam for hosts to add tools
40
+ - Returns the `FastifyInstance` so the host can register additional plugins
41
+
42
+ `@boring/workspace` owns everything UI-bridge-related:
43
+
44
+ - The `UiBridge` interface, `UiCommand` discriminated union, `UiState` shape (in a new `@boring/workspace/shared` subpath — needed on both client and server, so it lives outside both bundles).
45
+ - `createInMemoryBridge()` impl.
46
+ - `uiRoutes` Fastify plugin.
47
+ - `createGetUiStateTool` / `createExecUiTool` factories.
48
+ - A convenience wrapper `createWorkspaceAgentApp(opts)` that builds the bridge, builds tools that close over it, registers `uiRoutes`, and delegates everything else to `createAgentApp`.
49
+
50
+ App shells get one import:
51
+
52
+ ```ts
53
+ import { createWorkspaceAgentApp } from "@boring/workspace/server"
54
+
55
+ const app = await createWorkspaceAgentApp({
56
+ workspaceRoot: process.cwd(),
57
+ mode: "local",
58
+ })
59
+ ```
60
+
61
+ Standalone agent users (CLI, non-workspace embedders) keep using `createAgentApp` directly and get zero UI surface — smaller bundle, honest contract.
62
+
63
+ ## Dependency direction (verify no cycle)
64
+
65
+ After refactor:
66
+
67
+ ```
68
+ app shell
69
+ └─→ @boring/workspace/server
70
+ └─→ @boring/agent/server (createAgentApp, FastifyInstance type)
71
+ └─→ @boring/agent/shared (AgentTool interface, ToolResult — only the truly generic tool types stay here)
72
+ └─→ @boring/workspace/shared (UiBridge / UiCommand / UiState)
73
+ @boring/workspace/front
74
+ └─→ @boring/workspace/shared (for UiCommand type)
75
+ └─→ @boring/agent/ui-shadcn (ChatPanel — unchanged)
76
+ @boring/agent (no edges into workspace, never)
77
+ ```
78
+
79
+ `@boring/workspace/server` imports from `@boring/agent`. The reverse never happens.
80
+
81
+ `@boring/boring-macro` (existing) imports `AgentTool` + `ToolResult` from `@boring/agent/shared` — those are pure tool types and stay in agent. No change.
82
+
83
+ `@boring/agent/front-shadcn/ChatPanel.tsx` currently has a `bridge?: UiBridge` prop that is **unused in the implementation**. The prop is destructured at the top of `ChatPanel` but never threaded into any child. It's dead-code that gives the impression UiBridge is a ChatPanel concern. **Drop the prop entirely** as part of this PR — removes the last surface in agent that references UiBridge.
84
+
85
+ `@boring/core` does not import `UiBridge` / `UiCommand` / `UiState` from anywhere (verified via repo-wide grep). After refactor, core depends only on `@boring/workspace/front` (existing) and never transitively pulls fastify, since workspace's front bundle is split from its server bundle (see Bundling section).
86
+
87
+ ## Scope
88
+
89
+ ### In scope
90
+
91
+ - File moves listed below.
92
+ - `standardCatalog` API change — drop `uiBridge?` from both the parameter destructure AND the `ToolCatalog` interface definition (per Gemini: "remove from the interface, not just the function").
93
+ - `createAgentApp` — drop `uiRoutes` registration, drop the `createInMemoryBridge()` call. Keep `extraTools`.
94
+ - `@boring/agent`'s `front-shadcn/ChatPanel` — drop the unused `bridge?: UiBridge` prop and the corresponding type import.
95
+ - Workspace package: new `./shared` and `./server` exports, new `tsconfig.server.json` and `tsconfig.front.json`, new `fastify` dependency.
96
+ - App shell migration: `apps/workspace-playground/vite.config.ts` switches from `createAgentApp` to `createWorkspaceAgentApp`.
97
+ - All affected tests move with their code; one new agent-side regression test asserts UI tools are NOT in the standalone catalog.
98
+
99
+ ### Out of scope
100
+
101
+ - Adding new tools or new command kinds.
102
+ - Changing the wire protocol (SSE format, PUT body shape).
103
+ - The deferred Gemini-review items from earlier round: Last-Event-ID seq replay, multi-tab session keying, promise-based `whenReady` to replace double-RAF.
104
+ - Touching `@boring/core` or any core integration.
105
+
106
+ ## File moves and edits
107
+
108
+ ### Files moved out of `@boring/agent`
109
+
110
+ | From | To |
111
+ |------|----|
112
+ | `packages/agent/src/shared/ui-bridge.ts` | `packages/workspace/src/shared/ui-bridge.ts` |
113
+ | `packages/agent/src/server/ui-bridge/createInMemoryBridge.ts` | `packages/workspace/src/server/ui-bridge/createInMemoryBridge.ts` |
114
+ | `packages/agent/src/server/ui-bridge/__tests__/createInMemoryBridge.test.ts` | `packages/workspace/src/server/ui-bridge/__tests__/createInMemoryBridge.test.ts` |
115
+ | `packages/agent/src/server/http/routes/ui.ts` | `packages/workspace/src/server/http/uiRoutes.ts` |
116
+ | The `createGetUiStateTool` + `createExecUiTool` factories from `packages/agent/src/server/catalog/standardCatalog.ts` (lines ~19-90) | new `packages/workspace/src/server/uiTools.ts` |
117
+ | `packages/agent/src/server/catalog/tools/__tests__/uiTools.test.ts` | `packages/workspace/src/server/__tests__/uiTools.test.ts` |
118
+
119
+ ### Files edited in `@boring/agent`
120
+
121
+ - `src/server/catalog/standardCatalog.ts` — drop `uiBridge?` from the destructured `ToolCatalog` deps AND from the `ToolCatalog` interface definition. Drop the `if (uiBridge) {...}` branch. The catalog signature shrinks; no other behavioural change.
122
+ - `src/server/createAgentApp.ts` — delete:
123
+ - `import { uiRoutes } from './http/routes/ui'`
124
+ - `import { createInMemoryBridge } from './ui-bridge/createInMemoryBridge'`
125
+ - `const uiBridge = createInMemoryBridge()` line
126
+ - `await app.register(uiRoutes, { bridge: uiBridge })` line
127
+ - The `uiBridge` arg to `standardCatalog({ ...runtimeBundle, uiBridge })` — becomes `standardCatalog(runtimeBundle)`.
128
+ - `src/front-shadcn/ChatPanel.tsx` — drop `bridge?: UiBridge` from `ChatPanelProps`, drop the destructure, drop the `import type { UiBridge }` line.
129
+ - `src/server/__tests__/createAgentApp.test.ts` — three tests added in commit `01bf41f` (`createAgentApp registers get_ui_state and exec_ui in the catalog`, `PUT /api/v1/ui/state is round-tripped by GET`, `exec_ui-style POST /api/v1/ui/commands enqueues for drain`) **move to workspace** — they now assert against `createWorkspaceAgentApp`. Add **one new test in agent** asserting the standalone agent's catalog does NOT include UI tools AND `/api/v1/ui/state` returns 404 — regression test pinning the new contract.
130
+ - `src/index.ts` / `src/server/index.ts` / `src/shared/index.ts` — remove any re-exports of `UiBridge`, `UiCommand`, `UiState`, `createInMemoryBridge`.
131
+
132
+ ### Files added in `@boring/workspace`
133
+
134
+ - `packages/workspace/src/shared/index.ts` — re-export `UiBridge`, `UiCommand`, `UiState`, `CommandResult` types from the moved `ui-bridge.ts`. **Strict isolation rule (build-enforced — see Bundling section): zero imports from `../server/**` or `../components/**` or `../front/**`.**
135
+ - `packages/workspace/src/server/index.ts` — public server-side surface:
136
+ ```ts
137
+ export { createWorkspaceAgentApp } from "./createWorkspaceAgentApp"
138
+ export { createInMemoryBridge } from "./ui-bridge/createInMemoryBridge"
139
+ export { uiRoutes } from "./http/uiRoutes"
140
+ export { createGetUiStateTool, createExecUiTool, createWorkspaceUiTools } from "./uiTools"
141
+ export type * from "../shared"
142
+ ```
143
+ - `packages/workspace/src/server/createWorkspaceAgentApp.ts` — the wrapper:
144
+ ```ts
145
+ import { createAgentApp, type CreateAgentAppOptions } from "@boring/agent/server"
146
+ import type { FastifyInstance } from "fastify"
147
+ import { createInMemoryBridge } from "./ui-bridge/createInMemoryBridge"
148
+ import { createWorkspaceUiTools } from "./uiTools"
149
+ import { uiRoutes } from "./http/uiRoutes"
150
+
151
+ export async function createWorkspaceAgentApp(
152
+ opts: CreateAgentAppOptions = {},
153
+ ): Promise<FastifyInstance> {
154
+ const bridge = createInMemoryBridge()
155
+ const tools = createWorkspaceUiTools(bridge)
156
+ const app = await createAgentApp({
157
+ ...opts,
158
+ extraTools: [...(opts.extraTools ?? []), ...tools],
159
+ })
160
+ await app.register(uiRoutes, { bridge })
161
+ return app
162
+ }
163
+ ```
164
+ - `packages/workspace/src/server/uiTools.ts` — the moved tool factories plus a `createWorkspaceUiTools(bridge)` convenience that returns both as an `AgentTool[]`.
165
+
166
+ ### Bundling — explicit plan (per Gemini, "verify during implementation" was insufficient)
167
+
168
+ Workspace today is a single browser-targeted bundle. After refactor it must produce three separate output trees:
169
+
170
+ | Bundle | Source | Targets | Allowed runtime |
171
+ |--------|--------|---------|-----------------|
172
+ | `dist/index.js` (front) | `src/components`, `src/dock`, plugin-owned frontend data such as `src/plugins/filesystemPlugin/front/data`, etc. | browser | DOM, React, Tailwind. NO node built-ins, NO fastify. |
173
+ | `dist/server/index.js` | `src/server` | node | Fastify, node http/streams. NO React, NO DOM. |
174
+ | `dist/shared/index.js` | `src/shared` | both | Pure types only, no runtime imports. |
175
+
176
+ Concrete changes:
177
+
178
+ 1. **`packages/workspace/package.json`**:
179
+ - Add `"fastify"` and `"zod"` (uiRoutes uses zod) as direct `dependencies`. Currently agent ships these and we'd transitively pull through; making them direct removes the implicit assumption that agent's deps are stable.
180
+ - Add `"@boring/agent": "workspace:*"` already exists in deps (verified). No change.
181
+ - `"exports"` map updated:
182
+ ```jsonc
183
+ {
184
+ ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" },
185
+ "./globals.css": "./dist/globals.css",
186
+ "./ui-shadcn": { "import": "./dist/ui-shadcn/index.js", "types": "./dist/ui-shadcn/index.d.ts" },
187
+ "./shared": { "import": "./dist/shared/index.js", "types": "./dist/shared/index.d.ts" },
188
+ "./server": { "import": "./dist/server/index.js", "types": "./dist/server/index.d.ts" }
189
+ }
190
+ ```
191
+ - `"files"` array includes `"dist"` already; no change needed.
192
+ - Add `"sideEffects": false` if not present so client bundles tree-shake cleanly.
193
+
194
+ 2. **TypeScript split**: replace single `tsconfig.json` with three:
195
+ - `tsconfig.front.json` — `lib: ["DOM","DOM.Iterable","ES2022"]`, includes `src/components/**`, `src/shared/**`, excludes `src/server/**`.
196
+ - `tsconfig.server.json` — `lib: ["ES2022"]`, includes `src/server/**`, `src/shared/**`. Adds `@types/node` to `types`. Excludes `src/components/**`.
197
+ - `tsconfig.json` — root project-references config that points to both. CI typecheck runs both.
198
+ - **Why**: prevents DOM types leaking into server code (silent `window` access at compile time) and node types leaking into front code (silent `process.env` access). Both are real bugs that strict tsconfig-split catches at typecheck time.
199
+
200
+ 3. **Build pipeline**: workspace currently uses Vite for the bundle. The server output should use a separate config or a separate tsup step. **Open question**: prefer Vite (consistent tooling) or tsup (simpler for pure-Node ESM). Lean: tsup for the server bundle — it's what `@boring/agent` already uses and matches the node-target ergonomics.
201
+
202
+ 4. **Build-time isolation check** (per Gemini, prevents `shared` accidentally importing from `server`):
203
+ - Add a small script `scripts/assert-bundle-isolation.mjs` that:
204
+ - Parses `dist/shared/index.js` AST and asserts no `import` / `require` references `fastify`, `@fastify/*`, `node:*`.
205
+ - Parses `dist/index.js` (front) AST and asserts no `import` references `fastify`, `@fastify/*`, `node:fs`, `node:http`, `@boring/workspace/server`.
206
+ - Wire as a `postbuild` step in `workspace/package.json`. Fails the build (and CI) on a regression. Implementation is ~30 lines using `acorn` or `es-module-lexer` (both are common dev deps).
207
+
208
+ 5. **CI check** that imports `@boring/workspace` (front) in a fresh Node process and asserts `require.cache` (or ESM equivalent — `import.meta.resolve` introspection) doesn't contain `fastify`. Catches the dynamic-import-loophole that AST analysis alone can miss.
209
+
210
+ ### Test coverage additions (per Gemini)
211
+
212
+ In addition to the test migration table:
213
+
214
+ - **`extraTools` merge test** in workspace: pass an arbitrary host tool `{ name: 'host_tool', ... }` to `createWorkspaceAgentApp({ extraTools: [hostTool] })`, hit `/api/v1/agent/catalog`, assert the response contains BOTH `host_tool` AND `get_ui_state` AND `exec_ui`. Pins that the wrapper merges rather than overwrites.
215
+ - **Bundle isolation test** in workspace's CI: a scripted check (see Bundling §4) plus a runtime test (Bundling §5).
216
+ - **Regression test in agent** (per the file-edits list above) — `createAgentApp` standalone must NOT include UI tools and `/api/v1/ui/state` must 404.
217
+
218
+ ## API surface — before / after
219
+
220
+ ### Before
221
+
222
+ ```ts
223
+ // agent
224
+ import { createAgentApp } from "@boring/agent/server"
225
+ import type { UiCommand, UiState, UiBridge } from "@boring/agent/shared"
226
+
227
+ const app = await createAgentApp({ workspaceRoot, mode: "local" })
228
+ // app exposes /api/v1/agent/*, /api/v1/ui/*, includes get_ui_state + exec_ui in catalog
229
+ ```
230
+
231
+ ### After
232
+
233
+ ```ts
234
+ // agent (standalone — no UI surface)
235
+ import { createAgentApp } from "@boring/agent/server"
236
+
237
+ const app = await createAgentApp({ workspaceRoot, mode: "local" })
238
+ // app exposes /api/v1/agent/* only, catalog has bash/read/write/edit/etc. — no UI tools
239
+ ```
240
+
241
+ ```ts
242
+ // agent + workspace
243
+ import { createWorkspaceAgentApp } from "@boring/workspace/server"
244
+ import type { UiCommand, UiState } from "@boring/workspace/shared"
245
+
246
+ const app = await createWorkspaceAgentApp({ workspaceRoot, mode: "local" })
247
+ // app exposes /api/v1/agent/* AND /api/v1/ui/*, catalog includes UI tools
248
+ ```
249
+
250
+ App shell migration is mechanical: rename the import + function call.
251
+
252
+ ## Migration plan — ATOMIC single PR (revised per Gemini)
253
+
254
+ Step-by-step migration is unsafe: in a multi-commit version, the intermediate state would have BOTH `createAgentApp` registering `uiRoutes` internally AND `createWorkspaceAgentApp` registering it again, producing a `FST_ERR_DUP_ROUTE` on boot. **Land everything in one PR.**
255
+
256
+ Implementation order within the PR (driven by what compiles at each step):
257
+
258
+ 1. Create `packages/workspace/src/shared/` and copy `ui-bridge.ts` types in. Update workspace's `tsconfig.json` → `tsconfig.front.json` + `tsconfig.server.json` + root project references. Add `fastify` + `zod` to workspace `package.json`.
259
+ 2. Create `packages/workspace/src/server/` with `createInMemoryBridge.ts`, `uiTools.ts`, `http/uiRoutes.ts`, `createWorkspaceAgentApp.ts`, `index.ts`. Update workspace `package.json` `exports` to expose `./shared` and `./server`.
260
+ 3. In agent: drop `bridge?: UiBridge` prop from ChatPanel, drop UI bridge import. Drop `uiBridge?` from `standardCatalog` interface and impl. In `createAgentApp`, remove the `createInMemoryBridge()` + `app.register(uiRoutes)` lines and the imports that supported them. Delete `src/shared/ui-bridge.ts`, `src/server/ui-bridge/`, `src/server/http/routes/ui.ts`, the UI tool factories, the related tests.
261
+ 4. In agent's `createAgentApp.test.ts`: replace the three UI bridge tests with the regression test (catalog has no UI tools, `/api/v1/ui/state` is 404).
262
+ 5. Add the new tests in workspace: round-trip / queue-drain against `createWorkspaceAgentApp`, plus the `extraTools` merge test.
263
+ 6. Migrate `apps/workspace-playground/vite.config.ts` to use `createWorkspaceAgentApp`.
264
+ 7. Wire bundle isolation check (`scripts/assert-bundle-isolation.mjs` + postbuild hook + CI runtime check).
265
+ 8. Documentation pass — update `archive/WORKSPACE_V2_PLAN.md` and `agent/docs/API.md`.
266
+
267
+ CI must be green at the END of the PR. Intermediate commits within the PR may be red — the agent-only step (3) breaks until the workspace side (1, 2) is in place. That's acceptable for a single PR landing as one merge.
268
+
269
+ ## Risks and unknowns (revised)
270
+
271
+ 1. **Bundle separation** — must produce three bundles cleanly (front, server, shared). Caught via tsconfig split + AST check + runtime test. Closed under the Bundling section above.
272
+
273
+ 2. **Fastify peer/direct dep duplication** — workspace will declare `fastify` directly; agent already declares it. pnpm will hoist a single instance, so route collisions don't happen at the module-identity level. Verified during implementation that `pnpm why fastify` in `apps/workspace-playground` resolves to one instance.
274
+
275
+ 3. **`uiBridge` removal from `ToolCatalog` interface** — this is a breaking type change for any direct consumer of `ToolCatalog`. Inside the monorepo only `standardCatalog` consumes it. External consumers don't exist yet (pre-1.0). Caught via grep during step 3.
276
+
277
+ 4. **In-flight session migration** — none. The bridge is in-memory and ephemeral. Across an agent server restart all state is lost regardless. Refactor doesn't change that.
278
+
279
+ 5. **`@boring/boring-macro`** — imports `AgentTool` + `ToolResult` from `@boring/agent/shared`. Those symbols stay in agent; macro is unaffected.
280
+
281
+ ## Open questions (resolved per Gemini review)
282
+
283
+ | Q | Resolution |
284
+ |---|------------|
285
+ | 1. Multi-commit migration vs single PR? | Single PR — multi-commit hits `FST_ERR_DUP_ROUTE` in intermediate state. |
286
+ | 2. Split `UiBridge` interface from impl? | Yes — interface in `workspace/shared`, impl in `workspace/server`. Mirrors agent's existing convention. |
287
+ | 3. `toolFactories` mechanism on `createAgentApp`? | No — workspace tools close over their bridge in the wrapper. Defer until a real second use case emerges. |
288
+ | 4. Core dep on UI types? | Verified clean — core has zero imports of UI bridge surface. |
289
+ | 5. `createWorkspaceAgentApp` naming? | Keep — precisely describes "agent app + workspace UI surface". |
290
+
291
+ ## Done definition
292
+
293
+ - [ ] All file moves complete (table above).
294
+ - [ ] `pnpm --filter @boring/agent test` green; agent test suite includes the new "no UI tools, no /api/v1/ui/* route" regression test.
295
+ - [ ] `pnpm --filter @boring/workspace test` green; tests for `createWorkspaceAgentApp`, `createInMemoryBridge`, UI tool factories, AND the `extraTools` merge test all live here.
296
+ - [ ] `apps/workspace-playground/vite.config.ts` uses `createWorkspaceAgentApp`. End-to-end smoke (PUT /ui/state → get_ui_state via tool → expected payload) passes manually.
297
+ - [ ] `pnpm --filter @boring/workspace build` produces `dist/index.js` (front, no fastify), `dist/server/index.js` (server, no React), `dist/shared/index.js` (no runtime deps).
298
+ - [ ] `scripts/assert-bundle-isolation.mjs` passes as a postbuild step.
299
+ - [ ] CI runtime test confirms importing `@boring/workspace` in node does not pull `fastify` into the module graph.
300
+ - [ ] No remaining `import ... from "@boring/agent/shared"` for `UiBridge` / `UiCommand` / `UiState` anywhere in the repo.
301
+ - [ ] `@boring/agent`'s `ChatPanel` no longer references `UiBridge`.
302
+ - [ ] `archive/WORKSPACE_V2_PLAN.md` and `agent/docs/API.md` reflect the new shape.
303
+ - [ ] Single PR (multiple commits within it OK as long as the final tree is green).