@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,3674 @@
|
|
|
1
|
+
# Workspace plugin model
|
|
2
|
+
|
|
3
|
+
> **Historical plan.** This file records the plugin model design process. Use
|
|
4
|
+
> `../../INTERFACES.md` and `../../PLUGIN_STRUCTURE.md` for the current concise
|
|
5
|
+
> contract reference.
|
|
6
|
+
|
|
7
|
+
**Status:** v7.7 — round-7 governance: ChatPanel via dependency injection (NOT plugin); baseline protocol uses `git worktree` (NOT `git stash`); systemPrompt ordering reaffirmed (bootstrap first, createAgentApp second); excludeDefaults semantics corrected (UI off; tools stay); j9p7.30 sed→explicit Edits; bead-level dep + path fixes integrated.
|
|
8
|
+
|
|
9
|
+
> **2026-04-30 routing update:** any later references in this historical plan
|
|
10
|
+
> to `PanelConfig.filePatterns`, `fileFallback`, or `PanelRegistry.resolve`
|
|
11
|
+
> are superseded by generic `surface-resolver` outputs. Workspace core owns
|
|
12
|
+
> only the resolver registry and `openSurface` dispatch. Filesystem path/glob
|
|
13
|
+
> mapping lives in `plugins/filesystemPlugin/surfaceResolver.ts`; data catalog
|
|
14
|
+
> row-to-visualization mapping lives in `plugins/dataCatalogPlugin`.
|
|
15
|
+
|
|
16
|
+
Prior status (v7.6): round-5 review patches — Step 0 sequencing (move workspace into v7.5 layout BEFORE Phase 1, not after); split plugin entrypoints (index.ts client + server.ts per plugin); strict type-only imports for cross-folder Plugin refs; cleanup pack (uiBridge dedup, EmptyFilePanel relocation, A/B parallelism tightened, TL;DR scrub, tsconfig excludes, pi-tools-migration catch-up). **Meta-rule: when files move, they go DIRECTLY to final v7.6 destinations — no intermediate placements.**
|
|
17
|
+
|
|
18
|
+
> **Factory pattern:** Plugins may be exposed as factories when they need
|
|
19
|
+
> runtime config (e.g. macro's `makeMacroServerPlugin()`). v7.0 dropped the
|
|
20
|
+
> filesystemPlugin factory because the plugin no longer carries `agentTools`
|
|
21
|
+
> — it's UI-only (panels + catalog) and constructible at module load.
|
|
22
|
+
> Domain plugins with runtime deps (DB clients, etc.) still use the factory
|
|
23
|
+
> shape; the `Plugin` contract is unchanged.
|
|
24
|
+
|
|
25
|
+
**Owners:** workspace
|
|
26
|
+
**Last updated:** 2026-04-29
|
|
27
|
+
|
|
28
|
+
## TL;DR
|
|
29
|
+
|
|
30
|
+
A `Plugin` is a tagged bag of contributions for the workspace's
|
|
31
|
+
existing per-type registries (panels, commands, catalogs, agentTools).
|
|
32
|
+
Hosts compose plugins; the workspace fans them into their respective
|
|
33
|
+
registries and that's it. No lifecycle hooks, no dep graph, no
|
|
34
|
+
ordering field, no route plumbing on the contract — those are either
|
|
35
|
+
unused in Phase 1 or solved by simpler primitives (factories, npm
|
|
36
|
+
sub-path exports, `app.register(...)`).
|
|
37
|
+
|
|
38
|
+
The model unifies five fragmented host-wiring APIs into one. The
|
|
39
|
+
boring-macro-v2 migration is the acceptance test — ~260 LOC of glue
|
|
40
|
+
+ inlined UI bridge collapse to ~30 LOC of plugin definition.
|
|
41
|
+
|
|
42
|
+
## Scope of this plan
|
|
43
|
+
|
|
44
|
+
**Phase 1 (this PR's scope):**
|
|
45
|
+
|
|
46
|
+
- The `Plugin` contract + `definePlugin` factory
|
|
47
|
+
- Subscribe-aware registries (Catalog new; retrofit Command + Panel)
|
|
48
|
+
- One default plugin: `filesystemPlugin` (UI-only — panels + catalog; no agentTools per v7.0+)
|
|
49
|
+
- `<WorkspaceProvider plugins={…}>` and
|
|
50
|
+
`createWorkspaceAgentApp({ plugins })` entry points
|
|
51
|
+
- File ops shared bundle in `@boring/agent`; filesystemPlugin
|
|
52
|
+
references the same bundle (single source of truth, standalone
|
|
53
|
+
agent stays a real coding agent)
|
|
54
|
+
- UI bridge moves from `@boring/agent` to `@boring/workspace`
|
|
55
|
+
- Path-aware (not basename-only) file-pattern panel resolver
|
|
56
|
+
- `WorkbenchLeftPane` becomes registry-driven so `excludeDefaults`
|
|
57
|
+
actually removes default tabs (Files / Data)
|
|
58
|
+
- `SurfaceShell` fallback chain replaced with `EmptyFilePanel` so
|
|
59
|
+
ghost tabs don't appear when registry resolution misses
|
|
60
|
+
- `<CommandPalette />` consumes catalogs via the registry
|
|
61
|
+
- Polymorphic Recent (entries tagged with their source catalog)
|
|
62
|
+
- `<ChatCenteredShell />` migrated off legacy `data` / `extraPanels`
|
|
63
|
+
props (KEEPS `chatSuggestions` prop — it's app config, not a
|
|
64
|
+
registry contribution; see §"Why no chatSuggestions on the
|
|
65
|
+
contract")
|
|
66
|
+
- boring-macro-v2 migrated to a single inline plugin
|
|
67
|
+
|
|
68
|
+
**Phase 2 (sketched, separate PR):** npm-installable plugins via
|
|
69
|
+
package.json sub-path exports (`./client`, `./server`); pi loader
|
|
70
|
+
extension to read the wider `Plugin` shape; `/api/v1/plugins`
|
|
71
|
+
discovery endpoint for system-prompt augmentation; generic
|
|
72
|
+
`search_catalog(id, q)` agent tool; workbench data-tab catalog
|
|
73
|
+
selector.
|
|
74
|
+
|
|
75
|
+
**Phase 3 (longer-term):** agent-authored plugins; hot-reload;
|
|
76
|
+
sandboxing; capability flags; if/when needed: `dependsOn`,
|
|
77
|
+
`onMount`, etc., re-introduced as the dep graph stops being
|
|
78
|
+
trivial.
|
|
79
|
+
|
|
80
|
+
## Problem
|
|
81
|
+
|
|
82
|
+
Boring-macro-v2 — the realest "child app" we have — contributes
|
|
83
|
+
five distinct kinds of things and wires them through five different
|
|
84
|
+
APIs:
|
|
85
|
+
|
|
86
|
+
| Contribution | Macro's instance | Today's wiring |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| Panels | `chart-canvas`, `deck` | `<WorkspaceProvider panels={…}>` |
|
|
89
|
+
| Catalogs | Macro series catalog (87k FRED series) | `<ChatCenteredShell data={DataPaneConfig}>` |
|
|
90
|
+
| Agent tools | `execute_sql`, `macro_search`, `get_series_data`, `persist_derived_series` | `createAgentApp({ extraTools })` |
|
|
91
|
+
| Server routes | `registerMacroRoutes` (takes `{ clickhouse, deckRoot }`) | `app.register(registerMacroRoutes, { … })` |
|
|
92
|
+
| Commands | (none today) | (would be) `useCommandRegistry().registerCommand` |
|
|
93
|
+
|
|
94
|
+
Plus chat suggestions (a 6th but UX-bounded thing) and ~150 LOC of
|
|
95
|
+
`@boring/workspace`'s UI bridge code inlined into
|
|
96
|
+
`apps/boring-macro-v2/src/server/uiBridge.ts` (still present at 9.3
|
|
97
|
+
KB on disk — confirmed). The workspace package's server export now
|
|
98
|
+
builds; the inlined copy is dead weight that this plan deletes.
|
|
99
|
+
|
|
100
|
+
The pi-coding-agent already has a plugin loader
|
|
101
|
+
(`packages/agent/src/server/harness/pi-coding-agent/pluginLoader.ts`)
|
|
102
|
+
that handles **agent tools only** — flat `default: AgentTool[]` /
|
|
103
|
+
`tools: AgentTool[]` exports from `.pi/extensions/`,
|
|
104
|
+
`~/.pi/agent/extensions/`, and `node_modules/pi-plugin-*`. The
|
|
105
|
+
discovery infrastructure exists; the plugin shape is too narrow.
|
|
106
|
+
|
|
107
|
+
## Goal
|
|
108
|
+
|
|
109
|
+
1. **One Plugin contract.** Every contribution type that goes into
|
|
110
|
+
an aggregating registry fits into one declarative object.
|
|
111
|
+
2. **Workspace orchestrates.** Bootstrap, file-pattern resolution,
|
|
112
|
+
`excludeDefaults` opt-out all live in `@boring/workspace`.
|
|
113
|
+
3. **Composable.** File-pattern-driven panel resolution lets domain
|
|
114
|
+
plugins bind their panes to their domain paths;
|
|
115
|
+
late-wins-on-id lets hosts override anything at the abstraction
|
|
116
|
+
level above.
|
|
117
|
+
4. **Honest boundaries.** Substrate is core (HTTP plumbing,
|
|
118
|
+
registries, bridge); capabilities are plugins (file ops, data
|
|
119
|
+
catalogs, macro). Defaults auto-mount; opt-outs are explicit and
|
|
120
|
+
really take effect, including UI tabs.
|
|
121
|
+
5. **Forward-compatible.** Phase 1's inline path doesn't paint the
|
|
122
|
+
model into a corner — Phase 2's npm + pi-loader extensions slot
|
|
123
|
+
in additively.
|
|
124
|
+
|
|
125
|
+
## Decisions log (locked unless explicitly revisited)
|
|
126
|
+
|
|
127
|
+
The key architectural choices, summarized for fresh-eyes reviewers.
|
|
128
|
+
Each links to the changelog entry that locked it.
|
|
129
|
+
|
|
130
|
+
| Decision | Rationale | Locked at |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| **Pure-data Plugin contract (no lifecycle)** | All Phase 1 plugins are React-component-based or factory-injected. `onMount`/cleanup adds API surface that no Phase 1 plugin uses. | v6 |
|
|
133
|
+
| **No `dependsOn` for plugin deps** | Phase 1 has 1 declared dep total (macro → filesystem). Topo sort + dep graph not worth the contract surface; array order suffices. | v6 |
|
|
134
|
+
| **Routes off the Plugin contract** | Routes are HTTP infrastructure, not registry contributions. Mixing blurs identity AND lies about lifecycle (Fastify routes are non-unregisterable). | v6 |
|
|
135
|
+
| **`chatSuggestions` stays as a `<ChatCenteredShell>` prop** | UX cap (~6 cards) forces hosts to curate. Registry aggregation adds nothing useful. | v6.3 |
|
|
136
|
+
| **Filesystem tools are harness substrate, not plugin contributions** | "Should agent have file tools?" (harness) vs "Should UI have file tree?" (workspace) are distinct concerns at different layers. | v7.0 |
|
|
137
|
+
| **Reserved tool names** (`read`/`write`/`edit`/`find`/`grep`/`ls`/`bash`/`executeIsolatedCode`) | Harness owns these names. Plugins use domain prefixes (`macro_*`). Dev-warn on collision; no hard reject (some override IS intended). | v7.1 |
|
|
138
|
+
| **Single-pass mount; array order > topo sort** | Defaults prepended → register first; user plugins follow; late-wins-on-id handles overrides. | v6 |
|
|
139
|
+
| **Path-aware micromatch resolver with `(segments × 10) + non-wildcard chars` specificity** | Domain patterns (`deck/**/*.md`) must beat generic (`**/*.md`). Concrete formula → testable. | v6 |
|
|
140
|
+
| **Polymorphic Recent (catalogs + commands)** | VS Code/Raycast/Linear all show recent commands. Catalog-only would be a regression. | v6.3 |
|
|
141
|
+
| **`disableDefaultFileTools` (harness) vs `excludeDefaults` (workspace)** | Two switches at two layers. Conflating them was over-engineering. | v7.0 |
|
|
142
|
+
| **Per-plugin React error boundaries** | A plugin's panel can't crash the workspace shell. Failure isolation per pluginId. | v7.2 |
|
|
143
|
+
| **`Plugin.systemPrompt?: string` for LLM context augmentation** | Plugins frame their own domain to the LLM. macro tells the model "FRED database, use macro_*." Reduces host-author burden. | v7.2 |
|
|
144
|
+
| **`<PluginInspector />` ships in Phase 1 (DEV-only)** | Plugin authors debug "why didn't my command appear?" visually instead of via React DevTools. ~50 LOC, zero production cost. | v7.2 (un-cut from v6) |
|
|
145
|
+
|
|
146
|
+
If a reviewer wants to re-litigate a locked decision, they should
|
|
147
|
+
(1) read the linked changelog entry first, then (2) propose a
|
|
148
|
+
revision against the rationale documented there. Don't relitigate
|
|
149
|
+
from first principles — we've been there.
|
|
150
|
+
|
|
151
|
+
## Non-goals
|
|
152
|
+
|
|
153
|
+
- A plugin marketplace, signing, trust model, or capability sandbox.
|
|
154
|
+
- Hot-reload of plugins at runtime (Phase 1 — server boot loads,
|
|
155
|
+
client boot loads; restart to add/remove). **Server-side caveat:**
|
|
156
|
+
Fastify routes are boot-time-only and cannot be unregistered;
|
|
157
|
+
this is one of the reasons routes are NOT on the Plugin contract
|
|
158
|
+
in Phase 1.
|
|
159
|
+
- Cross-plugin dependency declarations. Plugins coordinate
|
|
160
|
+
implicitly through shared registries (panel id namespace, catalog
|
|
161
|
+
registry); they don't declare "I require plugin X." If/when a
|
|
162
|
+
real dep graph appears, add `dependsOn` then.
|
|
163
|
+
- Plugin lifecycle hooks (`onMount` / `onUnmount`). All Phase 1
|
|
164
|
+
plugins are pure declarative bags; if/when a plugin needs
|
|
165
|
+
imperative setup, add the hook then.
|
|
166
|
+
- Numeric mount ordering (`Plugin.order`). Array order does the work
|
|
167
|
+
— defaults register first, host plugins after, late-wins-on-id
|
|
168
|
+
for collisions.
|
|
169
|
+
- Replacing the agent's runtime tools (`bash`,
|
|
170
|
+
`execute_isolated_code`). Those are harness-level, not
|
|
171
|
+
workspace-level. Stay in `@boring/agent`.
|
|
172
|
+
- Per-contribution dependency declarations.
|
|
173
|
+
- Cross-environment dynamic discovery in Phase 1 (the discovery
|
|
174
|
+
endpoint is Phase 2).
|
|
175
|
+
- Inline plugin sandboxing (full host privileges; spec is a
|
|
176
|
+
structuring tool, not a security boundary).
|
|
177
|
+
- Routes as a Plugin contract field. Plugin distributors ship
|
|
178
|
+
routes via npm sub-path exports; hosts wire routes via
|
|
179
|
+
`app.register(routePlugin, opts)`. See §"Distribution".
|
|
180
|
+
- Chat suggestions as a Plugin contribution. UX caps at ~6 cards →
|
|
181
|
+
hosts curate, registry aggregation is useless. Stays as a
|
|
182
|
+
`<ChatCenteredShell>` prop.
|
|
183
|
+
|
|
184
|
+
## Design
|
|
185
|
+
|
|
186
|
+
### The Plugin contract — six fields, all data
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
// @boring/workspace/shared/plugin.ts
|
|
190
|
+
import type { AgentTool } from "@boring/agent/shared"
|
|
191
|
+
import type { ExplorerAdapter, ExplorerRow } from "@boring/workspace"
|
|
192
|
+
|
|
193
|
+
export interface Plugin {
|
|
194
|
+
/** Stable id. Convention: package or app name. Used for
|
|
195
|
+
* late-wins-on-id, debug provenance. */
|
|
196
|
+
id: string
|
|
197
|
+
|
|
198
|
+
/** Human-readable label (defaults to id). */
|
|
199
|
+
label?: string
|
|
200
|
+
|
|
201
|
+
/** Optional context prepended to the agent's system prompt at boot.
|
|
202
|
+
* Use to tell the LLM what your plugin's domain is and when its
|
|
203
|
+
* agent tools apply. Concatenated across all registered plugins
|
|
204
|
+
* (in registration order) and joined with newlines. Plain Markdown.
|
|
205
|
+
* ~200-500 chars per plugin recommended; longer eats context window.
|
|
206
|
+
* v7.2 addition. */
|
|
207
|
+
systemPrompt?: string
|
|
208
|
+
|
|
209
|
+
// Aggregating registries — every field fans into ONE registry that
|
|
210
|
+
// genuinely benefits from cross-plugin merging.
|
|
211
|
+
panels?: PanelConfig[]
|
|
212
|
+
commands?: CommandConfig[]
|
|
213
|
+
catalogs?: CatalogConfig[]
|
|
214
|
+
|
|
215
|
+
// Server-only contribution.
|
|
216
|
+
agentTools?: AgentTool[]
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
That's the entire contract. No lifecycle. No deps. No ordering. No
|
|
221
|
+
routes. No chat suggestions. No mount context. Just data.
|
|
222
|
+
|
|
223
|
+
### Concrete contribution types
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
// PanelConfig — already a discriminated union in
|
|
227
|
+
// packages/workspace/src/registry/types.ts. v6 PRESERVES the
|
|
228
|
+
// existing shape; the plugin model only ADDS fields:
|
|
229
|
+
// - 'left-tab' | 'right-tab' to placement (registry-driven tabs)
|
|
230
|
+
// - pluginId?: string (auto-set provenance)
|
|
231
|
+
// Existing fields kept verbatim: SyncPanelConfig vs LazyPanelConfig
|
|
232
|
+
// discriminated by `lazy: true | false`, requiresCapabilities,
|
|
233
|
+
// essential, chromeless, source: 'builtin' | 'app',
|
|
234
|
+
// definePanel<T>() factory.
|
|
235
|
+
|
|
236
|
+
interface PanelConfigBase {
|
|
237
|
+
id: string
|
|
238
|
+
title: string
|
|
239
|
+
icon?: ComponentType<{ className?: string }>
|
|
240
|
+
placement?: "left" | "center" | "right" | "bottom" | "left-tab" | "right-tab"
|
|
241
|
+
filePatterns?: string[] // path-aware micromatch (Step 2d)
|
|
242
|
+
requiresCapabilities?: string[]
|
|
243
|
+
essential?: boolean
|
|
244
|
+
source?: "builtin" | "app"
|
|
245
|
+
chromeless?: boolean
|
|
246
|
+
pluginId?: string // auto-set by registry
|
|
247
|
+
}
|
|
248
|
+
interface SyncPanelConfig<T = unknown> extends PanelConfigBase {
|
|
249
|
+
component: ComponentType<PaneProps<T>>
|
|
250
|
+
lazy?: false
|
|
251
|
+
}
|
|
252
|
+
interface LazyPanelConfig<T = unknown> extends PanelConfigBase {
|
|
253
|
+
component: () => Promise<{ default: ComponentType<PaneProps<T>> }>
|
|
254
|
+
lazy: true
|
|
255
|
+
}
|
|
256
|
+
type PanelConfig<T = unknown> = SyncPanelConfig<T> | LazyPanelConfig<T>
|
|
257
|
+
|
|
258
|
+
// CommandConfig — already exists; only adds pluginId.
|
|
259
|
+
type CommandConfig = {
|
|
260
|
+
id: string
|
|
261
|
+
title: string
|
|
262
|
+
shortcut?: string
|
|
263
|
+
when?: () => boolean
|
|
264
|
+
run: () => void
|
|
265
|
+
pluginId?: string
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// CatalogConfig — new.
|
|
269
|
+
type CatalogConfig = {
|
|
270
|
+
id: string
|
|
271
|
+
label: string
|
|
272
|
+
adapter: ExplorerAdapter // existing DataExplorer type
|
|
273
|
+
onSelect: (row: ExplorerRow) => void
|
|
274
|
+
pluginId?: string // auto-set by registry
|
|
275
|
+
}
|
|
276
|
+
// NOTE: v5/v6 had a `recentKind?: string` field intended for Recent
|
|
277
|
+
// fallback when the source catalog is unregistered. The spec
|
|
278
|
+
// settled on "drop orphan entries" — so recentKind would be set
|
|
279
|
+
// but never read. Cut from v6.1. Future Phase 2 "filter Recent by
|
|
280
|
+
// kind" UX can add it back as a non-breaking optional field.
|
|
281
|
+
|
|
282
|
+
// AgentTool — already exists in @boring/agent/shared
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
`pluginId` is set automatically when contributions are fanned into
|
|
286
|
+
registries; plugin code never assigns it. Late-wins-on-id collisions
|
|
287
|
+
log a dev-mode warning identifying both contributors.
|
|
288
|
+
|
|
289
|
+
### PanelConfig roles — three uses, one type (v7.3)
|
|
290
|
+
|
|
291
|
+
The `PanelConfig` type unifies three conceptually-distinct
|
|
292
|
+
contributions disambiguated by `placement`. v7.x keeps them in one
|
|
293
|
+
type for impl simplicity; **Phase 2 may go to a discriminated union**
|
|
294
|
+
(see Phase 2 sketch). Plugin authors should treat the three roles
|
|
295
|
+
as semantically separate even though they share a TypeScript type.
|
|
296
|
+
|
|
297
|
+
#### 1. Sidebar tab — `placement: 'left-tab' | 'right-tab'`
|
|
298
|
+
|
|
299
|
+
Persistent tab in `WorkbenchLeftPane` (and, reserved, the
|
|
300
|
+
not-yet-built `WorkbenchRightPane`). Always-on; user clicks to
|
|
301
|
+
activate; the tab content renders inside the sidebar pane.
|
|
302
|
+
|
|
303
|
+
**Required fields:** `id`, `title`, `component`, `placement`.
|
|
304
|
+
**Should NOT set:** `filePatterns` (sidebar tabs aren't file-routed —
|
|
305
|
+
they're navigation surfaces).
|
|
306
|
+
**Examples:**
|
|
307
|
+
- `filesystemPlugin`'s `files` tab (FileTreePanel)
|
|
308
|
+
- macroPlugin's `macro-series` tab (DataExplorer with macroAdapter)
|
|
309
|
+
|
|
310
|
+
#### 2. Workbench pane — `placement: 'center'`
|
|
311
|
+
|
|
312
|
+
Ephemeral content opened via `surface.openFile(path)` (file-pattern
|
|
313
|
+
resolver picks the panel) or `surface.openPanel({component, params})`
|
|
314
|
+
(host-author picks explicitly). Lives as a tab in the dockview
|
|
315
|
+
center area. Closes when user closes the tab.
|
|
316
|
+
|
|
317
|
+
**Required fields:** `id`, `title`, `component`, `placement: 'center'`.
|
|
318
|
+
**Often set:** `filePatterns: string[]` to drive auto-routing on
|
|
319
|
+
`openFile()`. Without filePatterns, the panel can ONLY be opened
|
|
320
|
+
explicitly via `openPanel({component: '<id>'})`.
|
|
321
|
+
**Examples:**
|
|
322
|
+
- `filesystemPlugin`'s `code-editor` (`filePatterns: ["**/*.ts", ...]`)
|
|
323
|
+
- `filesystemPlugin`'s `markdown-editor` (`filePatterns: ["**/*.md"]`)
|
|
324
|
+
- macroPlugin's `chart-canvas` (no filePatterns — opened explicitly
|
|
325
|
+
by macroSeriesPanel's `onActivate`)
|
|
326
|
+
- macroPlugin's `deck` (`filePatterns: ["deck/**/*.md"]` — beats the
|
|
327
|
+
generic markdown editor for deck files via specificity scoring)
|
|
328
|
+
|
|
329
|
+
#### 3. Bottom dock — `placement: 'bottom'`
|
|
330
|
+
|
|
331
|
+
Fixed-position panel below the workbench center area. Persistent
|
|
332
|
+
across tab changes. Suitable for terminals, consoles, log viewers.
|
|
333
|
+
|
|
334
|
+
**Required fields:** `id`, `title`, `component`, `placement: 'bottom'`.
|
|
335
|
+
**Should NOT set:** `filePatterns`.
|
|
336
|
+
**Examples (none ship in Phase 1):** a future `terminalPlugin`
|
|
337
|
+
might contribute a bottom dock.
|
|
338
|
+
|
|
339
|
+
#### Reserved / future placements
|
|
340
|
+
|
|
341
|
+
- `'right-tab'` — symmetric to `'left-tab'` but no Phase 1 component
|
|
342
|
+
consumes it (no `WorkbenchRightPane` exists). Keep the union member
|
|
343
|
+
for plugin authors who anticipate the right pane shipping.
|
|
344
|
+
- `'left'` / `'right'` (without `-tab` suffix) — legacy placements
|
|
345
|
+
for non-tabbed left/right docks. Existing in current `PanelConfig`
|
|
346
|
+
union; Phase 1 plugins don't use them.
|
|
347
|
+
|
|
348
|
+
#### Future contribution type — `Plugin.pages?: PageConfig[]`
|
|
349
|
+
|
|
350
|
+
A "page" is a **full-viewport** view that replaces the chat-centered
|
|
351
|
+
shell entirely. Conceptually similar to VS Code's `viewsContainers`
|
|
352
|
+
(activity-bar entries). Use cases: a "Settings" page, a "Reports"
|
|
353
|
+
dashboard, a multi-step "Onboarding" flow.
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
interface PageConfig {
|
|
357
|
+
id: string // 'settings', 'reports'
|
|
358
|
+
title: string
|
|
359
|
+
icon: ComponentType<{ className?: string }>
|
|
360
|
+
route?: string // '/settings' — optional URL routing
|
|
361
|
+
component: ComponentType // mounts at viewport level
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
**Out of scope for Phase 1** — no boring-* host has a real page need.
|
|
366
|
+
Phase 2/3 if/when the first plugin author proposes it.
|
|
367
|
+
|
|
368
|
+
#### Why one type today, possibly split later
|
|
369
|
+
|
|
370
|
+
The three roles share enough structure (id, title, component,
|
|
371
|
+
placement, source provenance) that splitting up-front would just
|
|
372
|
+
add fields without enforcing meaningful invariants — `filePatterns`
|
|
373
|
+
is the only role-specific field, and runtime validation
|
|
374
|
+
(`PanelRegistry.resolve` only consults `filePatterns` for `'center'`
|
|
375
|
+
panels) handles it correctly even when authors set it on the wrong
|
|
376
|
+
placement.
|
|
377
|
+
|
|
378
|
+
The split becomes worth it when:
|
|
379
|
+
- A second role-specific field appears (e.g., sidebar tabs gain
|
|
380
|
+
`tabOrder`, bottom docks gain `defaultHeight`)
|
|
381
|
+
- Plugin authors hit type-confusion bugs in practice
|
|
382
|
+
|
|
383
|
+
Phase 2's discriminated-union refactor (`SidebarTabPanel | WorkbenchPane
|
|
384
|
+
| BottomDock`) is a 2-3 hour change once we decide to do it.
|
|
385
|
+
TypeScript narrowing makes the consumer-side ergonomic
|
|
386
|
+
(`registry.resolve()` filters by kind first; sidebar-tab consumers
|
|
387
|
+
filter by kind too).
|
|
388
|
+
|
|
389
|
+
### `definePlugin(spec)` and the factory pattern
|
|
390
|
+
|
|
391
|
+
Two distribution shapes for inline plugins:
|
|
392
|
+
|
|
393
|
+
**Stateless plugins** — `definePlugin({ ... })` directly:
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
import { definePlugin } from "@boring/workspace"
|
|
397
|
+
|
|
398
|
+
export const formattingPlugin = definePlugin({
|
|
399
|
+
id: "formatting",
|
|
400
|
+
label: "Formatting",
|
|
401
|
+
commands: [{ id: "format.json", title: "Format JSON", run: () => /*…*/ }],
|
|
402
|
+
})
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
**Stateful plugins (or plugins with server deps)** — wrap in a
|
|
406
|
+
factory function. The factory captures runtime config and returns a
|
|
407
|
+
Plugin:
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
export const makeMacroPlugin = (): Plugin =>
|
|
411
|
+
definePlugin({
|
|
412
|
+
id: "boring-macro",
|
|
413
|
+
label: "Macro",
|
|
414
|
+
panels: [chartCanvasPanel, deckPanel],
|
|
415
|
+
catalogs: [seriesCatalog],
|
|
416
|
+
agentTools: macroAgentTools,
|
|
417
|
+
})
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
For plugins whose **server-side dependencies** (DB clients,
|
|
421
|
+
filesystem roots, etc.) need to be constructed at boot, the deps
|
|
422
|
+
DON'T enter the Plugin shape — they go to the route handlers the
|
|
423
|
+
host wires separately. See §"Where do routes go?" below.
|
|
424
|
+
|
|
425
|
+
### What `definePlugin` validates
|
|
426
|
+
|
|
427
|
+
Validation runs synchronously at the `definePlugin({...})` call.
|
|
428
|
+
Throwing means the plugin module fails to import — the host's build
|
|
429
|
+
or dev server reports the error with a clear stack trace.
|
|
430
|
+
|
|
431
|
+
Checks:
|
|
432
|
+
|
|
433
|
+
1. `id` is a non-empty string. (Convention: kebab-case package or
|
|
434
|
+
app name; not enforced.)
|
|
435
|
+
2. Within `panels`: each `id` is unique within this plugin; each
|
|
436
|
+
`placement` is one of the allowed values; if `lazy: true` the
|
|
437
|
+
`component` is a function returning a Promise; if `lazy:
|
|
438
|
+
false`/absent the `component` is a `ComponentType`.
|
|
439
|
+
3. Within `commands`: each `id` is unique within this plugin; `run`
|
|
440
|
+
is a function.
|
|
441
|
+
4. Within `catalogs`: each `id` is unique within this plugin;
|
|
442
|
+
`adapter.search` is a function; `onSelect` is a function.
|
|
443
|
+
5. Within `agentTools`: delegates to `validateAgentTool` (which
|
|
444
|
+
re-exports `validateTool` from `@boring/agent/shared`). Each
|
|
445
|
+
tool has non-empty `name`, `description`, `parameters` object,
|
|
446
|
+
and `execute` function.
|
|
447
|
+
|
|
448
|
+
Cross-plugin id collisions (same panel id from two different
|
|
449
|
+
plugins) are NOT errors — they're handled by late-wins-on-id at
|
|
450
|
+
registration time, with a dev-mode warning.
|
|
451
|
+
|
|
452
|
+
```
|
|
453
|
+
PluginValidationError: plugin "boring-macro": catalogs[0].adapter.search
|
|
454
|
+
must be a function (got: undefined)
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### Reserved tool names (v7.1)
|
|
458
|
+
|
|
459
|
+
The harness substrate registers a fixed set of tool names: **`bash`,
|
|
460
|
+
`executeIsolatedCode`, `read`, `write`, `edit`, `find`, `grep`,
|
|
461
|
+
`ls`** (plus any custom non-pi additions made in
|
|
462
|
+
`buildFilesystemAgentTools`). Plugin-contributed `agentTools` MUST
|
|
463
|
+
NOT use these names.
|
|
464
|
+
|
|
465
|
+
**Convention:** plugin-contributed tool names should use a
|
|
466
|
+
domain prefix (e.g., `macro_search`, `macro_execute_sql`,
|
|
467
|
+
`docs_lookup`) so they're unambiguously plugin-scoped, never
|
|
468
|
+
shadowing harness tools.
|
|
469
|
+
|
|
470
|
+
**Why an explicit rule:** today `definePlugin` accepts any name
|
|
471
|
+
without checking against harness names; the existing `mergeTools`
|
|
472
|
+
path can let plugin tools override harness tools by `name`
|
|
473
|
+
collision (`packages/agent/src/server/catalog/mergeTools.ts:30`).
|
|
474
|
+
That override behavior is intentional for the LEGACY pi loader's
|
|
475
|
+
late-wins-on-name semantics, but it's an anti-pattern for the
|
|
476
|
+
v7.1 plugin model where harness ownership is supposed to be
|
|
477
|
+
clean.
|
|
478
|
+
|
|
479
|
+
**Phase 1 enforcement:** dev-mode `console.warn` from `definePlugin`
|
|
480
|
+
when a plugin's `agentTools[].name` matches the substrate set.
|
|
481
|
+
**No hard rejection** — there are real cases (a plugin shipping a
|
|
482
|
+
specialized `read` for encrypted files) where override IS
|
|
483
|
+
intended; the warn gives an audit trail without breaking those.
|
|
484
|
+
The plugin's intent should be explicit in the plugin's docs/README.
|
|
485
|
+
|
|
486
|
+
### Plugin id collision policy — plugin-level vs contribution-level
|
|
487
|
+
|
|
488
|
+
Two distinct collision types with different semantics:
|
|
489
|
+
|
|
490
|
+
| Collision | Example | Policy | Why |
|
|
491
|
+
|---|---|---|---|
|
|
492
|
+
| Two plugins share `Plugin.id` | Two npm packages both register `id: "boring-macro"` | **Throw at registration** (`PluginError { kind: 'duplicate-id' }`) | Same plugin id = same identity. Two things claiming the same identity is an authoring bug, not a composition pattern. Hosts should rename or remove one. |
|
|
493
|
+
| Two plugins contribute same panel/command/catalog id | macro and superCoder both contribute `id: "code-editor"` | **Late-wins, dev-warn** | Composition pattern: a host plugin overrides a default. Working as intended; warn so the override is traceable. |
|
|
494
|
+
|
|
495
|
+
The plugin-level collision throws because there's no useful
|
|
496
|
+
"override the whole plugin" semantic — if you want to replace a
|
|
497
|
+
plugin, exclude it via `excludeDefaults` and register a different
|
|
498
|
+
one with a different id.
|
|
499
|
+
|
|
500
|
+
### Build/bundle invariants
|
|
501
|
+
|
|
502
|
+
A plugin module split across `plugin.client.ts` and
|
|
503
|
+
`plugin.server.ts` MUST avoid cross-import. Server modules import
|
|
504
|
+
`node:*`, Fastify types, DB clients; bundling them into the client
|
|
505
|
+
breaks the build (or worse, ships secrets to browsers).
|
|
506
|
+
|
|
507
|
+
Three enforcement strategies (any one suffices):
|
|
508
|
+
|
|
509
|
+
1. **`"use server"` / `"use client"` directives** at the top of
|
|
510
|
+
each file (RSC-style). Bundlers honor them. This is the
|
|
511
|
+
recommended approach.
|
|
512
|
+
2. **Per-environment package.json `exports`**: in npm-distributed
|
|
513
|
+
plugins (Phase 2), expose `./client` and `./server` sub-paths
|
|
514
|
+
that point at non-overlapping bundles.
|
|
515
|
+
3. **Vite/tsup conditional imports**: `import.meta.env.SSR` guards
|
|
516
|
+
server-only requires.
|
|
517
|
+
|
|
518
|
+
Inline plugins (Phase 1) typically use strategy 1 plus a barrel
|
|
519
|
+
`plugin/index.ts` that re-exports only client-safe symbols by
|
|
520
|
+
default; server entry imports `plugin/server.ts` directly when it
|
|
521
|
+
needs the server half.
|
|
522
|
+
|
|
523
|
+
The plan does NOT add static enforcement (e.g., a custom ESLint
|
|
524
|
+
rule). Reviewers + CI build catches the leak. This can change if
|
|
525
|
+
mistakes prove common.
|
|
526
|
+
|
|
527
|
+
### Plugin testability
|
|
528
|
+
|
|
529
|
+
Plugins are pure data. Testing them at three levels:
|
|
530
|
+
|
|
531
|
+
**Unit — assert contract shape:**
|
|
532
|
+
|
|
533
|
+
```ts
|
|
534
|
+
import { describe, it, expect } from "vitest"
|
|
535
|
+
import { makeMacroPlugin } from "../plugin"
|
|
536
|
+
|
|
537
|
+
describe("makeMacroPlugin", () => {
|
|
538
|
+
it("registers expected contributions", () => {
|
|
539
|
+
const p = makeMacroPlugin()
|
|
540
|
+
expect(p.id).toBe("boring-macro")
|
|
541
|
+
expect(p.panels?.map((x) => x.id)).toContain("deck")
|
|
542
|
+
expect(p.catalogs?.map((x) => x.id)).toContain("macro-series")
|
|
543
|
+
expect(p.agentTools?.map((t) => t.name)).toEqual(
|
|
544
|
+
expect.arrayContaining(["execute_sql", "macro_search"]),
|
|
545
|
+
)
|
|
546
|
+
})
|
|
547
|
+
})
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
No registries, no provider, no Fastify — just inspect the returned
|
|
551
|
+
object. `definePlugin` validation already ran at module load.
|
|
552
|
+
|
|
553
|
+
**Integration — render through `<WorkspaceProvider>`:**
|
|
554
|
+
|
|
555
|
+
```ts
|
|
556
|
+
import { renderHook } from "@testing-library/react"
|
|
557
|
+
import { WorkspaceProvider, useCatalogs } from "@boring/workspace"
|
|
558
|
+
|
|
559
|
+
const wrapper = ({ children }) => (
|
|
560
|
+
<WorkspaceProvider plugins={[makeMacroPlugin()]}>{children}</WorkspaceProvider>
|
|
561
|
+
)
|
|
562
|
+
const { result } = renderHook(() => useCatalogs(), { wrapper })
|
|
563
|
+
expect(result.current.find((c) => c.id === "macro-series")).toBeDefined()
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
Tests the bootstrap fan-in + registry subscriptions in one shot.
|
|
567
|
+
|
|
568
|
+
**Server — boot a Fastify app with the plugin:**
|
|
569
|
+
|
|
570
|
+
```ts
|
|
571
|
+
import { createWorkspaceAgentApp } from "@boring/workspace/server"
|
|
572
|
+
|
|
573
|
+
const app = await createWorkspaceAgentApp({
|
|
574
|
+
plugins: [makeMacroPlugin()],
|
|
575
|
+
workspaceRoot: tmpDir,
|
|
576
|
+
})
|
|
577
|
+
const res = await app.inject({ url: "/api/v1/catalog/agent-tools" })
|
|
578
|
+
expect(res.json()).toEqual(expect.arrayContaining([
|
|
579
|
+
expect.objectContaining({ name: "execute_sql" }),
|
|
580
|
+
]))
|
|
581
|
+
await app.close()
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
Use Fastify's `inject()` for in-process testing; no port binding.
|
|
585
|
+
|
|
586
|
+
### Convenience: `createDataCatalogPlugin(opts)`
|
|
587
|
+
|
|
588
|
+
Dropping `data: DataPaneConfig` from `<ChatCenteredShell>` removed
|
|
589
|
+
the one-liner ergonomics for hosts that just want a simple data
|
|
590
|
+
tab with their adapter (gemini P2). Restore them via the reusable
|
|
591
|
+
data catalog plugin factory exported from `@boring/workspace`:
|
|
592
|
+
|
|
593
|
+
```ts
|
|
594
|
+
import { createDataCatalogPlugin } from "@boring/workspace"
|
|
595
|
+
import { myAdapter } from "./adapter"
|
|
596
|
+
|
|
597
|
+
export const dataPlugin = createDataCatalogPlugin({
|
|
598
|
+
id: "my-data",
|
|
599
|
+
label: "Data",
|
|
600
|
+
adapter: myAdapter,
|
|
601
|
+
catalogId: "my-data",
|
|
602
|
+
})
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
Host with simple needs (single adapter, no custom panel):
|
|
606
|
+
|
|
607
|
+
```tsx
|
|
608
|
+
<WorkspaceProvider plugins={[dataPlugin]}>
|
|
609
|
+
<ChatCenteredShell />
|
|
610
|
+
</WorkspaceProvider>
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
Apps with domain-specific behavior compose around this factory:
|
|
614
|
+
boring-macro installs the data catalog outputs inside its own
|
|
615
|
+
macro plugin, then keeps chart/deck panels and macro server tools
|
|
616
|
+
in the app plugin.
|
|
617
|
+
|
|
618
|
+
### Concrete filesystemPlugin source
|
|
619
|
+
|
|
620
|
+
```ts
|
|
621
|
+
// packages/workspace/src/plugin/defaults/filesystemPlugin.ts (v7.1 — UI-only)
|
|
622
|
+
import { definePlugin, type Plugin } from "../definePlugin"
|
|
623
|
+
import { FileTreePanel, CodeEditorPanel, MarkdownEditorPanel } from "../../panels"
|
|
624
|
+
import { filesCatalog } from "./filesystemCatalog"
|
|
625
|
+
|
|
626
|
+
export const filesystemPlugin: Plugin = definePlugin({
|
|
627
|
+
id: "filesystem",
|
|
628
|
+
label: "Filesystem",
|
|
629
|
+
|
|
630
|
+
panels: [
|
|
631
|
+
{
|
|
632
|
+
id: "files",
|
|
633
|
+
title: "Files",
|
|
634
|
+
component: FileTreePanel,
|
|
635
|
+
placement: "left-tab",
|
|
636
|
+
source: "builtin",
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
id: "code-editor",
|
|
640
|
+
title: "Code",
|
|
641
|
+
component: CodeEditorPanel,
|
|
642
|
+
placement: "center",
|
|
643
|
+
filePatterns: [
|
|
644
|
+
"**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx",
|
|
645
|
+
"**/*.py", "**/*.rs", "**/*.go", "**/*.json", "**/*.yml", "**/*.yaml",
|
|
646
|
+
],
|
|
647
|
+
source: "builtin",
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
id: "markdown-editor",
|
|
651
|
+
title: "Markdown",
|
|
652
|
+
component: MarkdownEditorPanel,
|
|
653
|
+
placement: "center",
|
|
654
|
+
filePatterns: ["**/*.md", "**/*.mdx"],
|
|
655
|
+
source: "builtin",
|
|
656
|
+
},
|
|
657
|
+
],
|
|
658
|
+
|
|
659
|
+
catalogs: [filesCatalog],
|
|
660
|
+
// No agentTools field — file tools are HARNESS substrate (live in
|
|
661
|
+
// @boring/agent via pi-tools-migration's bundle factories). See
|
|
662
|
+
// §"Tools belong with the harness, not the plugin".
|
|
663
|
+
})
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
The panel ids (`code-editor`, `markdown-editor`) are the override
|
|
667
|
+
seams: a host plugin can register the same id with a
|
|
668
|
+
`SuperCoderPanel` and late-wins-on-id replaces. `source: "builtin"`
|
|
669
|
+
means user/app-source plugins win the file-pattern resolver
|
|
670
|
+
tie-breaker.
|
|
671
|
+
|
|
672
|
+
`filesCatalog` (in `filesystemCatalog.ts`) wires
|
|
673
|
+
`/api/v1/files/search` to the catalog adapter — the same backend
|
|
674
|
+
the LLM's `find` tool uses, so the cmd palette and the LLM
|
|
675
|
+
share one search engine.
|
|
676
|
+
|
|
677
|
+
### Where do routes go?
|
|
678
|
+
|
|
679
|
+
Not on the Plugin contract. Routes are HTTP infrastructure; plugins
|
|
680
|
+
are registry contributions. Mixing them blurs identity AND lies
|
|
681
|
+
about lifecycle (Fastify routes can't be unregistered, but a
|
|
682
|
+
contract field would imply they could).
|
|
683
|
+
|
|
684
|
+
**Three categories of routes** in the running app:
|
|
685
|
+
|
|
686
|
+
| Source | Who registers | Where |
|
|
687
|
+
|---|---|---|
|
|
688
|
+
| Substrate (`/api/v1/ui/*`, `/api/v1/files`, `/tree`, `/files/search`) | `createWorkspaceAgentApp` itself, always | Inside the workspace package; hosts don't see this. |
|
|
689
|
+
| Agent core (`/api/v1/chat`, `/sessions`, `/models`) | `createAgentApp` itself, always | Inside the agent package; hosts don't see this. |
|
|
690
|
+
| Plugin-specific (e.g. `/api/v1/macro/*`) | The host, via Fastify's standard `app.register(...)` | The host's server entry. One line per plugin that has routes. |
|
|
691
|
+
|
|
692
|
+
**Macro's host server entry**:
|
|
693
|
+
|
|
694
|
+
```ts
|
|
695
|
+
// apps/boring-macro-v2/src/server/index.ts
|
|
696
|
+
import { createWorkspaceAgentApp } from "@boring/workspace/server"
|
|
697
|
+
import { makeMacroPlugin } from "../plugin"
|
|
698
|
+
import { registerMacroRoutes } from "../server/macroRoutes"
|
|
699
|
+
|
|
700
|
+
const clickhouse = await createClickHouseClient(env)
|
|
701
|
+
|
|
702
|
+
const app = await createWorkspaceAgentApp({
|
|
703
|
+
plugins: [makeMacroPlugin()], // ← UI/catalog/tool contributions
|
|
704
|
+
})
|
|
705
|
+
await app.register(registerMacroRoutes, { clickhouse, deckRoot }) // ← routes (one line)
|
|
706
|
+
await app.listen({ port })
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
In Phase 1, **only macro** has plugin-specific routes (filesystem's
|
|
710
|
+
routes are substrate; dataCatalog has none; chat-shell has none).
|
|
711
|
+
So this single extra line per plugin-with-routes is the entire
|
|
712
|
+
"routes story." No new abstraction needed.
|
|
713
|
+
|
|
714
|
+
### Why no chatSuggestions on the contract
|
|
715
|
+
|
|
716
|
+
The other contribution types pass an honest aggregation test: *N
|
|
717
|
+
plugins each contribute items, all merged by a registry, all useful
|
|
718
|
+
to the user.* Chat suggestions fail that test:
|
|
719
|
+
|
|
720
|
+
- Empty-state UX caps at ~4–6 cards.
|
|
721
|
+
- N plugins × 4–6 each = 12–24, way more than the cap.
|
|
722
|
+
- Truncation forces the host to **curate** which suggestions appear
|
|
723
|
+
→ if the host curates, the registry adds nothing.
|
|
724
|
+
|
|
725
|
+
So suggestions stay where they belong: a single `ChatSuggestion[]`
|
|
726
|
+
prop on `<ChatCenteredShell>`, host writes it directly. macro's
|
|
727
|
+
plugin module re-exports the suggestions array as a regular const;
|
|
728
|
+
the host imports + passes alongside the plugin:
|
|
729
|
+
|
|
730
|
+
```tsx
|
|
731
|
+
// apps/boring-macro-v2/src/web/App.tsx
|
|
732
|
+
import { makeMacroPlugin, macroChatSuggestions } from "../plugin"
|
|
733
|
+
|
|
734
|
+
<WorkspaceProvider plugins={[makeMacroPlugin()]}>
|
|
735
|
+
<ChatCenteredShell chatSuggestions={macroChatSuggestions} />
|
|
736
|
+
</WorkspaceProvider>
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
`ChatSuggestion` lives in `@boring/agent/front-shadcn` as today; no
|
|
740
|
+
type changes. The plan does NOT delete `chatSuggestions` from
|
|
741
|
+
`<ChatCenteredShell>`'s props (reverses an earlier draft).
|
|
742
|
+
|
|
743
|
+
### Default plugins — ONE, finalized (UI-only)
|
|
744
|
+
|
|
745
|
+
| Plugin | Contributes | Why a plugin (not core) |
|
|
746
|
+
|---|---|---|
|
|
747
|
+
| **`filesystemPlugin`** | UI-only: a Files catalog (cmd palette); FileTree panel registration as `placement: 'left-tab'`; CodeEditor + MarkdownEditor panel registrations (with `filePatterns`). **No `agentTools` field** — file tools are harness substrate (see §"Tools belong with the harness, not the plugin"). | Hosts that want a chat-only UI (no file tree, no code editor opening on file click) can opt out. When excluded: file UI disappears; LLM file tools STAY (controlled separately by `disableDefaultFileTools` on `createAgentApp`). |
|
|
748
|
+
|
|
749
|
+
**v6 had a `dataCatalogPlugin` second default; v6.2 cuts it.**
|
|
750
|
+
Reason (codex round-3 P1): with `recentKind` cut and no other
|
|
751
|
+
filter, a generic "Data" tab couldn't unambiguously pick *which*
|
|
752
|
+
catalog to display when multiple plugins contribute catalogs (e.g.,
|
|
753
|
+
filesystem's Files catalog + macro's Series catalog). Rather than
|
|
754
|
+
re-add a `defaultForDataTab` flag or invent precedence, **plugins
|
|
755
|
+
that want a workbench data tab register their own `placement:
|
|
756
|
+
'left-tab'` panel** that internally renders DataExplorer with their
|
|
757
|
+
adapter. macro's plugin gets a "Macro Series" tab; filesystem
|
|
758
|
+
already has the Files tab; no generic placeholder.
|
|
759
|
+
|
|
760
|
+
If a host wants a vanilla "pick any catalog" data tab, they can
|
|
761
|
+
register one — it's not a hard substrate concern.
|
|
762
|
+
|
|
763
|
+
**Note on filesystem ROUTES:** `/api/v1/files`, `/tree`,
|
|
764
|
+
`/files/search` are **substrate**, not part of `filesystemPlugin`.
|
|
765
|
+
`createWorkspaceAgentApp` always registers them so the workspace
|
|
766
|
+
UI's HTTP plumbing works; `excludeDefaults: ['filesystem']` removes
|
|
767
|
+
the filesystem **capability** (tools + tabs + editors) but leaves
|
|
768
|
+
the HTTP plumbing available for any host or other plugin.
|
|
769
|
+
|
|
770
|
+
The single default plugin auto-mounts. Hosts opt out via:
|
|
771
|
+
|
|
772
|
+
```tsx
|
|
773
|
+
<WorkspaceProvider
|
|
774
|
+
plugins={[macroPlugin]}
|
|
775
|
+
excludeDefaults={["filesystem"]} // or [] (only filesystem is a default)
|
|
776
|
+
>
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
`excludeDefaults` is the single switch — no `includeDefaults`
|
|
780
|
+
allowlist.
|
|
781
|
+
|
|
782
|
+
#### Tools belong with the harness, not the plugin (v7.0)
|
|
783
|
+
|
|
784
|
+
Earlier drafts (v5–v6.3) had `filesystemPlugin` carry `agentTools`
|
|
785
|
+
via a factory + a "dual registration path" that suppressed
|
|
786
|
+
duplication between standalone agent and workspace hosts. v7.0
|
|
787
|
+
drops this entire arrangement. **File ops tools are harness
|
|
788
|
+
substrate, not plugin contributions.** Two separate concerns,
|
|
789
|
+
two separate switches:
|
|
790
|
+
|
|
791
|
+
| Concern | Real opt-out switch | Layer |
|
|
792
|
+
|---|---|---|
|
|
793
|
+
| Should the agent have file tools? | `disableDefaultFileTools: true` on `createAgentApp` | Harness config |
|
|
794
|
+
| Should the UI have a file tree / Files tab? | `excludeDefaults: ['filesystem']` on `<WorkspaceProvider>` | Workspace config |
|
|
795
|
+
|
|
796
|
+
These should NOT be the same switch — they live at different
|
|
797
|
+
layers. Conflating them was over-engineering.
|
|
798
|
+
|
|
799
|
+
#### Where file tools actually live (v7.0)
|
|
800
|
+
|
|
801
|
+
Per `packages/agent/docs/plans/pi-tools-migration.md` (which ships
|
|
802
|
+
before this plan), `@boring/agent` exposes:
|
|
803
|
+
|
|
804
|
+
- `buildHarnessAgentTools(bundle): AgentTool[]` — `[bash, executeIsolatedCode]`
|
|
805
|
+
- `buildFilesystemAgentTools(bundle): AgentTool[]` — `[read, write, edit, find, grep, ls]` plus any custom non-pi additions
|
|
806
|
+
|
|
807
|
+
`createAgentApp` registers both bundles by default. Opt out of
|
|
808
|
+
file ops with `disableDefaultFileTools: true`. Standalone CLI agent
|
|
809
|
+
keeps file tools because it's a coding agent — that's the harness's
|
|
810
|
+
job, not a plugin's.
|
|
811
|
+
|
|
812
|
+
`createWorkspaceAgentApp` does **not** pass
|
|
813
|
+
`disableDefaultFileTools` — it just wraps `createAgentApp`
|
|
814
|
+
unchanged + runs the plugin bootstrap on top. No dual-registration.
|
|
815
|
+
|
|
816
|
+
#### Custom (non-pi) filesystem tools
|
|
817
|
+
|
|
818
|
+
`buildFilesystemAgentTools(bundle)` is NOT restricted to pi's
|
|
819
|
+
factories. It can return pi tools + project-specific filesystem
|
|
820
|
+
tools that don't exist in pi's catalog. Examples that might land
|
|
821
|
+
here later:
|
|
822
|
+
|
|
823
|
+
- `watch_files(glob)` — long-poll for file changes (pi has no equivalent)
|
|
824
|
+
- `stat(path)` — file metadata (size, mtime, perms)
|
|
825
|
+
- `git_status` / `git_diff` — git-aware filesystem ops
|
|
826
|
+
- `multi_edit(edits[])` — atomic batch edits across many files
|
|
827
|
+
|
|
828
|
+
These would be **substrate** alongside pi's defaults — same
|
|
829
|
+
registration path, same lifecycle. They live in
|
|
830
|
+
`@boring/agent/server/tools/filesystem/` (not in
|
|
831
|
+
`filesystemPlugin`). Author wraps them as `AgentTool`; bundle
|
|
832
|
+
factory composes them into the array.
|
|
833
|
+
|
|
834
|
+
The principle from pi-tools-migration's Principle 3 still applies:
|
|
835
|
+
add custom tools only when pi cannot be made to work. But "can't
|
|
836
|
+
be made to work" includes "pi doesn't ship this capability at all."
|
|
837
|
+
|
|
838
|
+
**filesystemPlugin (v7.0) — UI-only, plain const, no factory:**
|
|
839
|
+
|
|
840
|
+
```ts
|
|
841
|
+
// packages/workspace/src/plugin/defaults/filesystemPlugin.ts
|
|
842
|
+
import { definePlugin } from "../definePlugin"
|
|
843
|
+
import { FileTreePanel, CodeEditorPanel, MarkdownEditorPanel } from "../../panels"
|
|
844
|
+
import { filesCatalog } from "./filesystemCatalog"
|
|
845
|
+
|
|
846
|
+
export const filesystemPlugin = definePlugin({
|
|
847
|
+
id: "filesystem",
|
|
848
|
+
label: "Filesystem",
|
|
849
|
+
panels: [
|
|
850
|
+
{ id: "files", title: "Files", component: FileTreePanel, placement: "left-tab", source: "builtin" },
|
|
851
|
+
{ id: "code-editor", title: "Code", component: CodeEditorPanel, placement: "center", filePatterns: [...], source: "builtin" },
|
|
852
|
+
{ id: "markdown-editor", title: "Markdown", component: MarkdownEditorPanel, placement: "center", filePatterns: ["**/*.md", "**/*.mdx"], source: "builtin" },
|
|
853
|
+
],
|
|
854
|
+
catalogs: [filesCatalog],
|
|
855
|
+
// No agentTools — file tools live with the harness in @boring/agent.
|
|
856
|
+
})
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
No `(deps)` argument. No runtime bundle dependency. Plain
|
|
860
|
+
module-scope const that imports cleanly. WorkspaceProvider /
|
|
861
|
+
createWorkspaceAgentApp prepend it as a default unless
|
|
862
|
+
`excludeDefaults: ['filesystem']` says otherwise.
|
|
863
|
+
|
|
864
|
+
No "ONE source of truth, TWO registration paths" puzzle. Just:
|
|
865
|
+
**harness owns tools; plugins own UI.**
|
|
866
|
+
|
|
867
|
+
### Core — substrate, not plugins
|
|
868
|
+
|
|
869
|
+
Always present. Not pluggable. Replacing core by accident shouldn't
|
|
870
|
+
be possible.
|
|
871
|
+
|
|
872
|
+
- Per-type registries (Catalog new; Command + Panel retrofitted
|
|
873
|
+
subscribable)
|
|
874
|
+
- EventBus (already shipped in `packages/workspace/src/events/`)
|
|
875
|
+
- UiBridge (in-memory message queue;
|
|
876
|
+
`@boring/workspace/src/bridge/`)
|
|
877
|
+
- React component primitives: `CodeEditor`, `MarkdownEditor`,
|
|
878
|
+
`FileTree`, `DataExplorer`, `EmptyPane`, `EmptyFilePanel` (the
|
|
879
|
+
components themselves are core exports; their **panel
|
|
880
|
+
registrations** with `filePatterns` belong to the relevant
|
|
881
|
+
default plugin)
|
|
882
|
+
- Default agent tools that expose the substrate: `get_ui_state`,
|
|
883
|
+
`exec_ui` (registered directly by `createWorkspaceAgentApp`)
|
|
884
|
+
- Default routes: `/api/v1/ui/*`, `/api/v1/files`, `/tree`,
|
|
885
|
+
`/files/search` (registered directly)
|
|
886
|
+
- Default commands: `toggleSidebar`, `toggleAgentPanel`, `closeTab`
|
|
887
|
+
- Chat shell + palette + workbench themselves
|
|
888
|
+
- ChatPanel mount point
|
|
889
|
+
|
|
890
|
+
**Substrate is core, capabilities are plugins.**
|
|
891
|
+
|
|
892
|
+
### Plugin composability — file-pattern resolution + late-wins
|
|
893
|
+
|
|
894
|
+
**File-pattern panel resolution.** The current resolver
|
|
895
|
+
(`PanelRegistry.ts:91`, `SurfaceShell.tsx:98`) matches **basename
|
|
896
|
+
only** via a hand-rolled `*suffix`/exact matcher. It also has a
|
|
897
|
+
working source-priority tie-breaker (`app` beats `builtin`) which
|
|
898
|
+
we PRESERVE. Phase 1 step 2 upgrades the matcher itself to
|
|
899
|
+
**path-aware micromatch** so patterns like `deck/**/*.md` actually
|
|
900
|
+
work. When `openFile(path)` runs:
|
|
901
|
+
|
|
902
|
+
1. Filter panels whose `filePatterns` include the full `path` under
|
|
903
|
+
path-aware micromatch (`{ matchBase: false, dot: true }`).
|
|
904
|
+
2. Sort by **specificity** —
|
|
905
|
+
`score = (segment_count * 10) + non_wildcard_chars`. Higher wins.
|
|
906
|
+
3. Tie-break A: `source: 'app'` beats `source: 'builtin'` (current
|
|
907
|
+
behavior, preserved).
|
|
908
|
+
4. Tie-break B: registration order, late wins.
|
|
909
|
+
5. Hosts can bypass pattern matching at the call site:
|
|
910
|
+
`surface.openPanel({ component: "<id>", … })`.
|
|
911
|
+
|
|
912
|
+
**Late-wins-on-id.** If two contributions share the same `id`, the
|
|
913
|
+
later registration wins. Combined with the convention that defaults
|
|
914
|
+
mount before host plugins, this means:
|
|
915
|
+
|
|
916
|
+
```
|
|
917
|
+
1. ADD a domain pane for a domain path
|
|
918
|
+
filesystem: { id: "markdown-editor", patterns: ["**/*.md"] }
|
|
919
|
+
macro: { id: "deck", patterns: ["deck/**/*.md"] }
|
|
920
|
+
notes.md → MarkdownEditor (default)
|
|
921
|
+
deck/labor/labor.md → DeckPane (specificity wins)
|
|
922
|
+
|
|
923
|
+
2. REPLACE a default pane (late-wins-on-id)
|
|
924
|
+
filesystem: { id: "code-editor", component: CodeEditor, patterns: ["**/*.ts"] }
|
|
925
|
+
superCoder: { id: "code-editor", component: SuperCoder, patterns: ["**/*.ts"] }
|
|
926
|
+
any *.ts → SuperCoder (same id ⇒ replaces)
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
Late-wins logs a dev-mode `console.warn` so the override is
|
|
930
|
+
traceable.
|
|
931
|
+
|
|
932
|
+
### Plugin patterns: cross-plugin communication (v7.2)
|
|
933
|
+
|
|
934
|
+
The plan deliberately omits `dependsOn` (see §Non-goals) — but
|
|
935
|
+
plugins still need to coordinate. Three canonical patterns, each
|
|
936
|
+
with a worked example:
|
|
937
|
+
|
|
938
|
+
**1. Shared registry — read another plugin's catalog.** No dep
|
|
939
|
+
edge needed; null-check + graceful degradation.
|
|
940
|
+
|
|
941
|
+
```ts
|
|
942
|
+
// Inside Plugin B's panel component
|
|
943
|
+
const catalogs = useCatalogs()
|
|
944
|
+
const filesCatalog = catalogs.find(c => c.id === "files")
|
|
945
|
+
if (filesCatalog) {
|
|
946
|
+
const result = await filesCatalog.adapter.search({
|
|
947
|
+
query: "foo", filters: {}, limit: 50, offset: 0, signal,
|
|
948
|
+
})
|
|
949
|
+
// …use result.items
|
|
950
|
+
}
|
|
951
|
+
// If filesystemPlugin isn't loaded → filesCatalog is undefined → skip.
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
**2. Event bus — emit + subscribe.** Decoupled; transitions only.
|
|
955
|
+
|
|
956
|
+
```ts
|
|
957
|
+
// Plugin A: in a panel component
|
|
958
|
+
const onClick = () =>
|
|
959
|
+
events.emit("plugin-a:thing-happened", { ts: Date.now(), userId })
|
|
960
|
+
|
|
961
|
+
// Plugin B: in another panel component
|
|
962
|
+
useEvent("plugin-a:thing-happened", (payload) => { /* react */ })
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
New event keys go in `WorkspaceEventMap` (events declared on
|
|
966
|
+
demand — see §Event bus integration).
|
|
967
|
+
|
|
968
|
+
**3. Late-wins override — replace another plugin's panel.**
|
|
969
|
+
|
|
970
|
+
```ts
|
|
971
|
+
// Plugin B (registered after Plugin A) overrides A's "code-editor"
|
|
972
|
+
definePlugin({
|
|
973
|
+
id: "super-coder",
|
|
974
|
+
panels: [{
|
|
975
|
+
id: "code-editor", // SAME id as filesystem's default
|
|
976
|
+
component: SuperCoderPanel,
|
|
977
|
+
placement: "center",
|
|
978
|
+
filePatterns: ["**/*.ts"],
|
|
979
|
+
source: "app",
|
|
980
|
+
}],
|
|
981
|
+
})
|
|
982
|
+
```
|
|
983
|
+
|
|
984
|
+
Late-wins-on-id replaces filesystem's `CodeEditorPanel` for any
|
|
985
|
+
`*.ts` file. Dev-mode `console.warn` flags the override; no hard
|
|
986
|
+
error.
|
|
987
|
+
|
|
988
|
+
**When to use which:**
|
|
989
|
+
|
|
990
|
+
| Pattern | When |
|
|
991
|
+
|---|---|
|
|
992
|
+
| Shared registry | Plugin B needs Plugin A's data (catalog, panel, command) at runtime. |
|
|
993
|
+
| Event bus | Plugin B reacts to something Plugin A does. No return value, no dependency. |
|
|
994
|
+
| Late-wins override | Plugin B replaces a default's panel/command/catalog by id. Composition pattern. |
|
|
995
|
+
|
|
996
|
+
**Anti-patterns — don't do these:**
|
|
997
|
+
|
|
998
|
+
- Direct module import between plugins (couples them; defeats the registry).
|
|
999
|
+
- Module-scope `events.on(...)` (fires globally; leaks subscriptions).
|
|
1000
|
+
- Polling for another plugin's state (use the event bus instead).
|
|
1001
|
+
|
|
1002
|
+
### Workspace orchestration — bootstrap
|
|
1003
|
+
|
|
1004
|
+
```ts
|
|
1005
|
+
// BootstrapOptions (v7.7 — chatPanel slot added)
|
|
1006
|
+
import type { ComponentType } from 'react'
|
|
1007
|
+
import type { ChatPanelProps } from '@boring/agent' // TYPE-ONLY (Inv #7)
|
|
1008
|
+
|
|
1009
|
+
export interface BootstrapOptions {
|
|
1010
|
+
/** Required. The ChatPanel implementation, value-imported by the host
|
|
1011
|
+
* app from @boring/agent. Workspace stores the slot on context; the
|
|
1012
|
+
* internal `chatPanel` chrome reads + renders it with workspace
|
|
1013
|
+
* integrations (auto-open hooks, command-stream, suggestions).
|
|
1014
|
+
* v7.7 addition. */
|
|
1015
|
+
chatPanel: ComponentType<ChatPanelProps>
|
|
1016
|
+
|
|
1017
|
+
plugins?: Plugin[]
|
|
1018
|
+
excludeDefaults?: string[]
|
|
1019
|
+
registries: {
|
|
1020
|
+
panels: PanelRegistry
|
|
1021
|
+
commands: CommandRegistry
|
|
1022
|
+
catalogs: CatalogRegistry
|
|
1023
|
+
agentTools?: AgentToolRegistry
|
|
1024
|
+
}
|
|
1025
|
+
defaults?: Plugin[]
|
|
1026
|
+
}
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
```
|
|
1030
|
+
bootstrap(opts):
|
|
1031
|
+
if !opts.chatPanel: throw — workspace will not silently fallback
|
|
1032
|
+
store opts.chatPanel on WorkspaceContext (read by ChatPanelHost chrome)
|
|
1033
|
+
|
|
1034
|
+
finalSet = [...defaultPlugins.filter(d => !excludeDefaults.includes(d.id)),
|
|
1035
|
+
...opts.plugins]
|
|
1036
|
+
|
|
1037
|
+
for each plugin in finalSet (array order):
|
|
1038
|
+
fan plugin.panels → PanelRegistry (pluginId provenance)
|
|
1039
|
+
fan plugin.commands → CommandRegistry (pluginId provenance)
|
|
1040
|
+
fan plugin.catalogs → CatalogRegistry (pluginId provenance)
|
|
1041
|
+
(server) fan plugin.agentTools → AgentToolRegistry
|
|
1042
|
+
|
|
1043
|
+
systemPromptAppend = finalSet
|
|
1044
|
+
.filter(p => p.systemPrompt?.trim())
|
|
1045
|
+
.map(p => p.systemPrompt!.trim())
|
|
1046
|
+
.join('\n\n')
|
|
1047
|
+
|
|
1048
|
+
return { registered: finalSet.map(p => p.id), systemPromptAppend }
|
|
1049
|
+
```
|
|
1050
|
+
|
|
1051
|
+
Single pass. No async. No lifecycle. No ordering contract beyond
|
|
1052
|
+
"array order." Defaults are prepended so they register first; host
|
|
1053
|
+
plugins register after; late-wins-on-id gives hosts a clean
|
|
1054
|
+
override mechanism without explicit precedence rules.
|
|
1055
|
+
|
|
1056
|
+
The retrofit applies to existing `CommandRegistry` and
|
|
1057
|
+
`PanelRegistry` — they get `subscribe()` semantics so late
|
|
1058
|
+
`registerCommand` calls reach an open palette.
|
|
1059
|
+
|
|
1060
|
+
#### Chat as core chrome — DI shape, not plugin (v7.7)
|
|
1061
|
+
|
|
1062
|
+
Chat is core: workspace lays it out, sizes it, knows where it goes.
|
|
1063
|
+
**Only the React component is injected.** The workspace package
|
|
1064
|
+
holds **zero value imports** of `@boring/agent` (Inv #7); a TYPE-ONLY
|
|
1065
|
+
import for `ChatPanelProps` is fine and grep-enforced.
|
|
1066
|
+
|
|
1067
|
+
Worked example:
|
|
1068
|
+
|
|
1069
|
+
```tsx
|
|
1070
|
+
// CONSUMING APP — value-imports ChatPanel and passes it
|
|
1071
|
+
import { ChatPanel } from '@boring/agent' // value import — host's prerogative
|
|
1072
|
+
import { WorkspaceProvider, type Plugin } from '@boring/workspace'
|
|
1073
|
+
import { myPlugin } from './plugin'
|
|
1074
|
+
|
|
1075
|
+
export const App = () => (
|
|
1076
|
+
<WorkspaceProvider chatPanel={ChatPanel} plugins={[myPlugin]}>
|
|
1077
|
+
{/* layouts, etc. */}
|
|
1078
|
+
</WorkspaceProvider>
|
|
1079
|
+
)
|
|
1080
|
+
```
|
|
1081
|
+
|
|
1082
|
+
```tsx
|
|
1083
|
+
// WORKSPACE INTERNAL — type-only import; chrome that consumes the slot
|
|
1084
|
+
// packages/workspace/src/front/chrome/chat/ChatPanelHost.tsx
|
|
1085
|
+
import type { ChatPanelProps } from '@boring/agent' // type-only
|
|
1086
|
+
import { useWorkspaceContext } from '../../WorkspaceProvider'
|
|
1087
|
+
|
|
1088
|
+
export function ChatPanelHost(props: ChatPanelProps) {
|
|
1089
|
+
const { chatPanel: ChatPanelImpl } = useWorkspaceContext()
|
|
1090
|
+
// workspace integrations: auto-open agent files, suggestions wiring, etc.
|
|
1091
|
+
return <ChatPanelImpl {...props} />
|
|
1092
|
+
}
|
|
1093
|
+
```
|
|
1094
|
+
|
|
1095
|
+
The chat chrome's `definition.ts` registers `ChatPanelHost` (not the
|
|
1096
|
+
agent's ChatPanel) into the PanelRegistry as a CORE panel. Hosts that
|
|
1097
|
+
need to swap chat impls (e.g., a stripped-down terminal renderer)
|
|
1098
|
+
just pass a different `chatPanel` prop — they don't author a plugin
|
|
1099
|
+
to do it.
|
|
1100
|
+
|
|
1101
|
+
Why this shape (vs plugin-ifying chat):
|
|
1102
|
+
|
|
1103
|
+
- **Inv #7 stays verifiable.** `grep -RE "from ['\"]@boring/agent['\"]" packages/workspace/src` finds zero non-type-import hits.
|
|
1104
|
+
- **Bootstrap stays single-purpose.** Plugins describe optional contributions; chat is non-optional core. Forcing chat into the Plugin contract would mean either (a) every host registers a "chat plugin" boilerplate-style, or (b) the workspace value-imports its own chat plugin (violating Inv #7).
|
|
1105
|
+
- **Layout knows about chat.** `ChatLayout`, `IdeLayout` reference `'chat'` panel id directly; that's correct because chat is chrome the layouts can rely on, not a maybe-present contribution.
|
|
1106
|
+
|
|
1107
|
+
### Per-plugin error boundaries (v7.2)
|
|
1108
|
+
|
|
1109
|
+
A plugin's panel that throws during render must NOT crash the
|
|
1110
|
+
workspace shell. Every plugin contribution that renders React
|
|
1111
|
+
(panels; catalog adapter row renderers; `<ChatEmptyState>` cards
|
|
1112
|
+
sourced from chatSuggestions) is wrapped in
|
|
1113
|
+
`<PluginErrorBoundary pluginId={id}>`. On error:
|
|
1114
|
+
|
|
1115
|
+
1. Boundary renders an `<ErrorChip>` showing the plugin id +
|
|
1116
|
+
short error message in place of the contribution.
|
|
1117
|
+
2. A `PluginError { kind: 'contribution', pluginId, error }` is
|
|
1118
|
+
pushed onto `WorkspaceContext.errors` (consumed by
|
|
1119
|
+
`<PluginInspector />`).
|
|
1120
|
+
3. Other plugins continue rendering unaffected.
|
|
1121
|
+
|
|
1122
|
+
Implementation sites:
|
|
1123
|
+
|
|
1124
|
+
- `<PanelHost panelId>` — wraps the resolved panel component.
|
|
1125
|
+
- `<CatalogResults>` — wraps each row renderer (so a bad row
|
|
1126
|
+
doesn't kill the palette).
|
|
1127
|
+
- The chat shell's empty-state card list — wraps each
|
|
1128
|
+
`<ChatSuggestion>` card.
|
|
1129
|
+
|
|
1130
|
+
No contract change for plugin authors. The boundary is a
|
|
1131
|
+
host-side wrapper; plugins just opt in implicitly by having their
|
|
1132
|
+
contribution mounted.
|
|
1133
|
+
|
|
1134
|
+
**Server-side parallel:** per-catalog-search try/catch already
|
|
1135
|
+
covered in §Error model. The CommandPalette's debounced search
|
|
1136
|
+
loop catches per-catalog `search()` rejections and surfaces them
|
|
1137
|
+
as inline error chips per catalog group; one bad adapter doesn't
|
|
1138
|
+
fail the entire palette query.
|
|
1139
|
+
|
|
1140
|
+
### Search semantics — debouncing + cancellation (v7.2)
|
|
1141
|
+
|
|
1142
|
+
The CommandPalette runs catalog searches on every keystroke.
|
|
1143
|
+
Without coordination this would (a) race (older search resolves
|
|
1144
|
+
after newer; UI shows stale results) and (b) waste work (5
|
|
1145
|
+
catalogs × 10 keystrokes = 50 in-flight HTTP fetches).
|
|
1146
|
+
|
|
1147
|
+
`@boring/workspace/plugin` exports `useDebouncedCatalogSearch`:
|
|
1148
|
+
|
|
1149
|
+
```ts
|
|
1150
|
+
function useDebouncedCatalogSearch(
|
|
1151
|
+
query: string,
|
|
1152
|
+
opts?: { debounceMs?: number },
|
|
1153
|
+
): {
|
|
1154
|
+
results: Map<string, ExplorerRow[]> // catalogId → rows
|
|
1155
|
+
loading: boolean
|
|
1156
|
+
errors: Map<string, Error> // catalogId → error (per-catalog)
|
|
1157
|
+
}
|
|
1158
|
+
```
|
|
1159
|
+
|
|
1160
|
+
Behavior:
|
|
1161
|
+
- Debounces 150ms by default (override via `debounceMs`).
|
|
1162
|
+
- On every new query: aborts in-flight searches via
|
|
1163
|
+
`AbortController.abort()`, fires fresh ones across all registered
|
|
1164
|
+
catalogs in parallel.
|
|
1165
|
+
- Per-catalog errors (one adapter throws) isolated — surface in
|
|
1166
|
+
`errors` map; other catalogs still return results.
|
|
1167
|
+
|
|
1168
|
+
**Adapter contract:** `ExplorerAdapter.search(args: SearchArgs)`
|
|
1169
|
+
already accepts `args.signal?: AbortSignal` (verified live at
|
|
1170
|
+
`packages/workspace/src/components/DataExplorer/types.ts:46-55`).
|
|
1171
|
+
Adapters that honor the signal get cancellation; adapters that
|
|
1172
|
+
ignore it are still safe (last-write-wins via the debounce).
|
|
1173
|
+
|
|
1174
|
+
**Plugin authors:** if your catalog hits an HTTP backend, pass
|
|
1175
|
+
`args.signal` to `fetch(url, { signal })`. If your catalog runs
|
|
1176
|
+
synchronous filtering, you can ignore the signal — debouncing
|
|
1177
|
+
handles the wasted-work case.
|
|
1178
|
+
|
|
1179
|
+
### Polymorphic Recent
|
|
1180
|
+
|
|
1181
|
+
The Command Palette today has a Recent section with a known bug
|
|
1182
|
+
(`CommandPalette.tsx:34`-`60`, `:157`, `:230-232`): it stores items
|
|
1183
|
+
uniformly as path strings and renders all entries through
|
|
1184
|
+
`FilePathLabel`. When a command is the most-recent action it
|
|
1185
|
+
renders as a (broken) file path.
|
|
1186
|
+
|
|
1187
|
+
The plugin model fixes this by tagging each Recent entry with the
|
|
1188
|
+
catalog it came from. RecentStore entries:
|
|
1189
|
+
|
|
1190
|
+
```ts
|
|
1191
|
+
type RecentEntry =
|
|
1192
|
+
| {
|
|
1193
|
+
type: "catalog"
|
|
1194
|
+
catalogId: string // ↔ CatalogConfig.id
|
|
1195
|
+
rowId: string // ↔ ExplorerRow.id within that catalog
|
|
1196
|
+
/** Snapshot of the row at time of selection. Guards against
|
|
1197
|
+
* catalog data changing under our feet (file renamed, series
|
|
1198
|
+
* re-tagged, …). Cheap (~200 bytes); essential because
|
|
1199
|
+
* adapters don't have a `getById(rowId)` method.
|
|
1200
|
+
* IMPORTANT: ExplorerRow participating in Recent MUST be
|
|
1201
|
+
* 100% JSON-serializable — see §"Recent serialization
|
|
1202
|
+
* invariant" (gemini P1). */
|
|
1203
|
+
rowSnapshot: ExplorerRow
|
|
1204
|
+
selectedAt: number // unix ms
|
|
1205
|
+
}
|
|
1206
|
+
| {
|
|
1207
|
+
type: "command"
|
|
1208
|
+
commandId: string // ↔ CommandConfig.id
|
|
1209
|
+
/** Snapshot of the command's title at time of selection,
|
|
1210
|
+
* in case the command is later unregistered. */
|
|
1211
|
+
titleSnapshot: string
|
|
1212
|
+
selectedAt: number
|
|
1213
|
+
}
|
|
1214
|
+
```
|
|
1215
|
+
|
|
1216
|
+
Render flow:
|
|
1217
|
+
|
|
1218
|
+
1. For each entry, look up the source by id (catalog by
|
|
1219
|
+
`catalogId`, command by `commandId`). If absent (plugin
|
|
1220
|
+
uninstalled, command unregistered), drop the entry — don't
|
|
1221
|
+
render orphans. Show `titleSnapshot` text only if the user
|
|
1222
|
+
needs to see what they recently used (we drop on click since
|
|
1223
|
+
we can't run a missing command).
|
|
1224
|
+
2. For `type: "catalog"`: render via the catalog's adapter row
|
|
1225
|
+
renderer; on click → `catalog.onSelect(rowSnapshot)`.
|
|
1226
|
+
3. For `type: "command"`: render the title with a small "command"
|
|
1227
|
+
chip; on click → `command.run()`.
|
|
1228
|
+
|
|
1229
|
+
**Recent covers BOTH catalog rows AND commands** (gemini P1
|
|
1230
|
+
correction — earlier drafts said "catalog-only," but every mature
|
|
1231
|
+
palette UX — VS Code, Raycast, Linear — keeps recent commands.
|
|
1232
|
+
Re-running frequent actions like "Toggle Theme" / "Format JSON"
|
|
1233
|
+
is the primary use case for many users).
|
|
1234
|
+
|
|
1235
|
+
Existing localStorage entries (`boring-ui-v2:command-palette:recent`)
|
|
1236
|
+
are read once on first load. Strings prefixed `cmd:` (today's
|
|
1237
|
+
broken command path) become `{type: "command", commandId: ...}`;
|
|
1238
|
+
plain path strings become `{type: "catalog", catalogId: "files",
|
|
1239
|
+
rowId: path, rowSnapshot: {...minimal row…}}`. Unrecognizable
|
|
1240
|
+
entries dropped.
|
|
1241
|
+
|
|
1242
|
+
#### Recent serialization invariant (gemini P1)
|
|
1243
|
+
|
|
1244
|
+
`rowSnapshot: ExplorerRow` is round-tripped through
|
|
1245
|
+
`JSON.stringify`/`JSON.parse` in localStorage. ExplorerRow values
|
|
1246
|
+
that contain `Date`, `Map`, `Set`, functions, React nodes, or
|
|
1247
|
+
class instances will be silently corrupted on save and crash on
|
|
1248
|
+
restore.
|
|
1249
|
+
|
|
1250
|
+
**Invariant:** any `ExplorerRow` shape that can appear in a
|
|
1251
|
+
catalog's selected items MUST be JSON-serializable. Adapters that
|
|
1252
|
+
naturally hold non-serializable values (e.g., a Date object for
|
|
1253
|
+
"last modified") should serialize at row construction time
|
|
1254
|
+
(ISO string) and re-hydrate in the renderer.
|
|
1255
|
+
|
|
1256
|
+
**No `deserializeRecent` hook in Phase 1.** If a real adapter has
|
|
1257
|
+
a need that the JSON-serializable invariant can't express, the
|
|
1258
|
+
hook can be added as a non-breaking optional field on
|
|
1259
|
+
`CatalogConfig`. Phase 1 doesn't need it; documenting the
|
|
1260
|
+
constraint is sufficient.
|
|
1261
|
+
|
|
1262
|
+
### Registry-driven workbench tabs
|
|
1263
|
+
|
|
1264
|
+
Today `WorkbenchLeftPane` hardcodes Files / Data tabs
|
|
1265
|
+
(`WorkbenchLeftPane.tsx:97`, `:174`, `:181`) — which means
|
|
1266
|
+
`excludeDefaults: ['filesystem']` would suppress the filesystem
|
|
1267
|
+
agent tools and catalogs but leave a dead Files tab in the UI.
|
|
1268
|
+
|
|
1269
|
+
Phase 1 step 5c retrofits `WorkbenchLeftPane` to query
|
|
1270
|
+
`PanelRegistry` for `placement: 'left-tab'`, sorted by
|
|
1271
|
+
registration order. `filesystemPlugin` contributes the Files tab;
|
|
1272
|
+
`dataCatalogPlugin` contributes the Data tab.
|
|
1273
|
+
`excludeDefaults: ['filesystem']` truly removes the tab.
|
|
1274
|
+
|
|
1275
|
+
(`'right-tab'` is reserved in the contract but no Phase 1 component
|
|
1276
|
+
consumes it; `WorkbenchRightPane` doesn't exist yet.)
|
|
1277
|
+
|
|
1278
|
+
### Closing the SurfaceShell hardcoded-fallback hole
|
|
1279
|
+
|
|
1280
|
+
`SurfaceShell.fallbackComponentForPath`
|
|
1281
|
+
(`SurfaceShell.tsx:81-91`) maps extensions to literal panel ids
|
|
1282
|
+
(`code-editor`, `markdown-editor`, `csv-viewer`) regardless of
|
|
1283
|
+
whether they're registered. `resolvePanelForPath`
|
|
1284
|
+
(`SurfaceShell.tsx:99-108`) checks `registry.has(fallback)` then
|
|
1285
|
+
returns the fallback id anyway as a "last-ditch."
|
|
1286
|
+
|
|
1287
|
+
Step 5c also fixes the resolver chain: registry resolve →
|
|
1288
|
+
registered fallback (only if `has()`) → `EmptyFilePanel` (a core
|
|
1289
|
+
panel) showing "No editor for `<path>` — install or enable a
|
|
1290
|
+
plugin that handles `<ext>`." Zero ghost tabs when defaults are
|
|
1291
|
+
excluded.
|
|
1292
|
+
|
|
1293
|
+
### Event bus integration
|
|
1294
|
+
|
|
1295
|
+
The bus is **already implemented** at
|
|
1296
|
+
`packages/workspace/src/events/{bus,index,types,useEvent}.ts`. This
|
|
1297
|
+
section reflects the actual API.
|
|
1298
|
+
|
|
1299
|
+
```ts
|
|
1300
|
+
// Module singleton — import directly anywhere
|
|
1301
|
+
import { events, useEvent } from "@boring/workspace/events"
|
|
1302
|
+
import type { WorkspaceEventMap } from "@boring/workspace/events"
|
|
1303
|
+
|
|
1304
|
+
events.on("file:moved", ({ from, to }) => { /* … */ }) // returns unsubscribe
|
|
1305
|
+
events.emit("file:moved", { ...userMeta(), from, to })
|
|
1306
|
+
|
|
1307
|
+
// React hook
|
|
1308
|
+
useEvent("file:moved", ({ from, to }) => { /* … */ })
|
|
1309
|
+
```
|
|
1310
|
+
|
|
1311
|
+
**Events are declared on demand** — `WorkspaceEventMap` (in
|
|
1312
|
+
`events/types.ts`) intentionally pre-declares no future events.
|
|
1313
|
+
Phase 1 does NOT add plugin lifecycle events because Phase 1
|
|
1314
|
+
plugins have no lifecycle. If/when something emits and consumes,
|
|
1315
|
+
the key gets added to the map.
|
|
1316
|
+
|
|
1317
|
+
**Plugin authors** subscribe via `useEvent` inside panel components
|
|
1318
|
+
(natural React lifecycle, automatic cleanup) or via `events.on(...)`
|
|
1319
|
+
inside route handlers. They do NOT receive an injected bus through
|
|
1320
|
+
a plugin context — there's no plugin context to inject into.
|
|
1321
|
+
|
|
1322
|
+
**Package-exports gap:** the workspace package's `exports` map
|
|
1323
|
+
(`packages/workspace/package.json:9-30`) currently exposes `.`,
|
|
1324
|
+
`./testing`, `./ui-shadcn`, `./shared`, `./server`, `./globals.css`
|
|
1325
|
+
— but NOT `./events`. Step 4a adds:
|
|
1326
|
+
|
|
1327
|
+
```json
|
|
1328
|
+
"./events": {
|
|
1329
|
+
"types": "./dist/events.d.ts",
|
|
1330
|
+
"import": "./dist/events.js"
|
|
1331
|
+
}
|
|
1332
|
+
```
|
|
1333
|
+
|
|
1334
|
+
Plus the corresponding `tsup` entry. Events are also re-exported
|
|
1335
|
+
from the package barrel for convenience.
|
|
1336
|
+
|
|
1337
|
+
### Phase 1 debug overlay: `<PluginInspector />` (v7.2)
|
|
1338
|
+
|
|
1339
|
+
DEV-only React component mounted by `<WorkspaceProvider>` when
|
|
1340
|
+
`import.meta.env.DEV` (zero production bundle impact). Toggle via
|
|
1341
|
+
`Cmd+Shift+P P` (or "Show Plugin Inspector" command).
|
|
1342
|
+
|
|
1343
|
+
Displays:
|
|
1344
|
+
- Registered plugins: id, label, source (default / inline / npm),
|
|
1345
|
+
contribution counts (N panels, N commands, N catalogs, N agentTools)
|
|
1346
|
+
- Per-plugin systemPrompt preview (first 200 chars; expandable)
|
|
1347
|
+
- Errors keyed to plugin id (from `WorkspaceContext.errors`)
|
|
1348
|
+
- Late-wins-on-id replacements: when plugin B overrode plugin A's
|
|
1349
|
+
contribution, both pluginIds shown
|
|
1350
|
+
- Reserved-name collisions (if a plugin's agentTools name collides
|
|
1351
|
+
with harness substrate)
|
|
1352
|
+
|
|
1353
|
+
Implementation: ~50 LOC reading from registries via the existing
|
|
1354
|
+
`useCatalogs`/`useCommands`/`useActivePanels` hooks plus a new
|
|
1355
|
+
`usePlugins()` returning the list of registered plugins.
|
|
1356
|
+
|
|
1357
|
+
Why ship in Phase 1 (re-introduced from v6 cut): plugin authors
|
|
1358
|
+
testing locally hit "why didn't my command appear?" repeatedly.
|
|
1359
|
+
Inspector turns 5-minute spelunking into a 2-second visual check.
|
|
1360
|
+
|
|
1361
|
+
### Inline plugin layout (Phase 1)
|
|
1362
|
+
|
|
1363
|
+
```
|
|
1364
|
+
apps/<some-app>/
|
|
1365
|
+
├── package.json
|
|
1366
|
+
├── src/
|
|
1367
|
+
│ ├── plugin/
|
|
1368
|
+
│ │ ├── index.ts ← env-aware barrel + makeXyzPlugin factory
|
|
1369
|
+
│ │ ├── plugin.shared.ts ← id, label, fixed config
|
|
1370
|
+
│ │ ├── plugin.client.ts ← panels, catalogs, commands
|
|
1371
|
+
│ │ └── plugin.server.ts ← agentTools (route handlers live in src/server/)
|
|
1372
|
+
│ ├── server/index.ts ← createWorkspaceAgentApp({ plugins }) + app.register(routes)
|
|
1373
|
+
│ └── web/App.tsx ← <WorkspaceProvider plugins={[…]}>
|
|
1374
|
+
```
|
|
1375
|
+
|
|
1376
|
+
For multi-plugin apps: `src/plugins/<id>/...` mirrors per plugin;
|
|
1377
|
+
`src/plugins/index.ts` exports an array.
|
|
1378
|
+
|
|
1379
|
+
### Entry points
|
|
1380
|
+
|
|
1381
|
+
```ts
|
|
1382
|
+
// CLIENT
|
|
1383
|
+
import { WorkspaceProvider } from "@boring/workspace"
|
|
1384
|
+
import { ChatCenteredShell } from "@boring/workspace"
|
|
1385
|
+
import { makeMacroPlugin, macroChatSuggestions } from "../plugin"
|
|
1386
|
+
|
|
1387
|
+
<WorkspaceProvider plugins={[makeMacroPlugin()]}>
|
|
1388
|
+
<ChatCenteredShell chatSuggestions={macroChatSuggestions} />
|
|
1389
|
+
</WorkspaceProvider>
|
|
1390
|
+
|
|
1391
|
+
// SERVER
|
|
1392
|
+
import { createWorkspaceAgentApp } from "@boring/workspace/server"
|
|
1393
|
+
import { makeMacroPlugin } from "../plugin"
|
|
1394
|
+
import { registerMacroRoutes } from "../server/macroRoutes"
|
|
1395
|
+
|
|
1396
|
+
const clickhouse = await createClickHouseClient(env)
|
|
1397
|
+
const app = await createWorkspaceAgentApp({
|
|
1398
|
+
plugins: [makeMacroPlugin()],
|
|
1399
|
+
// excludeDefaults: ["dataCatalog"] // optional
|
|
1400
|
+
})
|
|
1401
|
+
await app.register(registerMacroRoutes, { clickhouse, deckRoot })
|
|
1402
|
+
await app.listen({ port })
|
|
1403
|
+
```
|
|
1404
|
+
|
|
1405
|
+
Both entry points auto-mount default plugins (filesystem +
|
|
1406
|
+
dataCatalog) unless excluded.
|
|
1407
|
+
|
|
1408
|
+
## Distribution — Phase 2 sketch
|
|
1409
|
+
|
|
1410
|
+
**Inline plugins** (Phase 1) live in the host's source tree. No
|
|
1411
|
+
distribution model needed — direct imports.
|
|
1412
|
+
|
|
1413
|
+
**npm-distributed plugins** (Phase 2) follow the standard
|
|
1414
|
+
sub-path-exports pattern. Same Plugin shape, just delivered via
|
|
1415
|
+
package boundaries:
|
|
1416
|
+
|
|
1417
|
+
```json
|
|
1418
|
+
// pi-plugin-macro/package.json
|
|
1419
|
+
{
|
|
1420
|
+
"exports": {
|
|
1421
|
+
"./client": { "types": "./dist/client.d.ts", "import": "./dist/client.js" },
|
|
1422
|
+
"./server": { "types": "./dist/server.d.ts", "import": "./dist/server.js" }
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
```
|
|
1426
|
+
|
|
1427
|
+
```ts
|
|
1428
|
+
// pi-plugin-macro/dist/client.js — UI half
|
|
1429
|
+
export const macroClientPlugin = definePlugin({
|
|
1430
|
+
id: "boring-macro",
|
|
1431
|
+
panels: [chartCanvasPanel, deckPanel],
|
|
1432
|
+
catalogs: [seriesCatalog],
|
|
1433
|
+
})
|
|
1434
|
+
```
|
|
1435
|
+
|
|
1436
|
+
```ts
|
|
1437
|
+
// pi-plugin-macro/dist/server.js — server half
|
|
1438
|
+
export const macroServerPlugin = definePlugin({
|
|
1439
|
+
id: "boring-macro", // same id, different bag
|
|
1440
|
+
agentTools: macroAgentTools,
|
|
1441
|
+
})
|
|
1442
|
+
export const registerMacroRoutes = async (app, opts) => { /* … */ }
|
|
1443
|
+
|
|
1444
|
+
// optional convenience helper for hosts that want one-liner install:
|
|
1445
|
+
export const installMacroServer = async (app, opts) => {
|
|
1446
|
+
await app.register(registerMacroRoutes, opts)
|
|
1447
|
+
return macroServerPlugin
|
|
1448
|
+
}
|
|
1449
|
+
```
|
|
1450
|
+
|
|
1451
|
+
```ts
|
|
1452
|
+
// host server entry
|
|
1453
|
+
import { createWorkspaceAgentApp } from "@boring/workspace/server"
|
|
1454
|
+
import { macroServerPlugin, registerMacroRoutes } from "pi-plugin-macro/server"
|
|
1455
|
+
|
|
1456
|
+
const clickhouse = await createClickHouseClient(env)
|
|
1457
|
+
const app = await createWorkspaceAgentApp({ plugins: [macroServerPlugin] })
|
|
1458
|
+
await app.register(registerMacroRoutes, { clickhouse, deckRoot })
|
|
1459
|
+
```
|
|
1460
|
+
|
|
1461
|
+
Two imports + two calls per server-side plugin. Same shape mature
|
|
1462
|
+
ecosystems already use (Express middleware, Fastify plugins, Vite
|
|
1463
|
+
plugins).
|
|
1464
|
+
|
|
1465
|
+
**The same plugin id** (`"boring-macro"`) on client and server ties
|
|
1466
|
+
them together for provenance — but client and server processes don't
|
|
1467
|
+
share state, so a "single fat plugin object spanning both" was
|
|
1468
|
+
always an illusion.
|
|
1469
|
+
|
|
1470
|
+
## Relationship to pi-mono ecosystem
|
|
1471
|
+
|
|
1472
|
+
The pi-coding-agent's existing plugin loader
|
|
1473
|
+
(`packages/agent/src/server/harness/pi-coding-agent/pluginLoader.ts`)
|
|
1474
|
+
gives us discovery infrastructure for free. Verified what it
|
|
1475
|
+
actually does (2026-04-28):
|
|
1476
|
+
|
|
1477
|
+
- 4 discovery channels: `~/.pi/agent/extensions/`,
|
|
1478
|
+
`<cwd>/.pi/extensions/`, `node_modules/pi-plugin-*`, and
|
|
1479
|
+
`.pi/extensions.json`.
|
|
1480
|
+
- Plugin module shape: `{ default: AgentTool[] | tools: AgentTool[] }`
|
|
1481
|
+
— **tools only**, flat list.
|
|
1482
|
+
- Conflict resolution: late-wins-on-name with a warning.
|
|
1483
|
+
- npm namespace convention: `pi-plugin-*`.
|
|
1484
|
+
- Ecosystem index at
|
|
1485
|
+
`~/.pi/agent/extension-index.json` lists ~50+ extensions
|
|
1486
|
+
(`antigravity-image-gen`, `auto-commit-on-exit`, `bookmark`,
|
|
1487
|
+
`claude-rules`, …) — every one is a single-file `.ts` exporting
|
|
1488
|
+
tools.
|
|
1489
|
+
|
|
1490
|
+
**What pi explicitly DOESN'T have:** `dependsOn`, version ranges,
|
|
1491
|
+
load ordering, multi-contribution types, lifecycle hooks. Pi
|
|
1492
|
+
plugins are flat tool exporters. Cross-plugin coordination is not a
|
|
1493
|
+
pi concern.
|
|
1494
|
+
|
|
1495
|
+
**What we adopt from pi:**
|
|
1496
|
+
|
|
1497
|
+
- Discovery channels verbatim (Phase 2's pi loader extension uses
|
|
1498
|
+
the same paths).
|
|
1499
|
+
- `pi-plugin-*` npm convention.
|
|
1500
|
+
- Late-wins conflict resolution.
|
|
1501
|
+
- `.pi/extensions.json` settings file format.
|
|
1502
|
+
- Tool-only legacy plugins keep working — Phase 2's
|
|
1503
|
+
`extractTools` → `extractPlugin` is additive: a module exporting
|
|
1504
|
+
the new `Plugin` shape gets read as a Plugin; a module exporting
|
|
1505
|
+
the old `tools: AgentTool[]` keeps being read as tools-only.
|
|
1506
|
+
|
|
1507
|
+
**What we add on top:**
|
|
1508
|
+
|
|
1509
|
+
- Multi-contribution shape (`Plugin` with panels/catalogs/commands/
|
|
1510
|
+
agentTools).
|
|
1511
|
+
- Workspace-side aggregation (registries, file-pattern resolution,
|
|
1512
|
+
excludeDefaults).
|
|
1513
|
+
- npm sub-path exports (`./client`, `./server`) for full-plugin
|
|
1514
|
+
distribution.
|
|
1515
|
+
|
|
1516
|
+
Pi gives us **distribution infrastructure**; we add **coordination
|
|
1517
|
+
model**.
|
|
1518
|
+
|
|
1519
|
+
## Architecture diagram — post-Phase 1
|
|
1520
|
+
|
|
1521
|
+
### Package dependency graph
|
|
1522
|
+
|
|
1523
|
+
```
|
|
1524
|
+
┌─────────────────────────────────────┐
|
|
1525
|
+
│ apps/ │
|
|
1526
|
+
│ ├── boring-macro-v2 │
|
|
1527
|
+
│ ├── full-app │
|
|
1528
|
+
│ └── <new host> │
|
|
1529
|
+
└─────────────────┬───────────────────┘
|
|
1530
|
+
│ imports
|
|
1531
|
+
▼
|
|
1532
|
+
┌────────────────────────────────────────────────────────────┐
|
|
1533
|
+
│ @boring/workspace │
|
|
1534
|
+
│ • Plugin contract (definePlugin, factories) │
|
|
1535
|
+
│ • Registries (Panel / Command / Catalog) │
|
|
1536
|
+
│ • Default plugins (filesystemPlugin, dataCatalogPlugin) │
|
|
1537
|
+
│ • UI bridge core (moved from @boring/agent) │
|
|
1538
|
+
│ • Substrate routes /api/v1/{ui,files,tree,files/search} │
|
|
1539
|
+
│ • Event bus + WorkspaceEventMap │
|
|
1540
|
+
│ • WorkbenchLeftPane / SurfaceShell / CommandPalette │
|
|
1541
|
+
│ • createWorkspaceAgentApp (wraps createAgentApp) │
|
|
1542
|
+
└─────────────────┬───────────────────────────────────────────┘
|
|
1543
|
+
│ imports (one direction; never the reverse)
|
|
1544
|
+
▼
|
|
1545
|
+
┌────────────────────────────────────────────────────────────┐
|
|
1546
|
+
│ @boring/agent │
|
|
1547
|
+
│ • pi-coding-agent harness │
|
|
1548
|
+
│ • AgentTool type │
|
|
1549
|
+
│ • validateTool ← extracted to /shared (NEW location)│
|
|
1550
|
+
│ • Pi loader (legacy tools-only; Phase 2 extends) │
|
|
1551
|
+
│ • filesystemAgentTools bundle ← shared with workspace │
|
|
1552
|
+
│ • bash, execute_isolated_code (harness-only) │
|
|
1553
|
+
│ • Chat / session / model HTTP routes │
|
|
1554
|
+
│ • createAgentApp (disableDefaultFileTools? new flag) │
|
|
1555
|
+
└────────────────────────────────────────────────────────────┘
|
|
1556
|
+
|
|
1557
|
+
Invariants:
|
|
1558
|
+
• @boring/agent has NO dep on @boring/workspace (acyclic).
|
|
1559
|
+
• @boring/workspace imports from @boring/agent (one way).
|
|
1560
|
+
• @boring/agent/shared is browser-safe (no node:* imports);
|
|
1561
|
+
this is what workspace's client bundle pulls in.
|
|
1562
|
+
```
|
|
1563
|
+
|
|
1564
|
+
### Tool registration flow (file ops as worked example)
|
|
1565
|
+
|
|
1566
|
+
```
|
|
1567
|
+
filesystemAgentTools (shared bundle, in @boring/agent)
|
|
1568
|
+
┌────────────────────┐
|
|
1569
|
+
│ read, write, edit, │
|
|
1570
|
+
│ find, │
|
|
1571
|
+
│ grep │
|
|
1572
|
+
└─────────┬──────────┘
|
|
1573
|
+
│
|
|
1574
|
+
┌──────────────┴───────────────┐
|
|
1575
|
+
▼
|
|
1576
|
+
SINGLE PATH (v7.1 — both standalone + workspace use it)
|
|
1577
|
+
─────────────────────────────────────────────────────────
|
|
1578
|
+
createAgentApp({})
|
|
1579
|
+
└─ standardCatalog
|
|
1580
|
+
└─ buildHarnessAgentTools(bundle) (bash, executeIsolatedCode)
|
|
1581
|
+
└─ buildFilesystemAgentTools(bundle) (read/write/edit/find/grep/ls)
|
|
1582
|
+
+ any custom non-pi additions
|
|
1583
|
+
(default ON; opt out with disableDefaultFileTools: true
|
|
1584
|
+
for sandboxed/no-fs agents)
|
|
1585
|
+
|
|
1586
|
+
createWorkspaceAgentApp wraps createAgentApp PLAINLY (no
|
|
1587
|
+
disableDefaultFileTools dance) + bootstraps filesystemPlugin
|
|
1588
|
+
(UI-only) on top.
|
|
1589
|
+
|
|
1590
|
+
standalone CLI agent workspace host
|
|
1591
|
+
= real coding agent = same harness tools +
|
|
1592
|
+
plugin model adds UI;
|
|
1593
|
+
excludeDefaults:
|
|
1594
|
+
['filesystem'] removes
|
|
1595
|
+
Files left-tab + auto-
|
|
1596
|
+
routing — TOOLS STAY
|
|
1597
|
+
(use disableDefaultFileTools
|
|
1598
|
+
for that)
|
|
1599
|
+
```
|
|
1600
|
+
|
|
1601
|
+
### File tree — what changes in Phase 1
|
|
1602
|
+
|
|
1603
|
+
```
|
|
1604
|
+
packages/agent/
|
|
1605
|
+
├── src/
|
|
1606
|
+
│ ├── shared/
|
|
1607
|
+
│ │ ├── tool.ts [EXISTS]
|
|
1608
|
+
│ │ └── validateTool.ts [NEW — extracted from
|
|
1609
|
+
│ │ pluginLoader.ts; node-clean
|
|
1610
|
+
│ │ so workspace client can import]
|
|
1611
|
+
│ ├── server/
|
|
1612
|
+
│ │ ├── createAgentApp.ts [EXISTS — adds
|
|
1613
|
+
│ │ │ disableDefaultFileTools? flag]
|
|
1614
|
+
│ │ ├── catalog/
|
|
1615
|
+
│ │ │ ├── standardCatalog.ts [EXISTS — drops file ops,
|
|
1616
|
+
│ │ │ │ conditionally re-adds them
|
|
1617
|
+
│ │ │ │ from the shared bundle]
|
|
1618
|
+
│ │ │ └── tools/ [EXISTS — read/write/edit/
|
|
1619
|
+
│ │ │ │ find/grep
|
|
1620
|
+
│ │ │ │ individual implementations
|
|
1621
|
+
│ │ │ │ stay here]
|
|
1622
|
+
│ │ │ └── (read|write|edit|findFiles|grepFiles)Tool.ts
|
|
1623
|
+
│ │ ├── tools/
|
|
1624
|
+
│ │ │ └── filesystem/ [NEW]
|
|
1625
|
+
│ │ │ └── index.ts [NEW — exports
|
|
1626
|
+
│ │ │ filesystemAgentTools[]
|
|
1627
|
+
│ │ │ that re-bundles the
|
|
1628
|
+
│ │ │ individual tools above]
|
|
1629
|
+
│ │ ├── harness/pi-coding-agent/
|
|
1630
|
+
│ │ │ └── pluginLoader.ts [EXISTS — imports
|
|
1631
|
+
│ │ │ validateTool from
|
|
1632
|
+
│ │ │ ../../../shared/validateTool
|
|
1633
|
+
│ │ │ instead of defining it here]
|
|
1634
|
+
│ │ └── http/routes/
|
|
1635
|
+
│ │ ├── file.ts ──────moves to───► [packages/workspace/src/server/
|
|
1636
|
+
│ │ ├── tree.ts ──────moves to───► routes/files.ts]
|
|
1637
|
+
│ │ ├── search.ts ──────moves to───► [routes/files.ts]
|
|
1638
|
+
│ │ └── ui.ts ──────moves to───► [routes/ui.ts]
|
|
1639
|
+
│ └── ...
|
|
1640
|
+
└── package.json
|
|
1641
|
+
|
|
1642
|
+
packages/workspace/
|
|
1643
|
+
├── src/
|
|
1644
|
+
│ ├── shared/
|
|
1645
|
+
│ │ ├── plugin.ts [NEW — Plugin contract,
|
|
1646
|
+
│ │ │ 6 fields, pure data]
|
|
1647
|
+
│ │ └── ui-bridge.ts [MOVED from @boring/agent]
|
|
1648
|
+
│ ├── events/ [EXISTS — bus already shipped]
|
|
1649
|
+
│ ├── plugin/ [NEW — the plugin system]
|
|
1650
|
+
│ │ ├── definePlugin.ts (factory + validation)
|
|
1651
|
+
│ │ ├── validators.ts (validateAgentTool re-exports
|
|
1652
|
+
│ │ │ @boring/agent/shared)
|
|
1653
|
+
│ │ ├── bootstrap.ts (the single-pass mount loop)
|
|
1654
|
+
│ │ ├── CatalogRegistry.ts (subscribable)
|
|
1655
|
+
│ │ └── defaults/
|
|
1656
|
+
│ │ ├── filesystemPlugin.ts (imports filesystemAgentTools
|
|
1657
|
+
│ │ │ from @boring/agent)
|
|
1658
|
+
│ │ └── dataCatalogPlugin.ts
|
|
1659
|
+
│ ├── registry/
|
|
1660
|
+
│ │ ├── PanelRegistry.ts [EXISTS — retrofitted:
|
|
1661
|
+
│ │ │ subscribable, path-aware
|
|
1662
|
+
│ │ │ micromatch resolver,
|
|
1663
|
+
│ │ │ specificity scoring]
|
|
1664
|
+
│ │ ├── CommandRegistry.ts [EXISTS — retrofitted
|
|
1665
|
+
│ │ │ subscribable]
|
|
1666
|
+
│ │ └── types.ts [EXISTS — adds
|
|
1667
|
+
│ │ 'left-tab'/'right-tab'
|
|
1668
|
+
│ │ placement, pluginId]
|
|
1669
|
+
│ ├── components/
|
|
1670
|
+
│ │ ├── CommandPalette.tsx [EXISTS — refactored to
|
|
1671
|
+
│ │ │ consume useCatalogs();
|
|
1672
|
+
│ │ │ polymorphic Recent;
|
|
1673
|
+
│ │ │ drops fileSearchFn/
|
|
1674
|
+
│ │ │ onOpenFile props]
|
|
1675
|
+
│ │ └── chat/
|
|
1676
|
+
│ │ ├── ChatCenteredShell.tsx [EXISTS — drops `data` +
|
|
1677
|
+
│ │ │ `extraPanels` (KEEPS
|
|
1678
|
+
│ │ │ `chatSuggestions` prop);
|
|
1679
|
+
│ │ │ migrates imperative
|
|
1680
|
+
│ │ │ useEffect command reg]
|
|
1681
|
+
│ │ ├── WorkbenchLeftPane.tsx [EXISTS — registry-driven
|
|
1682
|
+
│ │ │ tabs from PanelRegistry
|
|
1683
|
+
│ │ │ (placement: 'left-tab')]
|
|
1684
|
+
│ │ ├── SurfaceShell.tsx [EXISTS — fallback chain
|
|
1685
|
+
│ │ │ fixed, EmptyFilePanel
|
|
1686
|
+
│ │ │ used when registry +
|
|
1687
|
+
│ │ │ registered fallback both
|
|
1688
|
+
│ │ │ miss]
|
|
1689
|
+
│ │ └── EmptyFilePanel.tsx [NEW — "No editor for X"
|
|
1690
|
+
│ │ panel; replaces ghost-tab
|
|
1691
|
+
│ │ fallback]
|
|
1692
|
+
│ ├── bridge/
|
|
1693
|
+
│ │ └── createInMemoryBridge.ts [MOVED from @boring/agent]
|
|
1694
|
+
│ └── server/
|
|
1695
|
+
│ ├── createWorkspaceAgentApp.ts [EXISTS — wraps createAgentApp
|
|
1696
|
+
│ │ with disableDefaultFileTools:
|
|
1697
|
+
│ │ true; runs bootstrap();
|
|
1698
|
+
│ │ registers substrate routes]
|
|
1699
|
+
│ ├── uiTools.ts [MOVED from @boring/agent —
|
|
1700
|
+
│ │ get_ui_state, exec_ui]
|
|
1701
|
+
│ └── routes/
|
|
1702
|
+
│ ├── ui.ts [MOVED from @boring/agent]
|
|
1703
|
+
│ └── files.ts [MOVED — file/tree/search
|
|
1704
|
+
│ consolidated]
|
|
1705
|
+
└── package.json [EXISTS — adds:
|
|
1706
|
+
"./events": { ... }
|
|
1707
|
+
export]
|
|
1708
|
+
|
|
1709
|
+
apps/boring-macro-v2/
|
|
1710
|
+
├── src/
|
|
1711
|
+
│ ├── plugin/ [NEW — Step 6]
|
|
1712
|
+
│ │ ├── index.ts (makeMacroPlugin factory +
|
|
1713
|
+
│ │ │ macroChatSuggestions const)
|
|
1714
|
+
│ │ ├── plugin.shared.ts
|
|
1715
|
+
│ │ ├── plugin.client.ts (panels, catalog)
|
|
1716
|
+
│ │ └── plugin.server.ts (agentTools)
|
|
1717
|
+
│ ├── web/App.tsx [EXISTS — shrinks to ~6 LOC]
|
|
1718
|
+
│ └── server/
|
|
1719
|
+
│ ├── index.ts [EXISTS — uses
|
|
1720
|
+
│ │ createWorkspaceAgentApp +
|
|
1721
|
+
│ │ one app.register() for
|
|
1722
|
+
│ │ routes; ~10 LOC]
|
|
1723
|
+
│ ├── macroRoutes.ts [EXISTS — registered via
|
|
1724
|
+
│ │ app.register() in
|
|
1725
|
+
│ │ server/index.ts]
|
|
1726
|
+
│ ├── macroTools.ts [EXISTS — referenced as
|
|
1727
|
+
│ │ plugin.agentTools]
|
|
1728
|
+
│ └── uiBridge.ts [DELETED — 150 LOC; the
|
|
1729
|
+
│ workspace's UI bridge core
|
|
1730
|
+
│ replaces it]
|
|
1731
|
+
```
|
|
1732
|
+
|
|
1733
|
+
Markers: **[NEW]** **[MOVED]** **[EXISTS]** (modified) **[DELETED]**.
|
|
1734
|
+
|
|
1735
|
+
## Reorganization (file moves)
|
|
1736
|
+
|
|
1737
|
+
| From | To | Lands as |
|
|
1738
|
+
|---|---|---|
|
|
1739
|
+
| `@boring/agent`: file ops (`find`, `grep`, `read`, `write`, `edit`, `ls`) | **Stays in `@boring/agent`** as `src/server/tools/filesystem/index.ts` (per pi-tools-migration's `buildFilesystemAgentTools(bundle)` factory; pi tools + custom non-pi additions allowed) | Always-on via `createAgentApp`'s `standardCatalog`; opt out with `disableDefaultFileTools: true`. **Not** imported by `filesystemPlugin` — that plugin is UI-only in v7.0+. |
|
|
1740
|
+
| `@boring/agent`: `validateTool` (in pluginLoader.ts) | `@boring/agent/shared/validateTool.ts` (extracted; node-clean) | Imported by pluginLoader; re-exported by `@boring/workspace`'s `validateAgentTool` |
|
|
1741
|
+
| `@boring/agent`: UI bridge tools (`get_ui_state`, `exec_ui`) | `@boring/workspace/server/uiTools.ts` | Core (registered directly by `createWorkspaceAgentApp`) |
|
|
1742
|
+
| `@boring/agent`: file/tree/search HTTP routes | `@boring/workspace/server/routes/files.ts` | Substrate (registered directly) |
|
|
1743
|
+
| `@boring/agent`: UI HTTP routes | `@boring/workspace/server/routes/ui.ts` | Substrate (registered directly) |
|
|
1744
|
+
| `@boring/agent`: `src/shared/ui-bridge.ts` (`UiState`, `UiCommand` types) | `@boring/workspace/src/shared/ui-bridge.ts` | Core types |
|
|
1745
|
+
| `@boring/agent`: `src/server/ui-bridge/createInMemoryBridge.ts` | `@boring/workspace/src/bridge/createInMemoryBridge.ts` | Core |
|
|
1746
|
+
|
|
1747
|
+
`@boring/agent` keeps:
|
|
1748
|
+
|
|
1749
|
+
- `pi-coding-agent` harness (LLM loop, sessions, models)
|
|
1750
|
+
- `AgentTool` type (shared contract)
|
|
1751
|
+
- `validateTool` (now in `/shared`, re-used by workspace)
|
|
1752
|
+
- Pi loader (legacy tools-only; Phase 2 extends)
|
|
1753
|
+
- File ops shared bundle (`filesystemAgentTools`)
|
|
1754
|
+
- `bash`, `execute_isolated_code` tools (harness-only)
|
|
1755
|
+
- Chat / session / model HTTP routes
|
|
1756
|
+
- `createAgentApp` (UI-less; standalone CLI; auto-includes file ops)
|
|
1757
|
+
|
|
1758
|
+
## Exact path: now → Phase 1 done
|
|
1759
|
+
|
|
1760
|
+
Six sequenced commits, plus a v7.6-mandated **Step 0** at the front
|
|
1761
|
+
to put files into their FINAL v7.6 destinations before Phase 1
|
|
1762
|
+
builds anything else. (Both reviewers in r5 flagged the alternative
|
|
1763
|
+
— building Phase 1 into the OLD flat layout then ripping up later —
|
|
1764
|
+
as wasted work; gemini called it P0.)
|
|
1765
|
+
|
|
1766
|
+
**Meta-rule (v7.6):** when files move, they go DIRECTLY to their
|
|
1767
|
+
final v7.6 destinations. No "move to old path then move again
|
|
1768
|
+
later." Step 0 is the ONE place the existing workspace files
|
|
1769
|
+
restructure; subsequent phases assume the v7.6 layout exists.
|
|
1770
|
+
|
|
1771
|
+
```
|
|
1772
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
1773
|
+
│ STEP 0 — Workspace package reorg into v7.6 layout (NEW; gemini │
|
|
1774
|
+
│ P0 + codex P1) │
|
|
1775
|
+
│ ─────────────────────────────────────────────────────────────── │
|
|
1776
|
+
│ Mechanical git mv of existing workspace src/ files into the │
|
|
1777
|
+
│ v7.6 four-folder layout. NO behavior change. NO new files. One │
|
|
1778
|
+
│ PR. │
|
|
1779
|
+
│ │
|
|
1780
|
+
│ Moves (existing → v7.6 destination): │
|
|
1781
|
+
│ src/components/{ui, DataExplorer, CommandPalette, SessionList,│
|
|
1782
|
+
│ PluginErrorBoundary?} → src/front/components/ │
|
|
1783
|
+
│ src/registry/ → src/front/registry/ │
|
|
1784
|
+
│ src/dock/ → src/front/dock/ │
|
|
1785
|
+
│ src/events/ → src/front/events/ │
|
|
1786
|
+
│ src/hooks/ → src/front/hooks/ │
|
|
1787
|
+
│ src/layouts/ → src/front/layout/ │
|
|
1788
|
+
│ src/panes/{ArtifactSurfacePane.tsx, → to be moved into │
|
|
1789
|
+
│ EmptyPane.tsx} chrome/ in Phase A │
|
|
1790
|
+
│ src/panes/{code-editor, markdown-editor, file-tree}/ │
|
|
1791
|
+
│ → to be moved INTO │
|
|
1792
|
+
│ filesystemPlugin in │
|
|
1793
|
+
│ Step 3 (NOT to │
|
|
1794
|
+
│ front/panes/) │
|
|
1795
|
+
│ src/panes/data-catalog/ → audit + delete OR │
|
|
1796
|
+
│ move to front/ │
|
|
1797
|
+
│ components/ │
|
|
1798
|
+
│ src/plugin/{types, definePlugin, → src/shared/plugin/ │
|
|
1799
|
+
│ bootstrap}.ts (the SHARED parts) │
|
|
1800
|
+
│ src/plugin/{CatalogRegistry, → src/front/plugin/ │
|
|
1801
|
+
│ use*.ts, index.ts} │
|
|
1802
|
+
│ src/store/, src/testing/, src/types/ → audit; fold into │
|
|
1803
|
+
│ shared/ if minimal │
|
|
1804
|
+
│ src/server/ (already in src/server;│
|
|
1805
|
+
│ stays; minor file │
|
|
1806
|
+
│ renames if needed) │
|
|
1807
|
+
│ src/shared/ (already in src/shared;│
|
|
1808
|
+
│ gains plugin/ subdir│
|
|
1809
|
+
│ from above) │
|
|
1810
|
+
│ │
|
|
1811
|
+
│ tsconfig changes: │
|
|
1812
|
+
│ - tsconfig.front.json adds excludes for │
|
|
1813
|
+
│ src/plugins/**/server/**, src/plugin/server/** (codex P1) │
|
|
1814
|
+
│ - tsconfig.shared.json (if exists) doesn't include DOM lib │
|
|
1815
|
+
│ │
|
|
1816
|
+
│ Deliverable: src/ has front/, server/, shared/, plugins/ at top │
|
|
1817
|
+
│ level. ALL existing tests pass unchanged (vi.mock paths follow │
|
|
1818
|
+
│ moved files). No new functionality. │
|
|
1819
|
+
│ ETA: 1-2 days. │
|
|
1820
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
1821
|
+
│
|
|
1822
|
+
▼
|
|
1823
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
1824
|
+
│ STEP 1 — REORG (no plugin model yet, pure refactor) │
|
|
1825
|
+
│ ─────────────────────────────────────────────────────────────── │
|
|
1826
|
+
│ 1a. UI bridge ownership refactor │
|
|
1827
|
+
│ Move ui-bridge types/tools/routes from @boring/agent → │
|
|
1828
|
+
│ @boring/workspace. boring-macro deletes its 150-LOC inline │
|
|
1829
|
+
│ copy. │
|
|
1830
|
+
│ │
|
|
1831
|
+
│ 1b. File ops bundle extraction │
|
|
1832
|
+
│ Extract find/grep/read/write/edit into │
|
|
1833
|
+
│ @boring/agent/server/tools/filesystem (a shared bundle). │
|
|
1834
|
+
│ standardCatalog imports the bundle by default; expose │
|
|
1835
|
+
│ `disableDefaultFileTools` on createAgentApp. Move file/ │
|
|
1836
|
+
│ tree/search HTTP routes to @boring/workspace/server. │
|
|
1837
|
+
│ standardCatalog tools (bash, execute_isolated_code) stay. │
|
|
1838
|
+
│ │
|
|
1839
|
+
│ ETA: 1–2 days. │
|
|
1840
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
1841
|
+
│
|
|
1842
|
+
▼
|
|
1843
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
1844
|
+
│ STEP 2 — PLUGIN PRIMITIVES │
|
|
1845
|
+
│ ─────────────────────────────────────────────────────────────── │
|
|
1846
|
+
│ 2a. validateTool extraction │
|
|
1847
|
+
│ Extract from pluginLoader.ts (node-leaky module) into │
|
|
1848
|
+
│ @boring/agent/shared/validateTool.ts (no node imports). │
|
|
1849
|
+
│ pluginLoader imports the extracted version. │
|
|
1850
|
+
│ │
|
|
1851
|
+
│ 2b. Plugin type + definePlugin + validators │
|
|
1852
|
+
│ packages/workspace/src/plugin/{types,definePlugin, │
|
|
1853
|
+
│ validators,bootstrap}.ts. validateAgentTool re-exports │
|
|
1854
|
+
│ from @boring/agent/shared. Single-pass bootstrap. │
|
|
1855
|
+
│ │
|
|
1856
|
+
│ 2c. CatalogRegistry (new) + subscribe retrofit for existing │
|
|
1857
|
+
│ CommandRegistry + PanelRegistry. │
|
|
1858
|
+
│ │
|
|
1859
|
+
│ 2d. Path-aware file-pattern resolver upgrade │
|
|
1860
|
+
│ Replace basename-only matcher (PanelRegistry.ts:91 + │
|
|
1861
|
+
│ SurfaceShell.tsx:98) with path-aware micromatch │
|
|
1862
|
+
│ ({ matchBase: false, dot: true }) + specificity-scoring │
|
|
1863
|
+
│ (segments × 10 + non-wildcard chars). Preserve the │
|
|
1864
|
+
│ app-beats-builtin source tie-breaker. │
|
|
1865
|
+
│ │
|
|
1866
|
+
│ ETA: 1.5–2 days. │
|
|
1867
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
1868
|
+
│
|
|
1869
|
+
▼
|
|
1870
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
1871
|
+
│ STEP 3 — DEFAULT PLUGINS │
|
|
1872
|
+
│ ─────────────────────────────────────────────────────────────── │
|
|
1873
|
+
│ 3a. filesystemPlugin (sole default; v7.1 UI-only) │
|
|
1874
|
+
│ Plain module-scope const (NO factory, NO agentTools field). │
|
|
1875
|
+
│ Contributes: Files catalog; FileTree (placement: │
|
|
1876
|
+
│ 'left-tab'); CodeEditor + MarkdownEditor (with │
|
|
1877
|
+
│ filePatterns). File ops tools are HARNESS substrate │
|
|
1878
|
+
│ registered by createAgentApp via pi-tools-migration's │
|
|
1879
|
+
│ buildFilesystemAgentTools(bundle) — NOT the plugin's job. │
|
|
1880
|
+
│ │
|
|
1881
|
+
│ (v6 had dataCatalogPlugin as a second default; v6.2 cuts │
|
|
1882
|
+
│ it — plugins that want a workbench data tab contribute │
|
|
1883
|
+
│ their own left-tab panel.) │
|
|
1884
|
+
│ │
|
|
1885
|
+
│ ETA: 0.5 day. │
|
|
1886
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
1887
|
+
│
|
|
1888
|
+
▼
|
|
1889
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
1890
|
+
│ STEP 4 — ENTRY POINTS │
|
|
1891
|
+
│ ─────────────────────────────────────────────────────────────── │
|
|
1892
|
+
│ 4a. <WorkspaceProvider plugins={…}> │
|
|
1893
|
+
│ Adds plugins prop + excludeDefaults prop. Auto-registers │
|
|
1894
|
+
│ defaults; runs bootstrap(). Adds package.json "./events" │
|
|
1895
|
+
│ export so plugins can `import { events } from │
|
|
1896
|
+
│ "@boring/workspace/events"`. │
|
|
1897
|
+
│ │
|
|
1898
|
+
│ 4b. createWorkspaceAgentApp({ plugins }) │
|
|
1899
|
+
│ Plain wrap of createAgentApp (NO disableDefaultFileTools │
|
|
1900
|
+
│ passed — v7.0 simplification: harness owns tools always). │
|
|
1901
|
+
│ Runs bootstrap() for server-side fan-in. Registers │
|
|
1902
|
+
│ substrate routes (/api/v1/ui/*, /files, /tree, /files/ │
|
|
1903
|
+
│ search) directly. │
|
|
1904
|
+
│ │
|
|
1905
|
+
│ ETA: 1 day. │
|
|
1906
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
1907
|
+
│
|
|
1908
|
+
▼
|
|
1909
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
1910
|
+
│ STEP 5 — CONSUMER REFACTORS │
|
|
1911
|
+
│ ─────────────────────────────────────────────────────────────── │
|
|
1912
|
+
│ 5a. <CommandPalette /> consumes useCatalogs() + polymorphic │
|
|
1913
|
+
│ Recent. Drop dead fileSearchFn / onOpenFile props. │
|
|
1914
|
+
│ Migrate existing localStorage Recent entries. │
|
|
1915
|
+
│ │
|
|
1916
|
+
│ 5b. <ChatCenteredShell /> migration (REVISED — gemini P0) │
|
|
1917
|
+
│ - ALL ChatCenteredShell-internal commands STAY as │
|
|
1918
|
+
│ imperative useEffect+registerCommand calls (toggleDrawer/ │
|
|
1919
|
+
│ toggleSurface/newChat AND per-session quick-switch). │
|
|
1920
|
+
│ Reason (gemini): toggleDrawer/toggleSurface close over │
|
|
1921
|
+
│ local useState — a module-scope "internal chat-shell │
|
|
1922
|
+
│ plugin" can't reach component instance state, and │
|
|
1923
|
+
│ bridging via events would just shift the same closure │
|
|
1924
|
+
│ problem to the event handler. Keeping these imperative │
|
|
1925
|
+
│ is honest: the plugin model is for module-stable │
|
|
1926
|
+
│ contributions; component-instance commands belong inside │
|
|
1927
|
+
│ the component. │
|
|
1928
|
+
│ - Registry's subscribe retrofit (Step 2c) ensures these │
|
|
1929
|
+
│ late registrations propagate to an open palette — that │
|
|
1930
|
+
│ was the original justification for the retrofit. │
|
|
1931
|
+
│ - Drop `data: DataPaneConfig` prop. Hosts that want a │
|
|
1932
|
+
│ workbench data tab register their own left-tab panel │
|
|
1933
|
+
│ or compose the reusable `createDataCatalogPlugin(opts)` │
|
|
1934
|
+
│ / `appendDataCatalogOutputs(...)` helpers. │
|
|
1935
|
+
│ - Drop `extraPanels` prop. Panels come from PanelRegistry; │
|
|
1936
|
+
│ new optional `allowedPanels?: string[]` for gating. │
|
|
1937
|
+
│ - KEEP `chatSuggestions: ChatSuggestion[]` prop. │
|
|
1938
|
+
│ │
|
|
1939
|
+
│ 5c. WorkbenchLeftPane registry-driven + SurfaceShell fallback │
|
|
1940
|
+
│ fix │
|
|
1941
|
+
│ - Read 'left-tab' panels from PanelRegistry. defaults │
|
|
1942
|
+
│ contribute their respective tabs. excludeDefaults: │
|
|
1943
|
+
│ ['filesystem'] truly removes the tab. │
|
|
1944
|
+
│ - Replace SurfaceShell.tsx:81-108 hardcoded fallback with: │
|
|
1945
|
+
│ registry resolve → registered fallback (only if has()) → │
|
|
1946
|
+
│ EmptyFilePanel. │
|
|
1947
|
+
│ - 'right-tab' placement reserved; no Phase 1 consumer. │
|
|
1948
|
+
│ │
|
|
1949
|
+
│ ETA: 1.5 days. │
|
|
1950
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
1951
|
+
│
|
|
1952
|
+
▼
|
|
1953
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
1954
|
+
│ STEP 6 — ACCEPTANCE: BORING-MACRO MIGRATION │
|
|
1955
|
+
│ ─────────────────────────────────────────────────────────────── │
|
|
1956
|
+
│ See §"Concrete before/after". Net: ~260 LOC → ~30 LOC. │
|
|
1957
|
+
│ ETA: 0.5–1 day. │
|
|
1958
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
1959
|
+
│
|
|
1960
|
+
▼
|
|
1961
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
1962
|
+
│ STEP 7 — TESTS + RELEASE NOTES │
|
|
1963
|
+
│ ─────────────────────────────────────────────────────────────── │
|
|
1964
|
+
│ Tests per §Test plan. Release notes documenting the THREE │
|
|
1965
|
+
│ breaking changes (CommandPaletteProps, │
|
|
1966
|
+
│ ChatCenteredShellProps.{data,extraPanels,withCommandPalette}, │
|
|
1967
|
+
│ WorkbenchLeftPane internal tab API). ETA: 1–2 days. │
|
|
1968
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
1969
|
+
|
|
1970
|
+
TOTAL: ~6–8 days of focused work.
|
|
1971
|
+
```
|
|
1972
|
+
|
|
1973
|
+
## Concrete before/after — boring-macro migration
|
|
1974
|
+
|
|
1975
|
+
Acceptance test for the model.
|
|
1976
|
+
|
|
1977
|
+
### BEFORE (today)
|
|
1978
|
+
|
|
1979
|
+
```ts
|
|
1980
|
+
// apps/boring-macro-v2/src/web/App.tsx — ~80 LOC
|
|
1981
|
+
const dataPaneConfig: DataPaneConfig = { /* …seriesAdapter, filesAdapter… */ }
|
|
1982
|
+
const macroPanels: PanelConfig[] = [chartCanvasPanel, deckPanel]
|
|
1983
|
+
const macroChatSuggestions = [/* …8 suggestions… */]
|
|
1984
|
+
|
|
1985
|
+
export function App() {
|
|
1986
|
+
return (
|
|
1987
|
+
<WorkspaceProvider panels={macroPanels}>
|
|
1988
|
+
<ChatCenteredShell
|
|
1989
|
+
data={dataPaneConfig}
|
|
1990
|
+
chatSuggestions={macroChatSuggestions}
|
|
1991
|
+
extraPanels={macroPanels}
|
|
1992
|
+
/>
|
|
1993
|
+
</WorkspaceProvider>
|
|
1994
|
+
)
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
// apps/boring-macro-v2/src/server/index.ts — ~30 LOC
|
|
1998
|
+
const clickhouse = await createClickHouseClient(env)
|
|
1999
|
+
const app = await createAgentApp({
|
|
2000
|
+
workspaceRoot,
|
|
2001
|
+
extraTools: [...macroAgentTools, ...uiTools],
|
|
2002
|
+
})
|
|
2003
|
+
await app.register(uiRoutes)
|
|
2004
|
+
await app.register(registerMacroRoutes, { clickhouse, deckRoot })
|
|
2005
|
+
await app.listen({ port })
|
|
2006
|
+
|
|
2007
|
+
// apps/boring-macro-v2/src/server/uiBridge.ts — ~150 LOC
|
|
2008
|
+
// Full inlined copy of @boring/workspace/server's UI bridge.
|
|
2009
|
+
```
|
|
2010
|
+
|
|
2011
|
+
### AFTER (Phase 1 done)
|
|
2012
|
+
|
|
2013
|
+
**Two plugin objects, same id, split by environment** (this honors
|
|
2014
|
+
the build invariant — never cross-import `node:*` symbols into
|
|
2015
|
+
client code; codex round-3 P2 caught the v6 example violating its
|
|
2016
|
+
own rule):
|
|
2017
|
+
|
|
2018
|
+
```ts
|
|
2019
|
+
// apps/boring-macro-v2/src/plugin/index.ts — CLIENT entry, ~18 LOC
|
|
2020
|
+
"use client"
|
|
2021
|
+
import { definePlugin, type Plugin } from "@boring/workspace"
|
|
2022
|
+
import type { ChatSuggestion } from "@boring/agent/front-shadcn"
|
|
2023
|
+
import { chartCanvasPanel, deckPanel, macroSeriesPanel } from "./panels"
|
|
2024
|
+
import { seriesCatalog } from "./catalogs"
|
|
2025
|
+
|
|
2026
|
+
export const macroChatSuggestions: ChatSuggestion[] = [
|
|
2027
|
+
{ label: "Find a series", prompt: "Help me find a macro series." },
|
|
2028
|
+
// …
|
|
2029
|
+
]
|
|
2030
|
+
|
|
2031
|
+
export const makeMacroClientPlugin = (): Plugin =>
|
|
2032
|
+
definePlugin({
|
|
2033
|
+
id: "boring-macro",
|
|
2034
|
+
label: "Macro",
|
|
2035
|
+
panels: [chartCanvasPanel, deckPanel, macroSeriesPanel],
|
|
2036
|
+
catalogs: [seriesCatalog],
|
|
2037
|
+
})
|
|
2038
|
+
```
|
|
2039
|
+
|
|
2040
|
+
```ts
|
|
2041
|
+
// apps/boring-macro-v2/src/plugin/server.ts — SERVER entry, ~10 LOC
|
|
2042
|
+
"use server"
|
|
2043
|
+
import { definePlugin, type Plugin } from "@boring/workspace"
|
|
2044
|
+
import { macroAgentTools } from "../server/macroTools"
|
|
2045
|
+
|
|
2046
|
+
export const makeMacroServerPlugin = (): Plugin =>
|
|
2047
|
+
definePlugin({
|
|
2048
|
+
id: "boring-macro", // same id as client; bootstrap dedupes
|
|
2049
|
+
label: "Macro",
|
|
2050
|
+
agentTools: macroAgentTools,
|
|
2051
|
+
})
|
|
2052
|
+
```
|
|
2053
|
+
|
|
2054
|
+
`macroSeriesPanel` is the workbench-data-tab equivalent of the v5
|
|
2055
|
+
`data: DataPaneConfig`: a panel with `placement: 'left-tab'` whose
|
|
2056
|
+
component is `DataExplorer` configured with macro's adapter and
|
|
2057
|
+
`onActivate` that calls `surface.openPanel({ component:
|
|
2058
|
+
"chart-canvas", … })`. This replaces the v5 dataPaneConfig wiring
|
|
2059
|
+
without needing a default `dataCatalogPlugin`. The catalog
|
|
2060
|
+
(`seriesCatalog`) stays separate — it's what powers the cmd palette
|
|
2061
|
+
search; the panel is what shows the workbench browser.
|
|
2062
|
+
|
|
2063
|
+
```tsx
|
|
2064
|
+
// apps/boring-macro-v2/src/web/App.tsx — ~7 LOC
|
|
2065
|
+
import { WorkspaceProvider, ChatCenteredShell } from "@boring/workspace"
|
|
2066
|
+
import { makeMacroClientPlugin, macroChatSuggestions } from "../plugin"
|
|
2067
|
+
|
|
2068
|
+
export const App = () => (
|
|
2069
|
+
<WorkspaceProvider plugins={[makeMacroClientPlugin()]}>
|
|
2070
|
+
<ChatCenteredShell chatSuggestions={macroChatSuggestions} />
|
|
2071
|
+
</WorkspaceProvider>
|
|
2072
|
+
)
|
|
2073
|
+
```
|
|
2074
|
+
|
|
2075
|
+
```ts
|
|
2076
|
+
// apps/boring-macro-v2/src/server/index.ts — ~11 LOC
|
|
2077
|
+
import { createWorkspaceAgentApp } from "@boring/workspace/server"
|
|
2078
|
+
import { makeMacroServerPlugin } from "../plugin/server"
|
|
2079
|
+
import { registerMacroRoutes } from "./macroRoutes"
|
|
2080
|
+
|
|
2081
|
+
const clickhouse = await createClickHouseClient(env)
|
|
2082
|
+
const app = await createWorkspaceAgentApp({
|
|
2083
|
+
workspaceRoot,
|
|
2084
|
+
plugins: [makeMacroServerPlugin()],
|
|
2085
|
+
})
|
|
2086
|
+
await app.register(registerMacroRoutes, { clickhouse, deckRoot })
|
|
2087
|
+
await app.listen({ port })
|
|
2088
|
+
|
|
2089
|
+
// DELETED: apps/boring-macro-v2/src/server/uiBridge.ts (~150 LOC)
|
|
2090
|
+
```
|
|
2091
|
+
|
|
2092
|
+
### LOC accounting
|
|
2093
|
+
|
|
2094
|
+
| File | Before | After | Δ |
|
|
2095
|
+
|---|--:|--:|--:|
|
|
2096
|
+
| `src/web/App.tsx` | 80 | 6 | -74 |
|
|
2097
|
+
| `src/server/index.ts` | 30 | 11 | -19 |
|
|
2098
|
+
| `src/server/uiBridge.ts` | 150 | 0 | -150 |
|
|
2099
|
+
| `src/plugin/index.ts` (client) | 0 | 18 | +18 |
|
|
2100
|
+
| `src/plugin/server.ts` | 0 | 10 | +10 |
|
|
2101
|
+
| **Total** | **260** | **46** | **-214 (-82%)** |
|
|
2102
|
+
|
|
2103
|
+
## Known gaps — deferred to Phase 2+
|
|
2104
|
+
|
|
2105
|
+
Things v6.1 deliberately does NOT solve, listed explicitly so
|
|
2106
|
+
reviewers don't think they were missed.
|
|
2107
|
+
|
|
2108
|
+
| Gap | Why deferred | When to revisit |
|
|
2109
|
+
|---|---|---|
|
|
2110
|
+
| **Hot-reload / unregister cleanup** | Fastify routes can't unregister; React subscriptions clean themselves; catalog adapters with their own state would leak. No Phase 1 plugin uses this. | Phase 3 hot-reload story. |
|
|
2111
|
+
| **Plugin-vs-substrate tool name collision** | A plugin shipping a tool called `read` replaces substrate's. Late-wins logged. Could confuse the LLM if names diverge mid-session. | When agent-authored plugins (Phase 3) make accidental collision likely. |
|
|
2112
|
+
| **Catalog adapter memory** | If an adapter holds a long-running subscription (e.g., websocket to a remote search service), there's no place to clean up because there's no `onUnmount`. | When a real adapter needs it. Adding a teardown hook on `CatalogConfig` is non-breaking. |
|
|
2113
|
+
| **Non-React stateful adapters need lifecycle** | Gemini P1: an adapter that wants to subscribe to `events.on('file:moved', …)` to invalidate its cache has nowhere to do it. Module-scope `events.on(...)` fires globally for all hosts. Lazy on-first-call leaks (no unsubscribe). | First plugin that needs it. Re-introduce `Plugin.onMount(ctx) → cleanup` (we cut it in v6 because Phase 1 plugins are all React-component-based or factory-injected; that assumption breaks for stateful adapters). |
|
|
2114
|
+
| **`registerAgentRoutes` lacks `disableDefaultFileTools`** | Codex round-4 P1: `createAgentApp` accepts `disableDefaultFileTools` (verified `createAgentApp.ts:47`) but `registerAgentRoutes` (the embedded-Fastify path used by `apps/full-app/src/server/main.ts:383`) does NOT — it always includes filesystem tools (`registerAgentRoutes.ts:94`). The "harness opt-out" promise only holds for the standalone `createAgentApp` path. | Add `disableDefaultFileTools` option to `registerAgentRoutes` plumbed through to its internal tool-catalog construction. **Out of scope for Phase 1** macro acceptance (macro uses `createWorkspaceAgentApp` which uses `createAgentApp`, not `registerAgentRoutes`). Add a follow-up bead under the j9p7 epic OR Phase 2 if a `registerAgentRoutes`-using host needs the opt-out. |
|
|
2115
|
+
| **Build-time enforcement of client/server split** | Documented invariant; not a custom lint rule. | If accidental cross-imports become common. |
|
|
2116
|
+
| **Plugin versioning / compat** | `Plugin.version` field cut. No compat negotiation. | Phase 2 npm distribution. |
|
|
2117
|
+
| **Layout migrations** | Renaming a panel id breaks cached dockview layouts. Same problem as today; not made worse by plugin model. | When layouts become stable enough to merit migration tooling. |
|
|
2118
|
+
| **System-prompt augmentation from registered plugins** | LLM doesn't currently see "these plugins are loaded; here's what they do." | Phase 2 discovery endpoint + prompt injection. |
|
|
2119
|
+
| **Permission / capability gating** | Inline plugins run with full host privileges. | Phase 3 sandbox / capability flags. |
|
|
2120
|
+
| **Per-plugin telemetry** | No per-plugin error counter / call latency telemetry. | When debug volume justifies it. |
|
|
2121
|
+
| ~~`<PluginInspector />`~~ | **PROMOTED to Phase 1 in v7.2** — see §"Phase 1 debug overlay". | (no longer deferred) |
|
|
2122
|
+
| **Plugin discovery via `/api/v1/plugins`** | Phase 1 hosts know what they imported. | Phase 2 npm + agent-authored. |
|
|
2123
|
+
|
|
2124
|
+
None of these block the boring-macro acceptance test.
|
|
2125
|
+
|
|
2126
|
+
## Phase 1.5: Consumer migration — decompose shells, declarative layouts, delete ChatCenteredShell
|
|
2127
|
+
|
|
2128
|
+
(v7.4 merger: was tracked separately as `DECLARATIVE_LAYOUT_MIGRATION.md` + epic `boring-ui-v2-zrby`. Folded in here as Phase 1.5 because it's the natural consumer-side migration of the plugin model machinery.)
|
|
2129
|
+
|
|
2130
|
+
### Why this phase exists
|
|
2131
|
+
|
|
2132
|
+
`@boring/workspace` ships TWO parallel layout systems today:
|
|
2133
|
+
|
|
2134
|
+
1. **Imperative shell** — `ChatCenteredShell` (29KB) + `SurfaceShell` (23KB) + `ChatTopBar` + `SessionBrowser` + `WorkbenchLeftPane` + `ChatStagePlaceholder`, all under `src/components/chat/`. Hardcodes the chat panel from `@boring/agent`, top-bar slots, session drawer, artifact dockview. Apps pass props in; they cannot restructure.
|
|
2135
|
+
|
|
2136
|
+
2. **Declarative layouts** — `ChatLayout` + `IdeLayout` + `ResponsiveDockviewShell` in `src/layouts/`. Compose panels by id (e.g. `<ChatLayout nav="session-list" center="chat" surface="artifact-surface" />`). Plugins register panels into the registry; the layout config resolves ids → components via dockview.
|
|
2137
|
+
|
|
2138
|
+
Today every consuming app (`apps/workspace-playground`, `apps/full-app`, `apps/boring-macro-v2`) uses **only the imperative shell**. The declarative layouts have **zero app consumers** — tested and exported but never reached.
|
|
2139
|
+
|
|
2140
|
+
The plugin model machinery (Phase 1 Steps 1-5) finally makes declarative composition viable: panels register via the contract; layouts resolve ids → components. **Phase 1.5 retires the imperative shell and migrates all three apps to the declarative pattern**, closing the loop the earlier sketches opened.
|
|
2141
|
+
|
|
2142
|
+
### Three-tier API after migration
|
|
2143
|
+
|
|
2144
|
+
All three tiers share the SAME core panel registrations (chat, session-list, workbench-left, artifact-surface). Only the shape composition differs.
|
|
2145
|
+
|
|
2146
|
+
#### Tier 1 — declarative pre-shaped layouts (~80% of apps)
|
|
2147
|
+
|
|
2148
|
+
```tsx
|
|
2149
|
+
import { WorkspaceProvider, ChatLayout, TopBar } from "@boring/workspace"
|
|
2150
|
+
import { macroPlugin } from "./macro-plugin"
|
|
2151
|
+
|
|
2152
|
+
<WorkspaceProvider plugins={[macroPlugin]}>
|
|
2153
|
+
<TopBar appTitle="Macro" right={<UserMenu />} />
|
|
2154
|
+
<ChatLayout
|
|
2155
|
+
nav="session-list"
|
|
2156
|
+
center="chat"
|
|
2157
|
+
sidebar="charts"
|
|
2158
|
+
surface="artifact-surface"
|
|
2159
|
+
/>
|
|
2160
|
+
</WorkspaceProvider>
|
|
2161
|
+
```
|
|
2162
|
+
|
|
2163
|
+
Apps swap panels by changing `nav` / `center` / `sidebar` / `surface` ids. Plugins register the implementations.
|
|
2164
|
+
|
|
2165
|
+
#### Tier 2 — custom LayoutConfig with stock chrome
|
|
2166
|
+
|
|
2167
|
+
```tsx
|
|
2168
|
+
import { WorkspaceProvider, ResponsiveDockviewShell, TopBar, type LayoutConfig } from "@boring/workspace"
|
|
2169
|
+
|
|
2170
|
+
const myLayout: LayoutConfig = {
|
|
2171
|
+
version: "2.0",
|
|
2172
|
+
groups: [
|
|
2173
|
+
{ id: "rail", position: "left", panel: "session-list", locked: true, hideHeader: true },
|
|
2174
|
+
{ id: "tree", position: "left", panel: "filetree", collapsible: true },
|
|
2175
|
+
{ id: "center", position: "center", panel: "chat" },
|
|
2176
|
+
{ id: "split-a", position: "right", panel: "code-editor" },
|
|
2177
|
+
{ id: "split-b", position: "right", panel: "live-preview" },
|
|
2178
|
+
],
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
<WorkspaceProvider plugins={[livePreviewPlugin]}>
|
|
2182
|
+
<TopBar appTitle="…" />
|
|
2183
|
+
<ResponsiveDockviewShell layout={myLayout} />
|
|
2184
|
+
</WorkspaceProvider>
|
|
2185
|
+
```
|
|
2186
|
+
|
|
2187
|
+
For apps that need non-stock layout shapes (split center, multiple right surfaces, custom group constraints) but still want responsive sidebar collapse + dockview integration + same `<TopBar>` chrome.
|
|
2188
|
+
|
|
2189
|
+
#### Tier 3 — full custom (rare)
|
|
2190
|
+
|
|
2191
|
+
```tsx
|
|
2192
|
+
import {
|
|
2193
|
+
WorkspaceProvider,
|
|
2194
|
+
DockviewShell,
|
|
2195
|
+
useViewportBreakpoint,
|
|
2196
|
+
useResponsiveSidebarCollapse,
|
|
2197
|
+
useTopBarSlot,
|
|
2198
|
+
} from "@boring/workspace"
|
|
2199
|
+
|
|
2200
|
+
function MyShell() {
|
|
2201
|
+
// App fully composes chrome + dockview structure
|
|
2202
|
+
}
|
|
2203
|
+
```
|
|
2204
|
+
|
|
2205
|
+
Raw primitives. For apps with bespoke chrome (non-rectangular layout, multiple dockview instances, embedded workspace inside a non-workspace shell).
|
|
2206
|
+
|
|
2207
|
+
### Substrate vs plugin (architectural commitment)
|
|
2208
|
+
|
|
2209
|
+
The migration is a chance to clarify what's substrate vs. plugin:
|
|
2210
|
+
|
|
2211
|
+
| Tier | What it is | Examples |
|
|
2212
|
+
|---|---|---|
|
|
2213
|
+
| **Substrate** | Constitutive workspace panels. Without them `@boring/workspace` is an empty dockview. Apps cannot opt out — these ARE what the package is. | `chat`, `session-list`, `workbench-left`, `artifact-surface` |
|
|
2214
|
+
| **Default plugins** | Optional capabilities apps can disable via `excludeDefaults: ['filesystem']`. | `filesystemPlugin` (file tree + code editor + markdown editor + filesCatalog) |
|
|
2215
|
+
| **App plugins** | Host-specific contributions registered via `<WorkspaceProvider plugins={[...]}>`. | `macroPlugin` (charts + slides + series), future `analyticsPlugin`, etc. |
|
|
2216
|
+
|
|
2217
|
+
**There is no `chatExperiencePlugin`.** Chat is core, not a plugin. The earlier sketch of a "chat experience plugin" bundling the chat-flavored panels was wrong — it conflates substrate with extension.
|
|
2218
|
+
|
|
2219
|
+
`WorkspaceProvider` registers core panels at mount, before running the plugin bootstrap:
|
|
2220
|
+
|
|
2221
|
+
```ts
|
|
2222
|
+
function WorkspaceProvider({ plugins, excludeDefaults, children }) {
|
|
2223
|
+
const panelRegistry = new PanelRegistry()
|
|
2224
|
+
panelRegistry.registerAll(coreWorkspacePanels) // ← substrate, always
|
|
2225
|
+
bootstrap({
|
|
2226
|
+
plugins,
|
|
2227
|
+
defaults: filteredDefaults, // filesystemPlugin unless excluded
|
|
2228
|
+
registries,
|
|
2229
|
+
})
|
|
2230
|
+
// …
|
|
2231
|
+
}
|
|
2232
|
+
```
|
|
2233
|
+
|
|
2234
|
+
### File-tree end state (v7.5 — final framing)
|
|
2235
|
+
|
|
2236
|
+
Three architectural commitments locked in v7.5:
|
|
2237
|
+
|
|
2238
|
+
1. **Top-level split: `front/` + `server/` + `shared/` + `plugins/`.**
|
|
2239
|
+
Mirrors `@boring/agent`'s convention. `front/` = client-side
|
|
2240
|
+
workspace code; `server/` = backend; `shared/` = cross-process
|
|
2241
|
+
types (used by both halves). `plugins/` is a sibling concern
|
|
2242
|
+
holding **actual plugin instances only** (no machinery —
|
|
2243
|
+
machinery lives in `front/plugin/` + `server/plugin/` + `shared/plugin/`).
|
|
2244
|
+
|
|
2245
|
+
2. **Plugins own both halves of their contributions.** A plugin in
|
|
2246
|
+
`plugins/<id>/` contains internal `front/` + `server/` directories
|
|
2247
|
+
for its own client-side panels/sidebar/catalogs and server-side
|
|
2248
|
+
tools/routes. Apps follow the same shape via `apps/<host>/src/plugin/{front,server}/`.
|
|
2249
|
+
|
|
2250
|
+
3. **Singular vs plural naming convention.**
|
|
2251
|
+
- `plugin/` (singular) = plugin model **machinery** (definePlugin,
|
|
2252
|
+
bootstrap, hooks, registries) — lives in `front/plugin/`,
|
|
2253
|
+
`server/plugin/`, `shared/plugin/`.
|
|
2254
|
+
- `plugins/` (plural) = a directory of plugin **instances** —
|
|
2255
|
+
lives at the workspace root for defaults; apps with multiple
|
|
2256
|
+
plugins use `apps/<host>/src/plugins/`.
|
|
2257
|
+
|
|
2258
|
+
```
|
|
2259
|
+
packages/workspace/src/
|
|
2260
|
+
|
|
2261
|
+
# ─── FRONT (workspace own client + plugin model client machinery) ──
|
|
2262
|
+
|
|
2263
|
+
front/
|
|
2264
|
+
├── chrome/ shell parts (NOT plugin contributions)
|
|
2265
|
+
│ ├── chat/ NEW (Phase A — substrate chat panel wrapper)
|
|
2266
|
+
│ │ ├── ChatPanel.tsx thin wrapper around @boring/agent's ChatPanel
|
|
2267
|
+
│ │ │ + workspace integrations
|
|
2268
|
+
│ │ └── definition.ts definePanel({ id: "chat", … })
|
|
2269
|
+
│ ├── session-list/ NEW (Phase A — was components/chat/SessionBrowser)
|
|
2270
|
+
│ ├── workbench-left/ NEW (Phase A — tab-strip chrome that HOSTS
|
|
2271
|
+
│ │ sidebar tabs contributed by plugins)
|
|
2272
|
+
│ ├── artifact-surface/ NEW (Phase A — dockview wrapper that HOSTS
|
|
2273
|
+
│ │ workbench panes contributed by plugins)
|
|
2274
|
+
│ ├── chat-stage-placeholder/ NEW (Phase A)
|
|
2275
|
+
│ └── EmptyPane.tsx MOVED (was loose panes/EmptyPane.tsx)
|
|
2276
|
+
│
|
|
2277
|
+
├── components/ cross-cutting UI primitives (NOT panels)
|
|
2278
|
+
│ ├── DataExplorer/ generic data subsystem; used by plugins
|
|
2279
|
+
│ ├── ui/ shadcn primitives
|
|
2280
|
+
│ ├── CommandPalette.tsx substrate palette overlay
|
|
2281
|
+
│ ├── SessionList.tsx data-list helper used by chrome/session-list/
|
|
2282
|
+
│ └── PluginErrorBoundary.tsx NEW (j9p7.22) — wraps plugin contributions
|
|
2283
|
+
│
|
|
2284
|
+
├── registry/ base registries (subscribe-aware singletons)
|
|
2285
|
+
│ ├── PanelRegistry.ts retrofitted (j9p7.6 done)
|
|
2286
|
+
│ ├── CommandRegistry.ts retrofitted (j9p7.6 done)
|
|
2287
|
+
│ ├── RegistryProvider.tsx React context anchor
|
|
2288
|
+
│ ├── coreRegistrations.ts NEW (Phase B — coreWorkspacePanels[])
|
|
2289
|
+
│ ├── types.ts PanelConfig, CommandConfig, PaneProps
|
|
2290
|
+
│ └── getFileIcon.ts
|
|
2291
|
+
│
|
|
2292
|
+
├── dock/ DockviewShell + LayoutConfig types
|
|
2293
|
+
├── events/ bus singleton (client side)
|
|
2294
|
+
├── bridge/ UI bridge client + uiCommandStream/Dispatcher
|
|
2295
|
+
├── hooks/ viewport, sidebar, etc.
|
|
2296
|
+
│
|
|
2297
|
+
├── layout/ composition layer (Tier 1/2/3)
|
|
2298
|
+
│ ├── ChatLayout.tsx
|
|
2299
|
+
│ ├── IdeLayout.tsx
|
|
2300
|
+
│ ├── ResponsiveDockviewShell.tsx
|
|
2301
|
+
│ ├── TopBar.tsx NEW (Phase C — was components/chat/ChatTopBar)
|
|
2302
|
+
│ └── index.ts
|
|
2303
|
+
│
|
|
2304
|
+
├── chrome/empty-file-panel/ NEW (j9p7.12 — fallback for unmatched
|
|
2305
|
+
│ files; relocated from panes/ in v7.6
|
|
2306
|
+
│ per gemini P2 — kills the lone-resident
|
|
2307
|
+
│ panes/ folder at workspace root)
|
|
2308
|
+
│
|
|
2309
|
+
├── plugin/ ← plugin model FRONT machinery (singular)
|
|
2310
|
+
│ ├── CatalogRegistry.ts (DONE — file at packages/workspace/src/plugin/
|
|
2311
|
+
│ │ today; moves under front/plugin/ in restructure)
|
|
2312
|
+
│ ├── PluginErrorBoundary.tsx NEW (j9p7.22)
|
|
2313
|
+
│ ├── PluginInspector.tsx NEW (j9p7.23, DEV-only)
|
|
2314
|
+
│ ├── usePlugins.ts NEW
|
|
2315
|
+
│ ├── useCatalogs.ts (DONE)
|
|
2316
|
+
│ ├── useCommands.ts (DONE)
|
|
2317
|
+
│ ├── useActivePanels.ts (DONE)
|
|
2318
|
+
│ └── index.ts
|
|
2319
|
+
│
|
|
2320
|
+
└── WorkspaceProvider.tsx
|
|
2321
|
+
|
|
2322
|
+
# ─── SERVER (workspace own backend + plugin model server machinery) ─
|
|
2323
|
+
|
|
2324
|
+
server/
|
|
2325
|
+
├── createWorkspaceAgentApp.ts
|
|
2326
|
+
├── http/ substrate routes (uiRoutes, fileRoutes)
|
|
2327
|
+
├── ui-bridge/ createInMemoryBridge
|
|
2328
|
+
└── plugin/ ← plugin model SERVER machinery (singular)
|
|
2329
|
+
└── … (server-side registries — currently minimal;
|
|
2330
|
+
AgentToolRegistry interface, etc.)
|
|
2331
|
+
|
|
2332
|
+
# ─── SHARED (types + functions both halves need) ───────────────────
|
|
2333
|
+
|
|
2334
|
+
shared/
|
|
2335
|
+
├── ui-bridge.ts UiState, UiCommand types
|
|
2336
|
+
├── events/ WorkspaceEventMap types
|
|
2337
|
+
└── plugin/ ← plugin model SHARED (singular)
|
|
2338
|
+
├── types.ts Plugin, CatalogConfig (DONE — needs v7.2
|
|
2339
|
+
│ systemPrompt addition per j9p7.4)
|
|
2340
|
+
├── definePlugin.ts factory + validation (DONE; both halves use)
|
|
2341
|
+
├── bootstrap.ts single-pass mount (DONE — needs v7.2
|
|
2342
|
+
│ systemPromptAppend output per j9p7.7)
|
|
2343
|
+
└── index.ts
|
|
2344
|
+
|
|
2345
|
+
# ─── PLUGINS (actual plugin instances; plural) ─────────────────────
|
|
2346
|
+
|
|
2347
|
+
plugins/ ← directory of plugin instances
|
|
2348
|
+
└── filesystemPlugin/ ← one folder per plugin
|
|
2349
|
+
├── index.ts exports CLIENT + SERVER Plugins (same id)
|
|
2350
|
+
├── front/ client-side contributions
|
|
2351
|
+
│ ├── panes/
|
|
2352
|
+
│ │ ├── CodeEditorPane.tsx MOVED (was src/panes/code-editor/)
|
|
2353
|
+
│ │ └── MarkdownEditorPane.tsx MOVED (was src/panes/markdown-editor/)
|
|
2354
|
+
│ ├── sidebar/
|
|
2355
|
+
│ │ └── FileTreePane.tsx MOVED (was src/panes/file-tree/)
|
|
2356
|
+
│ └── catalogs/
|
|
2357
|
+
│ └── filesCatalog.ts
|
|
2358
|
+
└── server/ server-side contributions
|
|
2359
|
+
└── … (filesystemPlugin v7 has NONE — UI-only;
|
|
2360
|
+
future plugins add agentTools, route handlers)
|
|
2361
|
+
|
|
2362
|
+
# ─── BARREL ────────────────────────────────────────────────────────
|
|
2363
|
+
|
|
2364
|
+
index.ts package public API (re-exports from front/,
|
|
2365
|
+
layout/, plugin model bits, plugins barrel)
|
|
2366
|
+
```
|
|
2367
|
+
|
|
2368
|
+
**Apps follow the same shape — singular `plugin/` for the host's one plugin:**
|
|
2369
|
+
|
|
2370
|
+
```
|
|
2371
|
+
apps/boring-macro-v2/src/
|
|
2372
|
+
|
|
2373
|
+
plugin/ ← THIS app's plugin (singular: one per host)
|
|
2374
|
+
├── index.ts exports CLIENT + SERVER Plugins (same id)
|
|
2375
|
+
├── front/ client-side contributions
|
|
2376
|
+
│ ├── panes/
|
|
2377
|
+
│ │ ├── ChartCanvasPane.tsx
|
|
2378
|
+
│ │ └── DeckPane.tsx
|
|
2379
|
+
│ ├── sidebar/MacroSeriesPane.tsx
|
|
2380
|
+
│ └── catalogs/seriesCatalog.ts
|
|
2381
|
+
└── server/ server-side contributions
|
|
2382
|
+
├── tools/ agent tool implementations
|
|
2383
|
+
│ ├── execute_sql.ts
|
|
2384
|
+
│ ├── macro_search.ts
|
|
2385
|
+
│ ├── get_series_data.ts
|
|
2386
|
+
│ └── persist_derived_series.ts
|
|
2387
|
+
└── routes/macroRoutes.ts Fastify route plugin
|
|
2388
|
+
|
|
2389
|
+
web/App.tsx app front entry (mounts WorkspaceProvider)
|
|
2390
|
+
server/index.ts app backend entry (Fastify boot)
|
|
2391
|
+
```
|
|
2392
|
+
|
|
2393
|
+
**If an app grows multi-plugin** (rare): rename `plugin/` → `plugins/<id>/` mirroring workspace's convention. Phase 1 macro doesn't need this.
|
|
2394
|
+
|
|
2395
|
+
**Apps follow the same pattern** — every plugin (default or app-contributed) owns its components inside its own directory:
|
|
2396
|
+
|
|
2397
|
+
```
|
|
2398
|
+
apps/boring-macro-v2/src/plugin/ ← macroPlugin lives here
|
|
2399
|
+
├── index.ts the Plugin definition (client half)
|
|
2400
|
+
├── server.ts server-half Plugin (agentTools)
|
|
2401
|
+
├── panes/ ← workbench-center contributions
|
|
2402
|
+
│ ├── ChartCanvasPane.tsx
|
|
2403
|
+
│ └── DeckPane.tsx
|
|
2404
|
+
├── sidebar/ ← sidebar-tab contributions
|
|
2405
|
+
│ └── MacroSeriesPane.tsx
|
|
2406
|
+
├── catalogs/
|
|
2407
|
+
│ └── seriesCatalog.ts
|
|
2408
|
+
└── server/ (paired with src/server/)
|
|
2409
|
+
└── tools/ agent tool implementations
|
|
2410
|
+
├── execute_sql.ts
|
|
2411
|
+
├── macro_search.ts
|
|
2412
|
+
└── …
|
|
2413
|
+
```
|
|
2414
|
+
|
|
2415
|
+
**What disappears:**
|
|
2416
|
+
|
|
2417
|
+
- `src/components/chat/` (whole folder; Phase A + C + G)
|
|
2418
|
+
- Top-level `src/panes/{code-editor, markdown-editor, file-tree}/` — moved INTO
|
|
2419
|
+
`src/plugin/defaults/filesystemPlugin/` (panes/sidebar split per role) by j9p7.9
|
|
2420
|
+
- `src/panes/data-catalog/` — orphaned (dataCatalogPlugin was cut in v6.2);
|
|
2421
|
+
audit during j9p7.30: delete if no consumer, otherwise re-home as a primitive
|
|
2422
|
+
in `components/` for plugin authors who want a generic data-tab implementation
|
|
2423
|
+
- `src/panes/EmptyPane.tsx` (loose) — moved to `chrome/EmptyPane.tsx`
|
|
2424
|
+
- `src/panes/ArtifactSurfacePane.tsx` (loose) — moved to `chrome/artifact-surface/`
|
|
2425
|
+
alongside SurfaceShell (Phase A)
|
|
2426
|
+
|
|
2427
|
+
**What stays exported (Tier 1 / Tier 2 / Tier 3 surface):**
|
|
2428
|
+
|
|
2429
|
+
```ts
|
|
2430
|
+
// Tier 1
|
|
2431
|
+
export { ChatLayout, IdeLayout, buildChatLayout, buildIdeLayout } from "./layouts"
|
|
2432
|
+
export type { ChatLayoutProps, IdeLayoutProps } from "./layouts"
|
|
2433
|
+
export { TopBar } from "./layouts/TopBar"
|
|
2434
|
+
|
|
2435
|
+
// Tier 2
|
|
2436
|
+
export { ResponsiveDockviewShell } from "./layouts/ResponsiveDockviewShell"
|
|
2437
|
+
|
|
2438
|
+
// Tier 3 (raw primitives)
|
|
2439
|
+
export { DockviewShell } from "./dock"
|
|
2440
|
+
export type { LayoutConfig, GroupConfig } from "./dock"
|
|
2441
|
+
export { useViewportBreakpoint, useResponsiveSidebarCollapse } from "./hooks"
|
|
2442
|
+
export { useTopBarSlot } from "./components/TopBarSlot"
|
|
2443
|
+
|
|
2444
|
+
// WorkspaceProvider + plugin model
|
|
2445
|
+
export { WorkspaceProvider } from "./WorkspaceProvider"
|
|
2446
|
+
export { definePlugin, definePanel, PluginError, … } from "./plugin"
|
|
2447
|
+
export { CatalogRegistry, useCommands, useActivePanels, useCatalogs, usePlugins, … } from "./plugin"
|
|
2448
|
+
```
|
|
2449
|
+
|
|
2450
|
+
The default `filesystemPlugin` is exported via `plugin/defaults/`; hosts that want
|
|
2451
|
+
to disable it pass `excludeDefaults: ['filesystem']`. The plugin's INTERNAL
|
|
2452
|
+
components (CodeEditorPane etc.) are NOT exported — they're encapsulated.
|
|
2453
|
+
|
|
2454
|
+
### Why this taxonomy (vs status quo)
|
|
2455
|
+
|
|
2456
|
+
**`panes/` was conflating three distinct categories.** Today
|
|
2457
|
+
`packages/workspace/src/panes/` mixes:
|
|
2458
|
+
|
|
2459
|
+
- Workbench center panes (`code-editor`, `markdown-editor`)
|
|
2460
|
+
- Sidebar tab content (`file-tree`, `data-catalog`)
|
|
2461
|
+
- Shell containers (`ArtifactSurfacePane.tsx`, `EmptyPane.tsx`)
|
|
2462
|
+
|
|
2463
|
+
Each of these has different runtime semantics (centered tab vs persistent sidebar vs chrome wrapper) and different ownership (filesystem-plugin vs filesystem-plugin vs substrate). Mixing them blurred the architecture and made plugin authors uncertain where their contributions belong.
|
|
2464
|
+
|
|
2465
|
+
**Plugin authors need a clear template.** The `panes/`-flat-at-workspace-root pattern told plugin authors "drop your panel here" — but that pattern is only correct for workspace substrate. Plugins should bundle their components, not scatter them. v7.5 makes the right path the obvious path: **everything a plugin contributes lives inside the plugin's directory**, mirroring what Phase 2's npm distribution will require anyway (a distributed plugin SHIPS its components).
|
|
2466
|
+
|
|
2467
|
+
**Apps and defaults follow the SAME pattern.** A `pi-plugin-foo` npm package's `dist/client/` will look identical to `apps/boring-macro-v2/src/plugin/` and `packages/workspace/src/plugin/defaults/filesystemPlugin/`. Same shape; different distribution channel.
|
|
2468
|
+
|
|
2469
|
+
**What disappears:**
|
|
2470
|
+
|
|
2471
|
+
- `src/components/chat/` (whole folder)
|
|
2472
|
+
- `ChatCenteredShell.tsx` + `ChatShellContext` (`context.ts`)
|
|
2473
|
+
- The old `presets.test.tsx` (superseded by migrated apps' e2e + new layout tests)
|
|
2474
|
+
- `src/index.ts` exports for `ChatCenteredShell`, `useChatShell`, `useChatSurface`, `ChatStagePlaceholder` (imperative-shell internals)
|
|
2475
|
+
|
|
2476
|
+
**What stays exported (Tier 1 / Tier 2 / Tier 3 surface):**
|
|
2477
|
+
|
|
2478
|
+
```ts
|
|
2479
|
+
// Tier 1
|
|
2480
|
+
export { ChatLayout, IdeLayout, buildChatLayout, buildIdeLayout } from "./layouts"
|
|
2481
|
+
export type { ChatLayoutProps, IdeLayoutProps } from "./layouts"
|
|
2482
|
+
export { TopBar } from "./layouts/TopBar"
|
|
2483
|
+
|
|
2484
|
+
// Tier 2
|
|
2485
|
+
export { ResponsiveDockviewShell } from "./layouts/ResponsiveDockviewShell"
|
|
2486
|
+
|
|
2487
|
+
// Tier 3 (raw primitives)
|
|
2488
|
+
export { DockviewShell } from "./dock"
|
|
2489
|
+
export type { LayoutConfig, GroupConfig } from "./dock"
|
|
2490
|
+
export { useViewportBreakpoint, useResponsiveSidebarCollapse } from "./hooks"
|
|
2491
|
+
export { useTopBarSlot } from "./components/TopBarSlot"
|
|
2492
|
+
|
|
2493
|
+
// WorkspaceProvider + plugin model (unchanged)
|
|
2494
|
+
export { WorkspaceProvider } from "./WorkspaceProvider"
|
|
2495
|
+
export { definePlugin, definePanel, PluginError, … } from "./plugin"
|
|
2496
|
+
export { CatalogRegistry, useCommands, useActivePanels, useCatalogs, … } from "./plugin"
|
|
2497
|
+
```
|
|
2498
|
+
|
|
2499
|
+
### Phase 1.5 breakdown — 7 phase beads (A through G)
|
|
2500
|
+
|
|
2501
|
+
A and B can run in parallel. C depends on A. D, E, F can run in parallel after A+B+C. G depends on D+E+F.
|
|
2502
|
+
|
|
2503
|
+
```
|
|
2504
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
2505
|
+
│ Phase A — Decompose chat shells into front/chrome/ + front/ │
|
|
2506
|
+
│ bridge/ (v7.6 paths, ONE-GO move) │
|
|
2507
|
+
│ ─────────────────────────────────────────────────────────────── │
|
|
2508
|
+
│ - git mv components/chat/{SessionBrowser, ChatStagePlaceholder, │
|
|
2509
|
+
│ SurfaceShell, WorkbenchLeftPane}.tsx → front/chrome/ │
|
|
2510
|
+
│ {session-list, chat-stage-placeholder, artifact- │
|
|
2511
|
+
│ surface, workbench-left}/ │
|
|
2512
|
+
│ - Each chrome folder gains a definition.ts exporting a │
|
|
2513
|
+
│ PanelConfig (used by Phase B's coreWorkspacePanels) │
|
|
2514
|
+
│ - Create front/chrome/chat/ChatPanel.tsx (thin wrapper around │
|
|
2515
|
+
│ @boring/agent's ChatPanel + workspace integrations) + │
|
|
2516
|
+
│ definition.ts │
|
|
2517
|
+
│ - git mv components/chat/{uiCommandStream, │
|
|
2518
|
+
│ uiCommandDispatcher}.ts → front/bridge/ │
|
|
2519
|
+
│ - Update internal imports │
|
|
2520
|
+
│ - DON'T touch ChatCenteredShell.tsx or ChatTopBar.tsx yet — │
|
|
2521
|
+
│ they stay until Phase G/C respectively. │
|
|
2522
|
+
│ Bead: j9p7.24 │
|
|
2523
|
+
│ │
|
|
2524
|
+
│ Note: Step 0 already ran the workspace reorg into v7.6 layout, │
|
|
2525
|
+
│ so source paths use front/ prefix. ArtifactSurfacePane.tsx and │
|
|
2526
|
+
│ EmptyPane.tsx (loose under panes/ before Step 0) ALSO move to │
|
|
2527
|
+
│ front/chrome/{artifact-surface, EmptyPane.tsx} in this Phase. │
|
|
2528
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
2529
|
+
│
|
|
2530
|
+
▼
|
|
2531
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
2532
|
+
│ Phase B — Wire core panel registrations in WorkspaceProvider │
|
|
2533
|
+
│ ─────────────────────────────────────────────────────────────── │
|
|
2534
|
+
│ Parallelism note (codex P2): can START in parallel with Phase A │
|
|
2535
|
+
│ but cannot CLOSE before Phase A's definition.ts files exist. │
|
|
2536
|
+
│ │
|
|
2537
|
+
│ - Create front/registry/coreRegistrations.ts exporting │
|
|
2538
|
+
│ coreWorkspacePanels: PanelConfig[] aggregating the 4 core │
|
|
2539
|
+
│ chrome panel defs (chat, session-list, workbench-left, │
|
|
2540
|
+
│ artifact-surface). Imports each pane's definition.ts. │
|
|
2541
|
+
│ - front/WorkspaceProvider.tsx imports and registers them at │
|
|
2542
|
+
│ mount, BEFORE bootstrap() runs │
|
|
2543
|
+
│ - Test: render WorkspaceProvider with no plugins; assert the │
|
|
2544
|
+
│ panel registry has the 4 core ids │
|
|
2545
|
+
│ Bead: j9p7.25 (depends on j9p7.24 closing for the actual │
|
|
2546
|
+
│ definition.ts files to import) │
|
|
2547
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
2548
|
+
│
|
|
2549
|
+
▼
|
|
2550
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
2551
|
+
│ Phase C — Lift TopBar chrome + expose ResponsiveDockviewShell │
|
|
2552
|
+
│ ─────────────────────────────────────────────────────────────── │
|
|
2553
|
+
│ - git mv components/chat/ChatTopBar.tsx │
|
|
2554
|
+
│ → front/layout/TopBar.tsx │
|
|
2555
|
+
│ - Rename type ChatTopBarProps → TopBarProps │
|
|
2556
|
+
│ - Update barrel │
|
|
2557
|
+
│ - Export ResponsiveDockviewShell from package barrel with jsdoc │
|
|
2558
|
+
│ explaining Tier 2 │
|
|
2559
|
+
│ - Add §"Three-tier API" to packages/workspace/README.md │
|
|
2560
|
+
│ Bead: j9p7.26 │
|
|
2561
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
2562
|
+
│
|
|
2563
|
+
▼
|
|
2564
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
2565
|
+
│ Phase D — Migrate workspace-playground to ChatLayout (canary) │
|
|
2566
|
+
│ ─────────────────────────────────────────────────────────────── │
|
|
2567
|
+
│ - Rewrite apps/workspace-playground/src/App.tsx to use │
|
|
2568
|
+
│ <WorkspaceProvider> + <TopBar> + <ChatLayout> │
|
|
2569
|
+
│ - Confirm e2e tests (apps/workspace-playground/e2e/*.spec.ts) │
|
|
2570
|
+
│ still pass │
|
|
2571
|
+
│ - Document any gotchas for the next two app migrations │
|
|
2572
|
+
│ Bead: j9p7.27 │
|
|
2573
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
2574
|
+
│
|
|
2575
|
+
▼
|
|
2576
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
2577
|
+
│ Phase E — Migrate boring-macro-v2: ChatLayout + extract │
|
|
2578
|
+
│ macroPlugin (v7.6 paths, ONE-GO move) │
|
|
2579
|
+
│ ─────────────────────────────────────────────────────────────── │
|
|
2580
|
+
│ - Create apps/boring-macro-v2/src/plugin/{index.ts, server.ts, │
|
|
2581
|
+
│ front/, server/} per v7.6 layout. SPLIT entrypoints (codex │
|
|
2582
|
+
│ P1): index.ts exports macroClientPlugin (panels, sidebar, │
|
|
2583
|
+
│ catalogs); server.ts exports macroServerPlugin (agentTools, │
|
|
2584
|
+
│ routes adapter). │
|
|
2585
|
+
│ - front/: panes/ (chart-canvas, deck), sidebar/ (macro-series),│
|
|
2586
|
+
│ catalogs/ (seriesCatalog) │
|
|
2587
|
+
│ - server/: tools/ (execute_sql, macro_search, get_series_data, │
|
|
2588
|
+
│ persist_derived_series), routes/ (macroRoutes) │
|
|
2589
|
+
│ - Rewrite apps/boring-macro-v2/src/web/App.tsx: │
|
|
2590
|
+
│ <WorkspaceProvider plugins={[macroClientPlugin]}> │
|
|
2591
|
+
│ <ChatLayout nav="session-list" center="chat" sidebar= │
|
|
2592
|
+
│ "macro-series" surface="artifact-surface" /> │
|
|
2593
|
+
│ </WorkspaceProvider> │
|
|
2594
|
+
│ - Rewrite apps/boring-macro-v2/src/server/index.ts to use │
|
|
2595
|
+
│ createWorkspaceAgentApp({ plugins: [macroServerPlugin()] }) │
|
|
2596
|
+
│ + app.register(registerMacroRoutes, opts) │
|
|
2597
|
+
│ - macro's uiBridge.ts is ALREADY deleted in Step 1a (NOT here │
|
|
2598
|
+
│ — gemini P2 catch). │
|
|
2599
|
+
│ - Confirm boring-macro e2e tests pass │
|
|
2600
|
+
│ Beads: j9p7.18 (plugin module), j9p7.19 (app refactor), │
|
|
2601
|
+
│ j9p7.20 (e2e gate). Reframed under Phase E. │
|
|
2602
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
2603
|
+
│
|
|
2604
|
+
▼
|
|
2605
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
2606
|
+
│ Phase F — Migrate full-app to ChatLayout (or IdeLayout) │
|
|
2607
|
+
│ ─────────────────────────────────────────────────────────────── │
|
|
2608
|
+
│ - Rewrite apps/full-app/src/front/main.tsx to use the │
|
|
2609
|
+
│ appropriate declarative layout │
|
|
2610
|
+
│ - Confirm e2e tests pass │
|
|
2611
|
+
│ Bead: j9p7.28 │
|
|
2612
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
2613
|
+
│
|
|
2614
|
+
▼
|
|
2615
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
2616
|
+
│ Phase G — Delete legacy chat shell + context + finalize │
|
|
2617
|
+
│ (SHIPPED by j9p7.29) │
|
|
2618
|
+
│ ─────────────────────────────────────────────────────────────── │
|
|
2619
|
+
│ Once D + E + F merged: │
|
|
2620
|
+
│ - Deleted components/chat/ChatCenteredShell.tsx │
|
|
2621
|
+
│ - Deleted components/chat/context.ts │
|
|
2622
|
+
│ - Dropped related exports from src/index.ts │
|
|
2623
|
+
│ - Removed now-empty components/chat/ folder │
|
|
2624
|
+
│ - Updated WORKSPACE_V2_PLAN.md and active package docs │
|
|
2625
|
+
│ Bead: j9p7.29 │
|
|
2626
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
2627
|
+
```
|
|
2628
|
+
|
|
2629
|
+
### Phase 1.5 risks
|
|
2630
|
+
|
|
2631
|
+
- **Pixel drift on canary migration.** Tier 1's `<ChatLayout>` may render at slightly different pixel offsets than `<ChatCenteredShell>` (different padding stack, different transition timing on the sidebar). Plan: regenerate visual snapshots when migrating workspace-playground; eyeball the diff for genuine regressions vs cosmetic shifts.
|
|
2632
|
+
- **Plugin id collisions.** boring-macro's existing panels might collide with workspace's core panel ids. Audit during Phase E; rename macro panels to be namespaced (`macro:charts`, `macro:slides`).
|
|
2633
|
+
- **boring-macro custom shell logic.** boring-macro's current App.tsx has more glue than the playground (custom session handling, custom topbar variants). Phase E may surface that some glue should live in `macroPlugin` (commands), and some should stay app-level (host wiring + auth). Will need careful split.
|
|
2634
|
+
- **`uiCommandStream` / `uiCommandDispatcher` re-homing.** They're called from inside `ChatCenteredShell` today. After Phase A's move + WorkspaceProvider's mount-time wiring, they should be invoked from a `useEffect` inside `WorkspaceProvider` (or a dedicated hook). Confirm the lifecycle: the stream must start before the chat panel mounts.
|
|
2635
|
+
|
|
2636
|
+
### Phase 1.5 ship criteria
|
|
2637
|
+
|
|
2638
|
+
- All 7 phase beads (j9p7.24-29 + j9p7.18-20) closed
|
|
2639
|
+
- Three apps run on declarative layouts in production
|
|
2640
|
+
- `ChatCenteredShell` and `ChatShellContext` deleted from the tree
|
|
2641
|
+
- Public API exposes Tier 1 / Tier 2 / Tier 3 entries with jsdoc
|
|
2642
|
+
- README section in `packages/workspace/` describing the three-tier model with one snippet per tier
|
|
2643
|
+
|
|
2644
|
+
### Phase 1.5 supersedes / replaces
|
|
2645
|
+
|
|
2646
|
+
The earlier Phase 1 Step 5b ("ChatCenteredShell drops `data` + `extraPanels` props") and Step 5c ("WorkbenchLeftPane registry-driven") are **subsumed by Phase 1.5**:
|
|
2647
|
+
|
|
2648
|
+
- Step 5b's "drop legacy props" becomes "delete the whole shell" (Phase G).
|
|
2649
|
+
- Step 5c's "registry-driven WorkbenchLeftPane" becomes "WorkbenchLeftPane is just one of the core panels registered by WorkspaceProvider; tab strip is gone" (Phase A + B).
|
|
2650
|
+
|
|
2651
|
+
Beads j9p7.16 (Step 5b) and j9p7.17 (Step 5c) close as **scope-shifted to Phase 1.5** — see Phase A/B beads for the new location.
|
|
2652
|
+
|
|
2653
|
+
## Phase 2/3 (sketched, deferred)
|
|
2654
|
+
|
|
2655
|
+
**Phase 2 — distributable plugins:**
|
|
2656
|
+
- npm sub-path exports pattern (see §Distribution).
|
|
2657
|
+
- Extend pi loader: `extractTools` → `extractPlugin`. Files in
|
|
2658
|
+
`.pi/extensions/` and `node_modules/pi-plugin-*` can export the
|
|
2659
|
+
full `Plugin` shape. Legacy tools-only plugins keep working.
|
|
2660
|
+
- `GET /api/v1/plugins` discovery endpoint for system-prompt
|
|
2661
|
+
augmentation.
|
|
2662
|
+
- Workbench data tab gains catalog selector — picks any registered
|
|
2663
|
+
catalog.
|
|
2664
|
+
- Generic `search_catalog(id, query)` agent tool, auto-generated
|
|
2665
|
+
from registered catalogs.
|
|
2666
|
+
|
|
2667
|
+
**Phase 3 — agent-authored + dynamic:**
|
|
2668
|
+
- `create_plugin` / `update_plugin` agent tools writing to
|
|
2669
|
+
`.pi/extensions/.agent-authored/`.
|
|
2670
|
+
- Plugin hot-reload (requires Fastify-routes workaround).
|
|
2671
|
+
- If/when needed (the dep graph stops being trivial): re-introduce
|
|
2672
|
+
`dependsOn`, `onMount`, lifecycle.
|
|
2673
|
+
- Per-plugin sandboxing / capability flags.
|
|
2674
|
+
|
|
2675
|
+
## Test plan
|
|
2676
|
+
|
|
2677
|
+
- **Unit**
|
|
2678
|
+
- `definePlugin` validation: well-formed plugin passes;
|
|
2679
|
+
malformed contributions throw with field-level errors.
|
|
2680
|
+
- Bootstrap: defaults register before host plugins;
|
|
2681
|
+
excludeDefaults skips them; plugin contributions appear in
|
|
2682
|
+
per-type registries with correct pluginId; late-wins-on-id
|
|
2683
|
+
replaces and warns in dev.
|
|
2684
|
+
- File-pattern resolution: path-aware micromatch
|
|
2685
|
+
(`deck/**/*.md` matches `deck/labor/labor.md`); specificity
|
|
2686
|
+
ordering (deck/**/*.md beats **/*.md); same-specificity →
|
|
2687
|
+
app-beats-builtin → late-wins; explicit `surface.openPanel`
|
|
2688
|
+
bypasses.
|
|
2689
|
+
- `disableDefaultFileTools: true` removes file ops from
|
|
2690
|
+
standardCatalog; default keeps them.
|
|
2691
|
+
- RecentEntry: catalog-tagged entries render via the right
|
|
2692
|
+
catalog adapter; entries pointing at uninstalled catalogs are
|
|
2693
|
+
dropped; localStorage migration from string entries.
|
|
2694
|
+
- `validateTool` from `@boring/agent/shared/validateTool` works
|
|
2695
|
+
in a non-Node environment (no `node:*` imports leak).
|
|
2696
|
+
|
|
2697
|
+
- **Integration**
|
|
2698
|
+
- `<WorkspaceProvider plugins={[testPlugin]}>` →
|
|
2699
|
+
catalog/command/panel all reachable via their hooks.
|
|
2700
|
+
- `createWorkspaceAgentApp({ plugins: [testPlugin] })` exposes
|
|
2701
|
+
`agentTools` in agent catalog endpoint; substrate routes
|
|
2702
|
+
register.
|
|
2703
|
+
- Cmd palette renders catalogs from registered plugins;
|
|
2704
|
+
error-isolated per group.
|
|
2705
|
+
- `excludeDefaults: ['filesystem']` — Files tab not rendered,
|
|
2706
|
+
file ops not in tool catalog, file routes still served
|
|
2707
|
+
(substrate).
|
|
2708
|
+
- `excludeDefaults: ['dataCatalog']` removes the Data tab.
|
|
2709
|
+
- SurfaceShell fallback: opening a `.foo` file with no matching
|
|
2710
|
+
panel renders `EmptyFilePanel` (not a ghost tab).
|
|
2711
|
+
- allowedPanels gating: when set, only listed panel ids appear
|
|
2712
|
+
in the surface.
|
|
2713
|
+
|
|
2714
|
+
- **E2E**
|
|
2715
|
+
- **boring-macro-v2 existing e2e suite is the Step 6 acceptance
|
|
2716
|
+
gate** — all 10 specs (composer-border, deck, catalog-to-chart,
|
|
2717
|
+
catalog, split-no-clip, layout-persistence, chat-suggestions,
|
|
2718
|
+
chart-tabs, topbar, agent) MUST pass post-migration. The specs
|
|
2719
|
+
are behavior-level; only `App.tsx` and `server/index.ts`
|
|
2720
|
+
reference the deleted props.
|
|
2721
|
+
- Open `deck/labor/labor.md` → DeckPane (not generic
|
|
2722
|
+
MarkdownEditor) — confirms path-aware resolver.
|
|
2723
|
+
- Recent: open file from palette → close + reopen palette →
|
|
2724
|
+
file appears in Recent rendered as file path; run a command,
|
|
2725
|
+
Recent stays files-only.
|
|
2726
|
+
|
|
2727
|
+
## Acceptance
|
|
2728
|
+
|
|
2729
|
+
### Baseline protocol (v7.7 — worktree-based)
|
|
2730
|
+
|
|
2731
|
+
Establish a pre-migration baseline of the macro e2e suite using
|
|
2732
|
+
**`git worktree`**, NOT `git stash`. AGENTS.md forbids destructive
|
|
2733
|
+
ops on user state (multi-agent awareness rule); `git stash` against
|
|
2734
|
+
a working tree another agent might be editing is exactly that
|
|
2735
|
+
hazard.
|
|
2736
|
+
|
|
2737
|
+
```bash
|
|
2738
|
+
BASELINE_REF=<pre-migration-sha-or-tag>
|
|
2739
|
+
WORKTREE_DIR=../baseline-${BASELINE_REF:0:8}
|
|
2740
|
+
|
|
2741
|
+
git worktree add "$WORKTREE_DIR" "$BASELINE_REF"
|
|
2742
|
+
trap 'git worktree remove --force "$WORKTREE_DIR" 2>/dev/null' EXIT
|
|
2743
|
+
|
|
2744
|
+
pushd "$WORKTREE_DIR/apps/boring-macro-v2" >/dev/null
|
|
2745
|
+
pnpm install --frozen-lockfile
|
|
2746
|
+
pnpm exec playwright test --reporter=json > /tmp/macro-e2e-baseline.json
|
|
2747
|
+
popd >/dev/null
|
|
2748
|
+
```
|
|
2749
|
+
|
|
2750
|
+
Properties:
|
|
2751
|
+
|
|
2752
|
+
- **Idempotent.** Re-running creates a fresh worktree at the same
|
|
2753
|
+
sha; no working-tree state to recover.
|
|
2754
|
+
- **Parallelizable.** Multiple runs use distinct worktree dirs.
|
|
2755
|
+
- **Failure mode is benign.** Crash mid-run leaves
|
|
2756
|
+
`../baseline-<sha>/`; `git worktree list` finds it; `git worktree
|
|
2757
|
+
remove --force` cleans it. Never overwrites user work.
|
|
2758
|
+
|
|
2759
|
+
The post-migration acceptance suite then runs against `HEAD` and
|
|
2760
|
+
diffs results against `/tmp/macro-e2e-baseline.json`. Specs that
|
|
2761
|
+
were green pre-migration must stay green; specs that were red
|
|
2762
|
+
pre-migration are not this gate's job.
|
|
2763
|
+
|
|
2764
|
+
### Acceptance criteria
|
|
2765
|
+
|
|
2766
|
+
- `Plugin` contract (six fields) + `definePlugin` exported from
|
|
2767
|
+
`@boring/workspace`.
|
|
2768
|
+
- `CatalogRegistry` new; `CommandRegistry` + `PanelRegistry`
|
|
2769
|
+
retrofitted subscribable.
|
|
2770
|
+
- `<WorkspaceProvider plugins={[…]}>` and
|
|
2771
|
+
`createWorkspaceAgentApp({ plugins: [...] })` are the only
|
|
2772
|
+
registration APIs hosts use.
|
|
2773
|
+
- One default plugin: `filesystemPlugin` (UI-only — panels + catalog; no agentTools per v7.0+).
|
|
2774
|
+
Both auto-mount; both individually opt-out-able; opt-out actually
|
|
2775
|
+
removes UI surface (registry-driven workbench tabs +
|
|
2776
|
+
EmptyFilePanel fallback).
|
|
2777
|
+
- File-ops shared bundle in `@boring/agent` so standalone
|
|
2778
|
+
`createAgentApp` stays a real coding agent.
|
|
2779
|
+
- `validateTool` extracted to `@boring/agent/shared` so the client
|
|
2780
|
+
bundle stays node-clean.
|
|
2781
|
+
- Path-aware file-pattern resolver — `deck/**/*.md` works.
|
|
2782
|
+
- `<CommandPalette />` renders catalogs from plugins; old
|
|
2783
|
+
`fileSearchFn`/`onOpenFile` props removed; Recent is polymorphic
|
|
2784
|
+
(catalog-tagged entries) and the type-mix bug is fixed.
|
|
2785
|
+
- `<ChatCenteredShell />` registers its commands declaratively via
|
|
2786
|
+
an internal plugin; legacy `data` + `extraPanels` props deleted;
|
|
2787
|
+
`chatSuggestions` prop kept.
|
|
2788
|
+
- `boring-macro-v2` migrated per §"Concrete before/after": ~260
|
|
2789
|
+
LOC → ~36 LOC. Same user-visible behavior. macro routes
|
|
2790
|
+
registered with `{ clickhouse, deckRoot }` opts via host's
|
|
2791
|
+
`app.register(...)` — one line in server/index.ts.
|
|
2792
|
+
- Three breaking changes (`CommandPaletteProps`,
|
|
2793
|
+
`ChatCenteredShellProps.{data,extraPanels,withCommandPalette}`,
|
|
2794
|
+
`WorkbenchLeftPane` internal tab API) documented.
|
|
2795
|
+
- `package.json` exports `./events`.
|
|
2796
|
+
- All Phase 1 tests + macro e2e suite green.
|
|
2797
|
+
|
|
2798
|
+
## Open questions
|
|
2799
|
+
|
|
2800
|
+
1. **Plugin client/server file split — env guard or package.json
|
|
2801
|
+
exports?** Both work. Inline plugins use env guard; npm-published
|
|
2802
|
+
plugins (Phase 2) use package.json `exports`. Documented both.
|
|
2803
|
+
2. **Discovery endpoint authentication?** Same as other agent routes
|
|
2804
|
+
(session cookie). Phase 2 concern.
|
|
2805
|
+
3. **Hot-reload of Fastify routes (Phase 3 only).** Today
|
|
2806
|
+
`app.register(plugin)` is irreversible; this is one of the
|
|
2807
|
+
reasons routes aren't on the Plugin contract.
|
|
2808
|
+
|
|
2809
|
+
## Reference
|
|
2810
|
+
|
|
2811
|
+
- Existing pi plugin loader:
|
|
2812
|
+
`packages/agent/src/server/harness/pi-coding-agent/pluginLoader.ts`
|
|
2813
|
+
- Existing `WorkspaceProvider`:
|
|
2814
|
+
`packages/workspace/src/WorkspaceProvider.tsx`
|
|
2815
|
+
- Existing `<CommandPalette />`:
|
|
2816
|
+
`packages/workspace/src/components/CommandPalette.tsx`
|
|
2817
|
+
(Recent bug at lines 34, 59-60, 157, 230-232)
|
|
2818
|
+
- Existing tool implementations (verified names: `read`, `write`,
|
|
2819
|
+
`edit`, `find`, `grep`):
|
|
2820
|
+
`packages/agent/src/server/catalog/tools/{readTool,writeTool,editTool,findFilesTool,grepFilesTool}.ts`
|
|
2821
|
+
- Workspace tool renderers (keyed to current names):
|
|
2822
|
+
`packages/agent/src/ui-shadcn/workspaceToolRenderers.tsx:30`
|
|
2823
|
+
- Hardcoded workbench tabs (target of step 5c):
|
|
2824
|
+
`packages/workspace/src/components/chat/WorkbenchLeftPane.tsx:97,174,181`
|
|
2825
|
+
- ChatCenteredShell legacy props (target of step 5b):
|
|
2826
|
+
`packages/workspace/src/components/chat/ChatCenteredShell.tsx:45,116,637`
|
|
2827
|
+
+ `packages/workspace/src/components/chat/SurfaceShell.tsx:466`
|
|
2828
|
+
- File-pattern resolver to upgrade in step 2d:
|
|
2829
|
+
`packages/workspace/src/registry/PanelRegistry.ts:91` +
|
|
2830
|
+
`packages/workspace/src/components/chat/SurfaceShell.tsx:98`
|
|
2831
|
+
- ExplorerAdapter (catalog adapter contract):
|
|
2832
|
+
`packages/workspace/src/components/DataExplorer/types.ts`
|
|
2833
|
+
- ChatSuggestion + ChatEmptyState (kept as-is):
|
|
2834
|
+
`packages/agent/src/front-shadcn/ChatEmptyState.tsx`
|
|
2835
|
+
- Boring-macro-v2 host (the migration target — `src/web/`, not
|
|
2836
|
+
`src/front/`; uiBridge.ts confirmed at 9.3 KB):
|
|
2837
|
+
`/home/ubuntu/projects/boring-macro-v2/src/{server/index.ts,
|
|
2838
|
+
web/App.tsx, server/macroTools.ts, server/uiBridge.ts,
|
|
2839
|
+
server/macroRoutes.ts}`
|
|
2840
|
+
- Sibling plans:
|
|
2841
|
+
- `UNIFIED_EVENT_BUS.md` — bus model (already implemented)
|
|
2842
|
+
- `UI_BRIDGE_OWNERSHIP_REFACTOR.md` — step 1a of this plan
|
|
2843
|
+
- Superseded plans: `COMMAND_PALETTE_REGISTRY.md` (older); v2-v5.2
|
|
2844
|
+
of this file (in git history).
|
|
2845
|
+
|
|
2846
|
+
## Changelog v7.6 → v7.7 (round-7 governance + bead-level fixes)
|
|
2847
|
+
|
|
2848
|
+
Round-6 (codex bead-level review) returned 2 P0 governance questions, 7 P1s, 3 P2s. The user resolved both P0s; this changelog documents the integrated answers and the bead-level patches.
|
|
2849
|
+
|
|
2850
|
+
### P0 — ChatPanel resolution: dependency injection (NOT plugin)
|
|
2851
|
+
|
|
2852
|
+
The plan ambiguated whether chat enters the workspace as a plugin contribution, a wrapped value-import, or an injected slot. **Locked answer: injected slot.** `BootstrapOptions.chatPanel: ComponentType<ChatPanelProps>` is required; the consuming app value-imports `ChatPanel` from `@boring/agent` and passes it. The workspace holds a `import type { ChatPanelProps } from '@boring/agent'` only — Inv #7 stays grep-verifiable. Chat remains core chrome (workspace lays it out and sizes it); only the React component is injected.
|
|
2853
|
+
|
|
2854
|
+
Spec edits: §"Workspace orchestration — bootstrap" gains the BootstrapOptions surface + a "Chat as core chrome — DI shape, not plugin" subsection with a worked example. j9p7.24 description was rewritten to describe the DI shape (no "wrap inside workspace" wording survives) and adds an explicit invariant assertion: `grep -RE "from ['\"]@boring/agent['\"]" packages/workspace/src` excluding `import type` must return 0.
|
|
2855
|
+
|
|
2856
|
+
### P0 — Baseline protocol: worktree-based (NOT git stash)
|
|
2857
|
+
|
|
2858
|
+
Round-5 used `git stash` to establish a pre-migration baseline. That violates AGENTS.md "no destructive ops on user state" + multi-agent awareness rule. **Locked replacement: `git worktree add ../baseline-<sha> <ref>` → run macro e2e inside the worktree → capture artifacts → `git worktree remove`.** Idempotent, parallelizable, failure mode is a leftover dir (not lost work).
|
|
2859
|
+
|
|
2860
|
+
Spec edits: §"Acceptance" prepends a "Baseline protocol (v7.7 — worktree-based)" subsection with the canonical script. j9p7.20 (round-7 notes) replaces all `git stash` mentions with the worktree procedure. j9p7.20's dep on j9p7.19 was REMOVED via `br dep remove` — baseline runs BEFORE migration, so the graph edge that implied "baseline blocks on migration" was wrong.
|
|
2861
|
+
|
|
2862
|
+
### P1 fixes (bead-level)
|
|
2863
|
+
|
|
2864
|
+
1. **systemPrompt ordering circularity (j9p7.11 / j9p7.31)** — both beads now state the canonical sequence: bootstrap FIRST (computes `systemPromptAppend` + stages agentTools), then `createAgentApp({ systemPromptAppend, extraTools: [...uiTools, ...stagedAgentTools] })`. agentTools are staged into a list during bootstrap, not registered against a live registry.
|
|
2865
|
+
|
|
2866
|
+
2. **excludeDefaults semantics correction (j9p7.10)** — the round-1 test "LLM file ops not in agentTool list" was wrong. `excludeDefaults: ['filesystem']` removes filesystemPlugin's UI contributions (panels + catalog). It does NOT remove file tools — those are HARNESS substrate, never in the plugin's `agentTools` (which is undefined). Two switches, two layers: `excludeDefaults` for UI, `disableDefaultFileTools` for tools.
|
|
2867
|
+
|
|
2868
|
+
3. **Missing dep edges** — added `j9p7.12 → j9p7.25` (EmptyFilePanel registration), `j9p7.21 → j9p7.29` (release notes after final shell deletion), `j9p7.27 → j9p7.10` + `j9p7.27 → j9p7.9` (canary needs WorkspaceProvider+filesystemPlugin), `j9p7.19 → j9p7.27` (macro refactor canary-gated on playground migration).
|
|
2869
|
+
|
|
2870
|
+
4. **Macro path drift (j9p7.18 / j9p7.19)** — references to `apps/boring-macro-v2/src/web/App.tsx` corrected to `src/front/App.tsx`. The `web/` directory does not exist (verified `ls`); the React app lives in `front/`.
|
|
2871
|
+
|
|
2872
|
+
5. **j9p7.30 broad sed → explicit Edits** — round-5's `find ... -exec sed -i` cascade violated AGENTS "no broad rewrite scripts." Replaced with: pre/post-grep counts as sanity check + per-file Edit operations (READ → identify the specific vi.mock string → Edit). Deletion decisions for `src/store/`, `src/types/`, `src/panes/data-catalog/`, etc. were locked to a concrete table — no agent discretion at implementation time.
|
|
2873
|
+
|
|
2874
|
+
### P2 fixes
|
|
2875
|
+
|
|
2876
|
+
- **j9p7.9 / j9p7.22** — round-7 notes flag pre-Step-0 paths (`src/plugin/defaults/`, `src/plugin/PluginErrorBoundary.tsx`) and pin the post-Step-0 canonical destinations (`src/plugins/filesystemPlugin/`, `src/front/plugin/PluginErrorBoundary.tsx`).
|
|
2877
|
+
- **j9p7.32** — dist filename verified (`dist/workspace.d.ts`); no path correction needed; minor port note added (macro = 5174, full-app = check vite config).
|
|
2878
|
+
|
|
2879
|
+
### Status correction
|
|
2880
|
+
|
|
2881
|
+
- `j9p7.13` was flagged as open in round-6 notes; verified CLOSED. No action needed.
|
|
2882
|
+
|
|
2883
|
+
## Changelog v7.5 → v7.6 (round-5 review patches)
|
|
2884
|
+
|
|
2885
|
+
Codex r5 (4 P1 + 3 P2) + gemini r2 (1 P0 + 2 P1 + 3 P2) returned with sharp findings. **Convergence**: both flagged the sequencing tension (gemini P0 / codex P1 #4) and the per-plugin halves entrypoint shape (codex P1 #3 / gemini P1 empty-server). User decided all four design questions; this changelog captures the integration.
|
|
2886
|
+
|
|
2887
|
+
### P0 — Sequencing fixed (gemini)
|
|
2888
|
+
|
|
2889
|
+
The plan had Phase 1.5 Phase A doing BOTH the workspace reorg AND the chat-shell decomposition. That meant Phase 1 would build into the OLD flat layout, then Phase A would rip up and re-arrange. **Fix:** new **Step 0** before Phase 1 runs the mechanical workspace-reorg into v7.6 layout (front/+server/+shared/+plugins/) one-go. Phase A then ONLY decomposes the chat shell. **Meta-rule added:** when files move, they go directly to final v7.6 destinations — no intermediate placements.
|
|
2890
|
+
|
|
2891
|
+
### P1 — Plugin entrypoints split (codex)
|
|
2892
|
+
|
|
2893
|
+
v7.5's spec said "plugin index.ts exports CLIENT + server Plugins (same id)." Codex flagged: this contradicts the build invariant that client barrels never re-export server-only code. **Fix:** every plugin has TWO entrypoints — `index.ts` (client side) and `server.ts` (server side). Hosts import each from the appropriate environment. Same id ties them; different files ship them. macro example updated. filesystemPlugin (UI-only) has only `index.ts` (no server.ts needed; gemini P1 — "create only halves you need").
|
|
2894
|
+
|
|
2895
|
+
### P1 — Strict type-only imports for shared/plugin
|
|
2896
|
+
|
|
2897
|
+
Codex flagged: `shared/plugin/types.ts` referencing `ExplorerAdapter` from `components/DataExplorer/types` would leak DOM lib into the shared bundle. **Fix:** all cross-folder type references in `shared/plugin/` use `import type` (TypeScript erases at compile time). Plus `tsconfig.shared.json` (if it exists) excludes DOM lib. Plus `tsconfig.front.json` adds excludes for `src/plugins/**/server/**` to prevent server code being typechecked as front (codex P1 #1).
|
|
2898
|
+
|
|
2899
|
+
### P1 — Phase 1.5 step text path-updated
|
|
2900
|
+
|
|
2901
|
+
Codex flagged that Phase A still said `panes/<id>/`, Phase C said `layouts/TopBar.tsx`, Phase E said `src/macroPlugin.ts` — all pre-v7.5. **Fix:** Phase A → `front/chrome/<id>/`; Phase C → `front/layout/TopBar.tsx`; Phase E → `apps/.../src/plugin/{front,server}/` directly. Bead descriptions to be updated post-commit.
|
|
2902
|
+
|
|
2903
|
+
### P2 cleanup pack (applied)
|
|
2904
|
+
|
|
2905
|
+
1. **uiBridge dedup** — drop deletion from Phase E; keep in Step 1a only (gemini P2)
|
|
2906
|
+
2. **EmptyFilePanel relocation** — `panes/EmptyFilePanel/` → `front/chrome/empty-file-panel/` to kill the lone-resident `panes/` folder (gemini P2)
|
|
2907
|
+
3. **TL;DR scrub** — "two default plugins" wording in scope text → "one default plugin: filesystemPlugin" (codex P2)
|
|
2908
|
+
4. **A/B parallelism tightened** — "B can start in parallel but cannot close before A's definition.ts files exist" (codex P2)
|
|
2909
|
+
5. **pi-tools-migration catch-up** — note that `standardCatalog.ts` no longer exists; `createAgentApp.ts:91` already uses `buildHarnessAgentTools` + `buildFilesystemAgentTools` directly
|
|
2910
|
+
6. **tsconfig excludes** — already noted under P1 type imports
|
|
2911
|
+
|
|
2912
|
+
### Verdict from both reviewers
|
|
2913
|
+
|
|
2914
|
+
- **Gemini r2:** "implementable as-is, provided the Step 0 sequencing tweak."
|
|
2915
|
+
- **Codex r5:** "implementable after above cleanup, not quite as-is."
|
|
2916
|
+
|
|
2917
|
+
After v7.6 patches: both verdicts converge on **implementable as-is**. No reversals from earlier rounds. The architecture stands.
|
|
2918
|
+
|
|
2919
|
+
### Beads needing path updates
|
|
2920
|
+
|
|
2921
|
+
- **NEW j9p7.31** — Step 0 workspace reorg (mechanical)
|
|
2922
|
+
- **j9p7.9** (filesystemPlugin) — paths under `plugins/filesystemPlugin/{index.ts (no server.ts since UI-only), front/}`
|
|
2923
|
+
- **j9p7.18** (macro plugin module) — paths under `apps/boring-macro-v2/src/plugin/{index.ts, server.ts, front/, server/}`
|
|
2924
|
+
- **j9p7.24** (Phase A) — paths under `front/chrome/<id>/` and `front/bridge/`
|
|
2925
|
+
- **j9p7.25** (Phase B) — `front/registry/coreRegistrations.ts`
|
|
2926
|
+
- **j9p7.26** (Phase C) — `front/layout/TopBar.tsx`
|
|
2927
|
+
|
|
2928
|
+
## Changelog v7.4 → v7.5 (final structural framing)
|
|
2929
|
+
|
|
2930
|
+
User iterated through four refinements (2026-04-29) to lock the
|
|
2931
|
+
post-migration directory layout. Each round narrowed scope:
|
|
2932
|
+
|
|
2933
|
+
1. **"`panes/` should be workbench panes only"** — panes/ was conflating
|
|
2934
|
+
workbench-center (code-editor, markdown-editor) with sidebar tabs
|
|
2935
|
+
(file-tree, data-catalog) and chrome (ArtifactSurfacePane,
|
|
2936
|
+
EmptyPane). Strict role split.
|
|
2937
|
+
2. **"Plan structure to fit the plugin system; panes will belong to a plugin"** —
|
|
2938
|
+
plugin-contributed components live INSIDE the contributing
|
|
2939
|
+
plugin's directory, not at the workspace root. CodeEditor /
|
|
2940
|
+
MarkdownEditor / FileTree all migrate into `plugins/filesystemPlugin/`.
|
|
2941
|
+
3. **"core/ + layout/ + plugins/ as high-level framing"** — initial
|
|
2942
|
+
three-folder taxonomy.
|
|
2943
|
+
4. **"Should we distinguish front and back?"** — yes; matches
|
|
2944
|
+
`@boring/agent`'s `front/ + server/ + shared/` convention.
|
|
2945
|
+
5. **"plugins/ should contain actual plugins only"** — plugin model
|
|
2946
|
+
machinery moves out of `plugins/` into `front/plugin/`,
|
|
2947
|
+
`server/plugin/`, `shared/plugin/`. Singular `plugin/` for
|
|
2948
|
+
machinery; plural `plugins/` for instances.
|
|
2949
|
+
|
|
2950
|
+
### Final structure (locked)
|
|
2951
|
+
|
|
2952
|
+
```
|
|
2953
|
+
src/
|
|
2954
|
+
├── front/ workspace front + plugin front machinery
|
|
2955
|
+
│ ├── chrome/ shell parts (chat, sessions, surface, …)
|
|
2956
|
+
│ ├── components/ UI primitives
|
|
2957
|
+
│ ├── registry/ base registries
|
|
2958
|
+
│ ├── dock/, events/, bridge/, hooks/, layout/
|
|
2959
|
+
│ ├── panes/EmptyFilePanel/ workbench-center fallback (substrate)
|
|
2960
|
+
│ ├── plugin/ ← plugin model FRONT machinery
|
|
2961
|
+
│ │ ├── CatalogRegistry.ts
|
|
2962
|
+
│ │ ├── PluginErrorBoundary.tsx
|
|
2963
|
+
│ │ ├── PluginInspector.tsx
|
|
2964
|
+
│ │ └── hooks (usePlugins, useCatalogs, useCommands, useActivePanels)
|
|
2965
|
+
│ └── WorkspaceProvider.tsx
|
|
2966
|
+
│
|
|
2967
|
+
├── server/ workspace server + plugin server machinery
|
|
2968
|
+
│ ├── createWorkspaceAgentApp.ts, http/, ui-bridge/
|
|
2969
|
+
│ └── plugin/ ← plugin model SERVER machinery
|
|
2970
|
+
│
|
|
2971
|
+
├── shared/ types both halves need
|
|
2972
|
+
│ ├── ui-bridge.ts, events/
|
|
2973
|
+
│ └── plugin/ ← plugin model SHARED
|
|
2974
|
+
│ ├── types.ts Plugin, CatalogConfig
|
|
2975
|
+
│ ├── definePlugin.ts
|
|
2976
|
+
│ └── bootstrap.ts
|
|
2977
|
+
│
|
|
2978
|
+
└── plugins/ ← ACTUAL plugin instances (plural)
|
|
2979
|
+
└── filesystemPlugin/
|
|
2980
|
+
├── index.ts client + server Plugins (same id)
|
|
2981
|
+
├── front/{panes, sidebar, catalogs}
|
|
2982
|
+
└── server/ (currently empty for filesystemPlugin)
|
|
2983
|
+
```
|
|
2984
|
+
|
|
2985
|
+
### Naming convention (locked)
|
|
2986
|
+
|
|
2987
|
+
| Folder | Meaning |
|
|
2988
|
+
|---|---|
|
|
2989
|
+
| `front/` (root) | Workspace's own client-side code |
|
|
2990
|
+
| `server/` (root) | Workspace's own backend code |
|
|
2991
|
+
| `shared/` (root) | Workspace's own cross-process types |
|
|
2992
|
+
| `plugins/` (root, plural) | Directory of plugin instances |
|
|
2993
|
+
| `plugin/` (in front/, server/, shared/) | Plugin model machinery (singular) |
|
|
2994
|
+
| `<plugin>/front/` (per-plugin) | The plugin's client-side contributions |
|
|
2995
|
+
| `<plugin>/server/` (per-plugin) | The plugin's server-side contributions |
|
|
2996
|
+
|
|
2997
|
+
Apps mirror: each app contributes ONE plugin, lives in `apps/<host>/src/plugin/{front,server}/`. Multi-plugin apps would rename to `plugins/<id>/`.
|
|
2998
|
+
|
|
2999
|
+
### Why this is the right framing
|
|
3000
|
+
|
|
3001
|
+
1. **Matches `@boring/agent` convention** — `front/ + server/ + shared/` parity across packages.
|
|
3002
|
+
2. **Bundle boundary explicit** — tsup config can target `front/index.ts` and `server/index.ts` cleanly. Phase 2 npm sub-path exports (`./client`/`./server`) line up directly.
|
|
3003
|
+
3. **Plugin = self-contained unit** — a plugin owns BOTH its front and server halves in ONE directory. Distributed npm plugins ship the same shape.
|
|
3004
|
+
4. **No category mixing at any level** — workspace own code is in `front/server/shared`; plugins are in `plugins/`; plugin machinery is in singular `plugin/` sub-folders. Zero ambiguity for new contributors.
|
|
3005
|
+
5. **Plays well with build invariants** — codex round-2 caught a P0 where server-only modules were about to leak into client code. With the explicit split, the lint rule "front/ must not import from server/" is one path-prefix check.
|
|
3006
|
+
|
|
3007
|
+
### Bead impact
|
|
3008
|
+
|
|
3009
|
+
Mostly path-string updates; no semantic changes:
|
|
3010
|
+
|
|
3011
|
+
- **j9p7.9** (filesystemPlugin) — paths update from
|
|
3012
|
+
`packages/workspace/src/plugin/defaults/filesystemPlugin.ts` to
|
|
3013
|
+
`packages/workspace/src/plugins/filesystemPlugin/{index.ts,front/...,server/...}`
|
|
3014
|
+
- **j9p7.24** (Phase A — decompose chat shells) — paths update from
|
|
3015
|
+
`panes/<id>/` to `front/chrome/<id>/` for all chrome moves
|
|
3016
|
+
- **j9p7.18** (macro plugin module) — paths update from
|
|
3017
|
+
`apps/boring-macro-v2/src/plugin/{index,server}.ts` to
|
|
3018
|
+
`apps/boring-macro-v2/src/plugin/{index.ts,front/,server/}`
|
|
3019
|
+
|
|
3020
|
+
A new bead **j9p7.30** could capture the workspace front/server reorg
|
|
3021
|
+
itself if it's substantive enough to warrant separate tracking. For
|
|
3022
|
+
now, fold into Phase A (j9p7.24) since they're both restructuring
|
|
3023
|
+
the same directory.
|
|
3024
|
+
|
|
3025
|
+
## Changelog v7.3 → v7.4 (merger of declarative-layout-migration plan)
|
|
3026
|
+
|
|
3027
|
+
User decision (2026-04-29): **merge into one mega-plan + one epic**. The earlier `DECLARATIVE_LAYOUT_MIGRATION.md` (epic boring-ui-v2-zrby, empty) is now Phase 1.5 of this plan. Single acceptance gate.
|
|
3028
|
+
|
|
3029
|
+
### What changed
|
|
3030
|
+
|
|
3031
|
+
1. **New §"Phase 1.5: Consumer migration — decompose shells, declarative layouts, delete ChatCenteredShell"** added between §"Concrete before/after" and §"Phase 2/3."
|
|
3032
|
+
- Documents three-tier API (Tier 1 declarative pre-shaped layouts; Tier 2 custom LayoutConfig with stock chrome; Tier 3 raw primitives)
|
|
3033
|
+
- Substrate vs default-plugin vs app-plugin clarification (chat is core, not a plugin)
|
|
3034
|
+
- File-tree end state (panes/ as canonical home; components/chat/ disappears)
|
|
3035
|
+
- 7 phase beads breakdown (Phase A → G) with parallelism map
|
|
3036
|
+
- Risks + ship criteria
|
|
3037
|
+
|
|
3038
|
+
2. **j9p7.16 (Step 5b) and j9p7.17 (Step 5c) marked obsolete** — scope-shifted to Phase 1.5. Closing them with reason "subsumed by Phase 1.5 Phase A/B." Replacement beads created for each Phase.
|
|
3039
|
+
|
|
3040
|
+
3. **j9p7.18 / .19 / .20** (macro plugin module / app refactor / e2e gate) **reframed under Phase E**, not Step 6. Same work, different grouping.
|
|
3041
|
+
|
|
3042
|
+
4. **6 new beads created** for Phase 1.5 phases that don't have direct j9p7 equivalents:
|
|
3043
|
+
- j9p7.24 (Phase A — decompose shells)
|
|
3044
|
+
- j9p7.25 (Phase B — wire core panel registrations)
|
|
3045
|
+
- j9p7.26 (Phase C — lift TopBar chrome)
|
|
3046
|
+
- j9p7.27 (Phase D — migrate workspace-playground canary)
|
|
3047
|
+
- j9p7.28 (Phase F — migrate full-app)
|
|
3048
|
+
- j9p7.29 (Phase G — delete ChatCenteredShell + finalize)
|
|
3049
|
+
|
|
3050
|
+
5. **`DECLARATIVE_LAYOUT_MIGRATION.md` marked superseded** with a banner pointing to this plan. Content preserved in git history; the file remains as a tombstone redirect.
|
|
3051
|
+
|
|
3052
|
+
6. **Epic boring-ui-v2-zrby closed** as merged into j9p7. Its 0 children become moot.
|
|
3053
|
+
|
|
3054
|
+
### Why merge (vs keep two plans)
|
|
3055
|
+
|
|
3056
|
+
The two plans had ~37 + 24 = 61 cross-references between them (`ChatCenteredShell`, `macroPlugin`, `delete uiBridge`). Macro migration was duplicated: j9p7 had it as Step 6; zrby had it as Phase E. The risk of doing j9p7's "ChatCenteredShell drops props" THEN having zrby delete the shell entirely is real wasted work — the user named it as the deciding concern.
|
|
3057
|
+
|
|
3058
|
+
Merging gives:
|
|
3059
|
+
- One coherent narrative: "build plugin model machinery → migrate consumers to declarative composition"
|
|
3060
|
+
- One acceptance gate (boring-macro e2e suite passes after Phase E)
|
|
3061
|
+
- One epic to track end-to-end progress
|
|
3062
|
+
- ~270 lines of doc (Phase 1.5 section) vs ~270 lines duplicated across two files
|
|
3063
|
+
|
|
3064
|
+
### Net spec impact
|
|
3065
|
+
|
|
3066
|
+
- PLUGIN_MODEL.md grows by ~280 lines (Phase 1.5 + this changelog entry)
|
|
3067
|
+
- DECLARATIVE_LAYOUT_MIGRATION.md gains a SUPERSEDED banner; content preserved in history
|
|
3068
|
+
- j9p7 epic gains 6 new beads, closes 2 obsolete beads
|
|
3069
|
+
- zrby epic closes
|
|
3070
|
+
- Acceptance contract updated: j9p7 closes when Phase 1.5 ship criteria met (declarative apps + ChatCenteredShell deleted + Tier 1/2/3 public API)
|
|
3071
|
+
|
|
3072
|
+
## Changelog v7.2 → v7.3 (PanelConfig roles clarified)
|
|
3073
|
+
|
|
3074
|
+
User question (2026-04-29): "should we distinguish between pane and
|
|
3075
|
+
component that belong to the left sidebar... + we could imagine full
|
|
3076
|
+
workbench pages as well?"
|
|
3077
|
+
|
|
3078
|
+
Honest answer: yes, the three roles ARE conceptually distinct. v7.3
|
|
3079
|
+
chooses to clarify them in docs without splitting the type — Phase 1
|
|
3080
|
+
impl already works; up-front splitting adds API surface without
|
|
3081
|
+
catching real bugs. Discriminated-union refactor is a good Phase 2
|
|
3082
|
+
candidate, blocked until a second role-specific field appears.
|
|
3083
|
+
|
|
3084
|
+
### Spec additions
|
|
3085
|
+
|
|
3086
|
+
1. **New §"PanelConfig roles — three uses, one type"** inside
|
|
3087
|
+
§"Concrete contribution types". Names the three roles
|
|
3088
|
+
(sidebar tab / workbench pane / bottom dock); for each:
|
|
3089
|
+
required fields, fields-not-to-set, concrete examples from
|
|
3090
|
+
filesystemPlugin and macroPlugin.
|
|
3091
|
+
|
|
3092
|
+
2. **Reserved/future placements documented:** `'right-tab'`
|
|
3093
|
+
(symmetric to left-tab; no Phase 1 consumer); `'left'` /
|
|
3094
|
+
`'right'` (legacy non-tabbed; Phase 1 plugins don't use).
|
|
3095
|
+
|
|
3096
|
+
3. **`Plugin.pages?: PageConfig[]` sketched as future contribution
|
|
3097
|
+
type** for full-viewport views (Settings, Reports, Onboarding
|
|
3098
|
+
flow). Out of scope for Phase 1; conceptually similar to VS
|
|
3099
|
+
Code's `viewsContainers`. Documented but not in the contract.
|
|
3100
|
+
|
|
3101
|
+
### Decision: defer the discriminated-union split
|
|
3102
|
+
|
|
3103
|
+
The split becomes worth it when:
|
|
3104
|
+
- A second role-specific field appears (e.g., sidebar tabs gain
|
|
3105
|
+
`tabOrder`, bottom docks gain `defaultHeight`)
|
|
3106
|
+
- Plugin authors hit type-confusion bugs in practice
|
|
3107
|
+
|
|
3108
|
+
Until then: one type with `placement` runtime-disambiguating is
|
|
3109
|
+
cheaper.
|
|
3110
|
+
|
|
3111
|
+
### `/registry` directory clarification (also v7.3)
|
|
3112
|
+
|
|
3113
|
+
User question: "will `packages/workspace/src/registry/` disappear
|
|
3114
|
+
after Phase 1?"
|
|
3115
|
+
|
|
3116
|
+
Answer: no — it stays. It holds the BASE PanelRegistry +
|
|
3117
|
+
CommandRegistry + RegistryProvider that the plugin model
|
|
3118
|
+
(`/plugin/`) FANS INTO via `bootstrap()`. The split is logical:
|
|
3119
|
+
`/registry` is "primitives any host could use directly without
|
|
3120
|
+
plugins"; `/plugin` is "the unified contribution layer." Active
|
|
3121
|
+
consumers of `/registry` outside the plugin path:
|
|
3122
|
+
WorkspaceProvider, DockviewShell, ChatCenteredShell, layout
|
|
3123
|
+
presets, ArtifactSurfacePane tests. Tearing down would break them
|
|
3124
|
+
without payoff.
|
|
3125
|
+
|
|
3126
|
+
### Net impact
|
|
3127
|
+
|
|
3128
|
+
- Spec: +130 lines (new role-clarification subsection + future
|
|
3129
|
+
`pages` sketch + this changelog entry).
|
|
3130
|
+
- Contract: unchanged — same 7 fields (id, label, systemPrompt,
|
|
3131
|
+
panels, commands, catalogs, agentTools).
|
|
3132
|
+
- Beads: unchanged. Bead descriptions can keep using
|
|
3133
|
+
`placement: 'left-tab'` / `'center'` etc. as today.
|
|
3134
|
+
|
|
3135
|
+
## Changelog v7.1 → v7.2 (planning-workflow review pass)
|
|
3136
|
+
|
|
3137
|
+
After 4 codex rounds + 1 gemini round + ultrathink self-audit, ran
|
|
3138
|
+
the GPT-Pro-style "review and propose your best revisions" pass.
|
|
3139
|
+
User selected the wholeheartedly-recommended subset (5 of 8
|
|
3140
|
+
proposals) plus the cross-plan alignment.
|
|
3141
|
+
|
|
3142
|
+
### Robustness additions (selected)
|
|
3143
|
+
|
|
3144
|
+
**1. Per-plugin React error boundaries.** Every plugin contribution
|
|
3145
|
+
that renders React (panels, catalog row renderers, chat-suggestion
|
|
3146
|
+
cards) is wrapped in `<PluginErrorBoundary pluginId>`. A buggy
|
|
3147
|
+
plugin can no longer crash the workspace shell. Dropped errors
|
|
3148
|
+
push into `WorkspaceContext.errors[]` for visibility via
|
|
3149
|
+
`<PluginInspector />`. Implementation sites: PanelHost, CatalogResults,
|
|
3150
|
+
chat-suggestion card list. ~50 LOC. New bead: j9p7.22.
|
|
3151
|
+
|
|
3152
|
+
**2. CatalogRegistry search debouncing + AbortSignal propagation.**
|
|
3153
|
+
New hook: `useDebouncedCatalogSearch(query, opts?)` debounces 150ms,
|
|
3154
|
+
aborts in-flight searches per keystroke via the AbortSignal that
|
|
3155
|
+
ExplorerAdapter.search ALREADY accepts (verified
|
|
3156
|
+
`packages/workspace/src/components/DataExplorer/types.ts:46-55` —
|
|
3157
|
+
no contract change needed; just consumer-side wiring). Per-catalog
|
|
3158
|
+
errors isolated via try/catch. Updates bead j9p7.13.
|
|
3159
|
+
|
|
3160
|
+
### Compelling-feature addition (selected)
|
|
3161
|
+
|
|
3162
|
+
**3. `Plugin.systemPrompt?: string`.** Optional field; bootstrap
|
|
3163
|
+
concatenates across registered plugins (in registration order),
|
|
3164
|
+
prepends to the harness's base system prompt. Lets plugin authors
|
|
3165
|
+
own their domain framing for the LLM ("you have a FRED database;
|
|
3166
|
+
use macro_* tools for X"). Concrete LLM-quality improvement;
|
|
3167
|
+
reduces host-author burden. Updates beads j9p7.4 (contract +
|
|
3168
|
+
validation), j9p7.7 (bootstrap concat), j9p7.18 (macro plugin sets).
|
|
3169
|
+
|
|
3170
|
+
### DX additions (selected)
|
|
3171
|
+
|
|
3172
|
+
**4. `<PluginInspector />` PROMOTED to Phase 1.** Was cut in v6
|
|
3173
|
+
("console.log is enough"). Reversal justified: plugin authors hit
|
|
3174
|
+
"why didn't my command appear?" daily; visual check beats
|
|
3175
|
+
spelunking React DevTools. ~50 LOC, zero production cost
|
|
3176
|
+
(import.meta.env.DEV gated). New bead: j9p7.23.
|
|
3177
|
+
|
|
3178
|
+
**5. New §"Decisions log".** Single-table summary of locked
|
|
3179
|
+
architectural decisions with one-line rationale + pointer to the
|
|
3180
|
+
changelog entry. Sits near the top so fresh-eyes reviewers don't
|
|
3181
|
+
re-litigate from first principles. ~30 lines docs.
|
|
3182
|
+
|
|
3183
|
+
**6. New §"Plugin patterns: cross-plugin communication".** Three
|
|
3184
|
+
canonical patterns — shared registry / event bus / late-wins
|
|
3185
|
+
override — each with a worked code example + anti-patterns table.
|
|
3186
|
+
~60 lines docs. Plugin authors get templates instead of
|
|
3187
|
+
reinventing.
|
|
3188
|
+
|
|
3189
|
+
### NOT selected from the proposal set
|
|
3190
|
+
|
|
3191
|
+
- **`Plugin.contractVersion?: number`** (Revision 4) — defensive
|
|
3192
|
+
forward-compat insurance for Phase 2 distribution. User chose to
|
|
3193
|
+
skip; revisit when Phase 2 lands.
|
|
3194
|
+
|
|
3195
|
+
### Cross-plan alignment
|
|
3196
|
+
|
|
3197
|
+
**7. pi-tools-migration.md aligned to v7.x.** v6.3-aligned text in
|
|
3198
|
+
4 places (lines 265, 294, 323, 886) updated to reflect v7.0+'s
|
|
3199
|
+
harness-owns-tools framing. The two plans now read consistently.
|
|
3200
|
+
|
|
3201
|
+
### Net impact
|
|
3202
|
+
|
|
3203
|
+
- New beads: j9p7.22 (error boundaries) + j9p7.23 (PluginInspector)
|
|
3204
|
+
- Updated beads: j9p7.4 (systemPrompt validation), j9p7.7 (bootstrap
|
|
3205
|
+
concat), j9p7.13 (debounced search), j9p7.18 (macro systemPrompt)
|
|
3206
|
+
- Spec additions: ~250 lines (decisions log + patterns + 3 new
|
|
3207
|
+
sections + v7.2 changelog)
|
|
3208
|
+
- Same boring-macro acceptance test; LOC accounting unchanged
|
|
3209
|
+
- Plugin contract grows from 6 fields to 7 (added optional
|
|
3210
|
+
systemPrompt)
|
|
3211
|
+
|
|
3212
|
+
## Changelog v7.0 → v7.1 (codex round-4 cleanup)
|
|
3213
|
+
|
|
3214
|
+
Codex round-4 verdict: **v7.0 concept is implementable** (no P0). But
|
|
3215
|
+
the v7.0 patch missed four spots where v6.3 text remained in active
|
|
3216
|
+
spec sections. Plus one missing policy + one cross-package
|
|
3217
|
+
implementation gap.
|
|
3218
|
+
|
|
3219
|
+
### Patches
|
|
3220
|
+
|
|
3221
|
+
1. **§"Concrete filesystemPlugin source"** (line ~474) — was still
|
|
3222
|
+
`import { filesystemAgentTools }` + `agentTools:
|
|
3223
|
+
filesystemAgentTools`. Fixed: import dropped, `agentTools` field
|
|
3224
|
+
gone, comment points to the harness-owns-tools section.
|
|
3225
|
+
2. **Tool registration flow ASCII diagram** (line ~1180) — was
|
|
3226
|
+
showing v6.x dual-registration ("STANDALONE PATH" vs "WORKSPACE
|
|
3227
|
+
PATH" with `disableDefaultFileTools: true`). Fixed: single path,
|
|
3228
|
+
v7.1 narrative, `excludeDefaults` semantics narrowed.
|
|
3229
|
+
3. **Reorganization table** (line ~1336) — said file ops bundle is
|
|
3230
|
+
"imported by both `createAgentApp` AND `filesystemPlugin.agentTools`."
|
|
3231
|
+
Fixed: only `createAgentApp` imports; "Not imported by
|
|
3232
|
+
filesystemPlugin" called out.
|
|
3233
|
+
4. **Step 3 ASCII (in exact-path block)** — said `makeFilesystemPlugin
|
|
3234
|
+
(deps)` contributes agentTools. Fixed: plain const, UI-only,
|
|
3235
|
+
pi-tools-migration owns the tools.
|
|
3236
|
+
|
|
3237
|
+
### Additions
|
|
3238
|
+
|
|
3239
|
+
5. **§"Reserved tool names"** (codex round-4 P2) — explicit policy:
|
|
3240
|
+
`bash` / `executeIsolatedCode` / `read` / `write` / `edit` /
|
|
3241
|
+
`find` / `grep` / `ls` are HARNESS substrate. Plugin-contributed
|
|
3242
|
+
`agentTools` should use a domain prefix (`macro_search`,
|
|
3243
|
+
`docs_lookup`). Dev-mode warn on collision; no hard reject
|
|
3244
|
+
(override is sometimes intended). Existing
|
|
3245
|
+
`mergeTools.ts:30`'s late-wins-on-name behavior is documented as
|
|
3246
|
+
the legacy semantic for the pi loader path.
|
|
3247
|
+
6. **Known-gaps register entry** — `registerAgentRoutes` lacks
|
|
3248
|
+
`disableDefaultFileTools` (codex round-4 P1). Live verified:
|
|
3249
|
+
`createAgentApp` has the flag but `registerAgentRoutes` doesn't.
|
|
3250
|
+
The "harness opt-out" promise only holds for the `createAgentApp`
|
|
3251
|
+
path. **Out of scope for Phase 1 macro acceptance** (macro uses
|
|
3252
|
+
`createWorkspaceAgentApp` → `createAgentApp`, not
|
|
3253
|
+
`registerAgentRoutes`). Tracked as a known-gap; promote to a
|
|
3254
|
+
bead if a `registerAgentRoutes`-using host needs the opt-out.
|
|
3255
|
+
|
|
3256
|
+
### Cross-plan note (NOT patched in this commit)
|
|
3257
|
+
|
|
3258
|
+
`packages/agent/docs/plans/pi-tools-migration.md` (the sibling plan
|
|
3259
|
+
that ships first) still has v6.3-aligned text in three places:
|
|
3260
|
+
- Line 265: file tools called from BOTH createAgentApp AND filesystemPlugin
|
|
3261
|
+
- Line 294: filesystemPlugin becomes a factory with agentTools
|
|
3262
|
+
- Lines 323, 886: `excludeDefaults: ['filesystem']` removes file tools
|
|
3263
|
+
|
|
3264
|
+
Live code (`createAgentApp.ts:91` + `createWorkspaceAgentApp.ts:50`)
|
|
3265
|
+
matches v7.x, NOT pi-tools-migration's text. The sibling plan
|
|
3266
|
+
needs an alignment pass — flagged for the user (separate plan
|
|
3267
|
+
ownership).
|
|
3268
|
+
|
|
3269
|
+
### Codex round-4 verdict on macro acceptance
|
|
3270
|
+
|
|
3271
|
+
Macro 260→46 still holds. v7 automatic file tools don't add macro
|
|
3272
|
+
glue; macro domain tools still fit `Plugin.agentTools`. Current
|
|
3273
|
+
j9p7 beads are v7-aligned (verified post f2d73bc). Old uhwx
|
|
3274
|
+
(pi-tools-migration) docs/beads are stale relative to v7 — track
|
|
3275
|
+
separately.
|
|
3276
|
+
|
|
3277
|
+
## Changelog v6.3 → v7.0 (separation of concerns: harness owns tools)
|
|
3278
|
+
|
|
3279
|
+
User insight (2026-04-28): "fs plugin should not expose tools —
|
|
3280
|
+
those tools belong to the harness." Sharp framing. Adopted.
|
|
3281
|
+
|
|
3282
|
+
### What changes
|
|
3283
|
+
|
|
3284
|
+
The dual-registration arrangement (filesystemPlugin + standalone
|
|
3285
|
+
agent share the same tool factory) was conflating two concerns:
|
|
3286
|
+
"should the agent have file tools?" (harness config) and "should
|
|
3287
|
+
the UI show a file tree?" (plugin config). Two separate switches,
|
|
3288
|
+
two separate layers. v7.0 untangles them.
|
|
3289
|
+
|
|
3290
|
+
- **`filesystemPlugin` becomes UI-only.** No `agentTools` field.
|
|
3291
|
+
Just panels (FileTree left-tab, CodeEditor center, MarkdownEditor
|
|
3292
|
+
center) + a Files catalog (cmd palette). Plain module-scope
|
|
3293
|
+
const — no `(deps)` factory needed.
|
|
3294
|
+
- **Substrate file tools live with the harness.** Per
|
|
3295
|
+
pi-tools-migration: `buildFilesystemAgentTools(bundle)` returns
|
|
3296
|
+
`[read, write, edit, find, grep, ls]`; `buildHarnessAgentTools`
|
|
3297
|
+
returns `[bash, executeIsolatedCode]`. Both registered by
|
|
3298
|
+
`createAgentApp` directly. Always-on for the harness path.
|
|
3299
|
+
- **`disableDefaultFileTools: true`** (on `createAgentApp`) is the
|
|
3300
|
+
only switch that removes file tools entirely. Use case:
|
|
3301
|
+
sandboxed deployment / no-fs agent.
|
|
3302
|
+
- **`excludeDefaults: ['filesystem']`** removes only the UI (Files
|
|
3303
|
+
tab, code/markdown editor auto-routing). The LLM still has
|
|
3304
|
+
`read`/`write`/etc. — they're substrate, not plugin
|
|
3305
|
+
contributions. Honest narrower promise.
|
|
3306
|
+
- **`createWorkspaceAgentApp` no longer passes
|
|
3307
|
+
`disableDefaultFileTools: true`** to the underlying
|
|
3308
|
+
`createAgentApp`. Plain wrap. No coordination dance.
|
|
3309
|
+
- **`Plugin.agentTools`** survives — but only for **domain tools**
|
|
3310
|
+
like macro's `execute_sql`/`macro_search`/`get_series_data`.
|
|
3311
|
+
Those depend on app-specific runtime state (ClickHouse client)
|
|
3312
|
+
the host owns; they belong on the plugin contract. Substrate
|
|
3313
|
+
tools don't.
|
|
3314
|
+
|
|
3315
|
+
### Custom (non-pi) filesystem tools — supported
|
|
3316
|
+
|
|
3317
|
+
`buildFilesystemAgentTools(bundle)` is **not** restricted to pi
|
|
3318
|
+
factories. It can return pi tools + project-specific filesystem
|
|
3319
|
+
tools that pi doesn't ship (e.g., `watch_files`, `stat`,
|
|
3320
|
+
`git_status`, `multi_edit`). These are substrate alongside pi's
|
|
3321
|
+
defaults — same registration path, same lifecycle. They live in
|
|
3322
|
+
`@boring/agent/server/tools/filesystem/`, NOT in
|
|
3323
|
+
`filesystemPlugin`. Author wraps as `AgentTool`; bundle factory
|
|
3324
|
+
composes them into the array.
|
|
3325
|
+
|
|
3326
|
+
This was an explicit user clarification: "we may add fs tools that
|
|
3327
|
+
are not the default pi ones."
|
|
3328
|
+
|
|
3329
|
+
### What this removes from the spec
|
|
3330
|
+
|
|
3331
|
+
- The whole §"File ops: shared bundle, dual registration path"
|
|
3332
|
+
walkthrough — replaced by §"Tools belong with the harness, not
|
|
3333
|
+
the plugin" + §"Custom (non-pi) filesystem tools".
|
|
3334
|
+
- The `(deps)` argument on `makeFilesystemPlugin` — plugin is now
|
|
3335
|
+
a plain module-scope const.
|
|
3336
|
+
- The `disableDefaultFileTools: true` indirection from
|
|
3337
|
+
`createWorkspaceAgentApp` — plain wrap.
|
|
3338
|
+
- ~50 lines of spec total.
|
|
3339
|
+
|
|
3340
|
+
### Acceptance test impact
|
|
3341
|
+
|
|
3342
|
+
boring-macro-v2 migration LOC accounting unchanged (-86%). What
|
|
3343
|
+
changes is that macro's agentTools come ONLY from
|
|
3344
|
+
`makeMacroServerPlugin` (which is right — they're domain tools).
|
|
3345
|
+
File tools come from the harness automatically. No plumbing
|
|
3346
|
+
difference for macro's host code.
|
|
3347
|
+
|
|
3348
|
+
### Bead impact
|
|
3349
|
+
|
|
3350
|
+
- **B2** (file ops bundle extraction) → reframed as "extract
|
|
3351
|
+
pi-factory wiring per pi-tools-migration; bundle includes pi
|
|
3352
|
+
tools + any custom additions." No dual-registration story.
|
|
3353
|
+
- **B9** (filesystemPlugin) → becomes a tiny bead: plain const
|
|
3354
|
+
with panels + catalog, no agentTools. ~10 LOC of plugin code.
|
|
3355
|
+
- **B11** (createWorkspaceAgentApp) → no
|
|
3356
|
+
`disableDefaultFileTools: true` wiring; just wraps
|
|
3357
|
+
`createAgentApp` and runs bootstrap.
|
|
3358
|
+
|
|
3359
|
+
All three bead descriptions updated to match v7.0 framing.
|
|
3360
|
+
|
|
3361
|
+
## Changelog v6.2 → v6.3 (gemini fresh-eyes review patches)
|
|
3362
|
+
|
|
3363
|
+
Gemini did a fresh-eyes review (Gemini hadn't reviewed v6.x; codex
|
|
3364
|
+
ran rounds 2 and 3). Surfaced 1 P0 + 4 P1 + 1 P2 — all real, none
|
|
3365
|
+
duplicating codex. Quality of the catch on the P0 was particularly
|
|
3366
|
+
good: it noticed an actual closure-over-React-state bug in v6.2's
|
|
3367
|
+
`ChatCenteredShell` migration that codex round-3 missed.
|
|
3368
|
+
|
|
3369
|
+
**P0 — Static "internal chat-shell plugin" can't access
|
|
3370
|
+
`ChatCenteredShell` local state.** v6.2 said static commands
|
|
3371
|
+
(`toggleSessions`/`toggleWorkbench`/`newChat`) move into a
|
|
3372
|
+
module-scope plugin while only per-session commands stay
|
|
3373
|
+
imperative. But `toggleDrawer` and `toggleSurface` are closures
|
|
3374
|
+
over `useState` *inside* the React component
|
|
3375
|
+
(`ChatCenteredShell.tsx:222-226`). A module-scope plugin can't
|
|
3376
|
+
reach that state, and bridging via events would just shift the
|
|
3377
|
+
closure problem to the event handler.
|
|
3378
|
+
|
|
3379
|
+
**Fix:** drop the "internal chat-shell plugin" idea entirely. ALL
|
|
3380
|
+
ChatCenteredShell-internal commands stay as imperative
|
|
3381
|
+
`useEffect`+`registerCommand` calls. The registry's subscribe
|
|
3382
|
+
retrofit (Step 2c) propagates them to an open palette. The plugin
|
|
3383
|
+
model is for module-stable contributions; component-instance
|
|
3384
|
+
commands belong inside the component. Honest.
|
|
3385
|
+
|
|
3386
|
+
**P1.1 — Removing commands from Recent is a UX regression.** v6.2
|
|
3387
|
+
said "Recent is catalog-only — commands don't appear in Recent."
|
|
3388
|
+
Every mature command palette (VS Code, Raycast, Linear) shows
|
|
3389
|
+
recent commands; users rely on them for quick re-runs of frequent
|
|
3390
|
+
actions ("Toggle Theme," "Format JSON"). **Fix:** `RecentEntry`
|
|
3391
|
+
becomes a discriminated union — `{type: 'catalog', ...} | {type:
|
|
3392
|
+
'command', ...}`. Render both, drop orphans of either. Existing
|
|
3393
|
+
localStorage `cmd:foo` entries map to the command branch on
|
|
3394
|
+
migration; plain paths to the catalog branch.
|
|
3395
|
+
|
|
3396
|
+
**P1.2 — `rowSnapshot` localStorage round-trip can corrupt
|
|
3397
|
+
non-serializable values.** `JSON.stringify` silently strips
|
|
3398
|
+
`Date` / `Map` / functions / React nodes; restore would crash.
|
|
3399
|
+
**Fix:** added §"Recent serialization invariant" — `ExplorerRow`
|
|
3400
|
+
participating in Recent MUST be 100% JSON-serializable; adapters
|
|
3401
|
+
naturally holding non-serializable values (e.g., Dates) serialize
|
|
3402
|
+
at row construction time and re-hydrate in the renderer. No
|
|
3403
|
+
deserialize hook in Phase 1 (add as non-breaking optional if a
|
|
3404
|
+
real case appears).
|
|
3405
|
+
|
|
3406
|
+
**P1.3 — Cutting `onMount` strands non-React stateful adapters.**
|
|
3407
|
+
A catalog adapter that wants `events.on('file:moved')` for cache
|
|
3408
|
+
invalidation has nowhere to do it: module-scope subscription fires
|
|
3409
|
+
globally for all hosts; lazy-on-first-call leaks.
|
|
3410
|
+
**Fix:** documented the limitation in §"Known gaps — deferred to
|
|
3411
|
+
Phase 2+". `onMount` is the trigger condition for re-introduction.
|
|
3412
|
+
Phase 1 plugins are all React-component-based or factory-injected,
|
|
3413
|
+
so the assumption holds for now; honest about when it breaks.
|
|
3414
|
+
|
|
3415
|
+
**P2 — Dropping `dataSources`/`data` props forces boilerplate for
|
|
3416
|
+
simple hosts.** A host that just wants "a data tab with my
|
|
3417
|
+
adapter" had a one-liner; v6.2's "register your own left-tab
|
|
3418
|
+
panel" makes it ~10 lines. **Fix:** use
|
|
3419
|
+
`createDataCatalogPlugin` for standalone data catalog plugins, or
|
|
3420
|
+
`appendDataCatalogOutputs` when an app plugin needs to install
|
|
3421
|
+
the data catalog as part of a domain plugin. Macro uses the latter
|
|
3422
|
+
so chart/deck behavior stays in the macro app.
|
|
3423
|
+
|
|
3424
|
+
### Verdict on the simplification cuts (gemini)
|
|
3425
|
+
|
|
3426
|
+
> *"Cutting `routes`, `dependsOn`, `order`, and `chatSuggestions`
|
|
3427
|
+
> is highly defensible and correctly shifts HTTP infrastructure
|
|
3428
|
+
> and declarative configuration out of the pure-data plugin
|
|
3429
|
+
> envelope. The only cut that went too far is `onMount`."*
|
|
3430
|
+
|
|
3431
|
+
Codex (round-3): *"order/dependsOn/optionalDeps/version/routes/
|
|
3432
|
+
chatSuggestions/onMount are mostly defensible for macro."*
|
|
3433
|
+
|
|
3434
|
+
Both reviewers converge on `onMount` being the riskiest cut, and
|
|
3435
|
+
both agree it's defensible for Phase 1 specifically (no Phase 1
|
|
3436
|
+
plugin needs it). v6.3 documents the limitation with an explicit
|
|
3437
|
+
re-introduction trigger; if/when a stateful adapter appears,
|
|
3438
|
+
adding `onMount` is a non-breaking optional contract field. The
|
|
3439
|
+
cut holds for Phase 1; the sensitivity is acknowledged.
|
|
3440
|
+
|
|
3441
|
+
### Net impact (v6.2 → v6.3)
|
|
3442
|
+
|
|
3443
|
+
- ChatCenteredShell migration honest: ALL its commands stay
|
|
3444
|
+
imperative; no fictional "internal plugin" indirection.
|
|
3445
|
+
- `RecentEntry` discriminated union: catalogs + commands.
|
|
3446
|
+
- JSON-serializable invariant on `ExplorerRow` documented.
|
|
3447
|
+
- Known-gaps register adds the non-React-adapter lifecycle item
|
|
3448
|
+
with `onMount` re-introduction trigger.
|
|
3449
|
+
- Replaced the static data factory with the reusable data catalog plugin helpers.
|
|
3450
|
+
- Same boring-macro acceptance test; LOC accounting unchanged.
|
|
3451
|
+
|
|
3452
|
+
## Changelog v6.1 → v6.2 (round-3 codex review patches)
|
|
3453
|
+
|
|
3454
|
+
Round-3 codex review against v6.1 surfaced 1 P0 + 1 P1 + 2 P2 — all
|
|
3455
|
+
real, all verified against the live codebase. The cuts from v6 (no
|
|
3456
|
+
dependsOn, no onMount, no routes, no chatSuggestions on contract)
|
|
3457
|
+
verdict: defensible. Patches focus on Phase 1 semantics that were
|
|
3458
|
+
underspecified.
|
|
3459
|
+
|
|
3460
|
+
**P0 — `filesystemAgentTools: AgentTool[]` static shape doesn't
|
|
3461
|
+
match runtime reality.** The current `standardCatalog.ts:84`
|
|
3462
|
+
constructs file tools from runtime deps (`workspace`, `sandbox`,
|
|
3463
|
+
`fileSearch`); each `createXxxTool(deps)` returns an `AgentTool`.
|
|
3464
|
+
The plan's claim that `filesystemAgentTools` is a static array
|
|
3465
|
+
can't be wired. **Fix:** the bundle is now a factory:
|
|
3466
|
+
`createFilesystemAgentTools(deps): AgentTool[]`. Both
|
|
3467
|
+
`createAgentApp` (default-on) and `filesystemPlugin` call it with
|
|
3468
|
+
their respective runtime bundles. `filesystemPlugin` itself
|
|
3469
|
+
becomes `makeFilesystemPlugin(deps)` (factory shape, like
|
|
3470
|
+
`makeMacroPlugin`) so it can be constructed with the runtime deps
|
|
3471
|
+
by `createWorkspaceAgentApp` rather than evaluated at module load.
|
|
3472
|
+
|
|
3473
|
+
**P1 — Workbench data tab semantics ambiguous after dropping
|
|
3474
|
+
`data` prop.** With `recentKind` cut and multiple registered
|
|
3475
|
+
catalogs (filesystem's Files + macro's Series), nothing in the
|
|
3476
|
+
spec said which one fills the generic Data tab. **Fix:** drop
|
|
3477
|
+
`dataCatalogPlugin` from defaults entirely. There is no generic
|
|
3478
|
+
Data tab; plugins that want a workbench data tab register their
|
|
3479
|
+
own `placement: 'left-tab'` panel (e.g., macro's `macroSeriesPanel`
|
|
3480
|
+
which internally renders DataExplorer with the macro adapter +
|
|
3481
|
+
`onActivate` → `surface.openPanel({ component: "chart-canvas",
|
|
3482
|
+
... })`). Cleaner, no precedence rule needed.
|
|
3483
|
+
|
|
3484
|
+
**P2 #1 — Macro example violated its own client/server build
|
|
3485
|
+
invariant.** v6.1's `apps/boring-macro-v2/src/plugin/index.ts`
|
|
3486
|
+
imported `macroAgentTools` from `../server/macroTools` — server
|
|
3487
|
+
code in client-facing index.ts. **Fix:** macro plugin splits into
|
|
3488
|
+
`makeMacroClientPlugin()` (panels/catalogs) in `plugin/index.ts`
|
|
3489
|
+
+ `makeMacroServerPlugin()` (agentTools) in `plugin/server.ts`,
|
|
3490
|
+
both `definePlugin({ id: "boring-macro", ... })` with the same id.
|
|
3491
|
+
`<WorkspaceProvider>` gets the client one; `createWorkspaceAgentApp`
|
|
3492
|
+
gets the server one. Same pattern as Phase 2 npm distribution
|
|
3493
|
+
(client/server sub-path exports).
|
|
3494
|
+
|
|
3495
|
+
**P2 #2 — ChatCenteredShell migration didn't address dynamic
|
|
3496
|
+
command registration.** The current code registers per-session
|
|
3497
|
+
quick-switch commands inside a `useEffect` that depends on
|
|
3498
|
+
`sessions` prop changing at runtime. A static plugin contribution
|
|
3499
|
+
can't represent that. **Fix:** spec now says ONLY the static
|
|
3500
|
+
commands (toggleSessions/toggleWorkbench/newChat) move to the
|
|
3501
|
+
internal chat-shell plugin; per-session commands STAY as
|
|
3502
|
+
imperative `useCommandRegistry().registerCommand` calls inside
|
|
3503
|
+
ChatCenteredShell. Coexistence works because the registry's
|
|
3504
|
+
subscribe retrofit (Step 2c) handles late registrations.
|
|
3505
|
+
|
|
3506
|
+
### Verdict on the v6 simplification cuts
|
|
3507
|
+
|
|
3508
|
+
Codex round-3 explicitly: *"`order/dependsOn/optionalDeps/version/
|
|
3509
|
+
routes/chatSuggestions/onMount` are mostly defensible for macro.
|
|
3510
|
+
The simplification breaks only where Phase 1 semantics are
|
|
3511
|
+
underspecified (catalog selection) or where the spec's tool-bundle
|
|
3512
|
+
shape is incompatible with current runtime dependency injection."*
|
|
3513
|
+
|
|
3514
|
+
Both blockers are now patched in v6.2. None of the v6 cuts get
|
|
3515
|
+
rolled back.
|
|
3516
|
+
|
|
3517
|
+
### Net impact (v6.1 → v6.2)
|
|
3518
|
+
|
|
3519
|
+
- One default plugin removed (`dataCatalogPlugin`).
|
|
3520
|
+
- Two factory shapes formalized (`createFilesystemAgentTools(deps)`
|
|
3521
|
+
and `makeFilesystemPlugin(deps)`).
|
|
3522
|
+
- Macro example honors client/server split.
|
|
3523
|
+
- ChatCenteredShell migration spec acknowledges static-vs-dynamic
|
|
3524
|
+
command lifetime.
|
|
3525
|
+
- LOC accounting updates: 260 → 46 (-82%); slightly less reduction
|
|
3526
|
+
than v6.1's claimed 36 because the macro plugin is split into
|
|
3527
|
+
two files now.
|
|
3528
|
+
|
|
3529
|
+
## Changelog v6 → v6.1 (ultrathink self-audit)
|
|
3530
|
+
|
|
3531
|
+
Self-applied ultrathink review against v6 — the kind of pass an
|
|
3532
|
+
external reviewer would make. Seven findings, all small:
|
|
3533
|
+
|
|
3534
|
+
1. **`recentKind` on CatalogConfig was dead code.** Set but never
|
|
3535
|
+
read after the spec normalized to "drop orphans." Cut from the
|
|
3536
|
+
contract; cut from `RecentEntry`. Phase 2 "filter Recent by
|
|
3537
|
+
kind" UX can re-add as a non-breaking optional field.
|
|
3538
|
+
|
|
3539
|
+
2. **`definePlugin` validation was claimed but never enumerated.**
|
|
3540
|
+
New §"What `definePlugin` validates" lists the five categories
|
|
3541
|
+
of checks (id, panels, commands, catalogs, agentTools) so plugin
|
|
3542
|
+
authors can predict what fails.
|
|
3543
|
+
|
|
3544
|
+
3. **Plugin-level id collision policy was missing.** Contribution-
|
|
3545
|
+
level (panel id, command id) is late-wins-with-warn; that's
|
|
3546
|
+
composition. But two plugins sharing `Plugin.id` is identity
|
|
3547
|
+
confusion, not composition — should throw. Documented in new
|
|
3548
|
+
§"Plugin id collision policy."
|
|
3549
|
+
|
|
3550
|
+
4. **Build/bundle invariants were implicit.** `plugin.client.ts`
|
|
3551
|
+
and `plugin.server.ts` MUST NOT cross-import or the client
|
|
3552
|
+
bundle leaks `node:*` imports. Added §"Build/bundle invariants"
|
|
3553
|
+
listing three enforcement strategies (RSC directives, npm
|
|
3554
|
+
sub-path exports, conditional imports) — any one suffices.
|
|
3555
|
+
|
|
3556
|
+
5. **No testing guidance.** Plugin authors had to figure out
|
|
3557
|
+
testing patterns themselves. Added §"Plugin testability" with
|
|
3558
|
+
three concrete patterns: unit (assert contract shape),
|
|
3559
|
+
integration (`<WorkspaceProvider plugins={[testPlugin]}>` +
|
|
3560
|
+
`renderHook`), server (`createWorkspaceAgentApp` + Fastify
|
|
3561
|
+
`inject()`).
|
|
3562
|
+
|
|
3563
|
+
6. **No concrete `filesystemPlugin` source.** The plan was abstract
|
|
3564
|
+
about what the canonical default plugin looks like. Added the
|
|
3565
|
+
actual code so the plan is self-contained; it's the most-cited
|
|
3566
|
+
exemplar in the spec.
|
|
3567
|
+
|
|
3568
|
+
7. **Known-gaps register added.** §"Known gaps — deferred to Phase
|
|
3569
|
+
2+" makes 11 things explicit (hot-reload, build-time
|
|
3570
|
+
enforcement, plugin versioning, layout migrations, …) with a
|
|
3571
|
+
"when to revisit" column. Reviewers who want to flag gaps can
|
|
3572
|
+
check whether they're already on the deferral list before
|
|
3573
|
+
adding scope.
|
|
3574
|
+
|
|
3575
|
+
### Considered but not changed
|
|
3576
|
+
|
|
3577
|
+
- **`Plugin.label` removal** — considered cutting (defaults to
|
|
3578
|
+
`id`). Kept because `label?: string` is one optional line and
|
|
3579
|
+
it'll be visible in any future inspector / discovery UI.
|
|
3580
|
+
- **`agentTools` field on Plugin** — considered moving server-only
|
|
3581
|
+
contributions into a separate `ServerPlugin` shape. Rejected:
|
|
3582
|
+
same plugin id on client and server is the design intent (see
|
|
3583
|
+
§Distribution); separate shapes would bifurcate the contract
|
|
3584
|
+
for no real benefit.
|
|
3585
|
+
- **`definePlugin` immutability via `Object.freeze`** — considered.
|
|
3586
|
+
Rejected as over-engineering until accidental mutation is a
|
|
3587
|
+
real problem.
|
|
3588
|
+
|
|
3589
|
+
### Net impact (v6 → v6.1)
|
|
3590
|
+
|
|
3591
|
+
- One field cut (`recentKind`).
|
|
3592
|
+
- Five sections added (~100 lines): validation enumeration, id
|
|
3593
|
+
collision policy, build invariants, testability, concrete
|
|
3594
|
+
filesystemPlugin source, known-gaps register.
|
|
3595
|
+
- Same boring-macro acceptance test.
|
|
3596
|
+
- Same 6-field Plugin contract (now actually 5 mandatory fields +
|
|
3597
|
+
optional `label`).
|
|
3598
|
+
|
|
3599
|
+
## Changelog v5.2 → v6 (simplification pass)
|
|
3600
|
+
|
|
3601
|
+
After multiple review rounds we noticed v5.2 had grown defensible
|
|
3602
|
+
fields the way good plans do — every reviewer adds one ornament.
|
|
3603
|
+
v6 audits each field against "does Phase 1 actually need this?"
|
|
3604
|
+
and cuts everything that doesn't earn its keep.
|
|
3605
|
+
|
|
3606
|
+
### Cut from the contract
|
|
3607
|
+
|
|
3608
|
+
| Field | Why it's safe to cut |
|
|
3609
|
+
|---|---|
|
|
3610
|
+
| `Plugin.order: number` | Array order does the work. Defaults prepended → register first; host plugins after; late-wins-on-id for collisions. No numeric ordering footgun for plugin authors. |
|
|
3611
|
+
| `Plugin.dependsOn` | Phase 1 has exactly one declared dep (macro→filesystem). Add when the dep graph stops being trivial. The "fail at boot if missing" gate is replaced by either runtime degradation (dev notices in 30s) or an `if (!ctx.catalogs.find(...))` line in `onMount` — and we're cutting onMount too. |
|
|
3612
|
+
| `Plugin.optionalDeps` | Soft deps that warn if missing → just a `console.warn` with extra contract surface. Plugins null-check the registry. |
|
|
3613
|
+
| `Plugin.version` | Used by nothing in Phase 1. |
|
|
3614
|
+
| `Plugin.routes: RouteRegistration[]` | Routes are HTTP infrastructure, not registry contributions. Mixing them blurs identity AND lies about lifecycle (Fastify can't unregister). Hosts wire routes via standard `app.register(...)` — one line per plugin that has routes. |
|
|
3615
|
+
| `Plugin.chatSuggestions` | Empty-state UX caps at ~6 cards → aggregation is impossible; hosts have to curate. If hosts curate, the registry adds nothing. Stays as a `<ChatCenteredShell>` prop. |
|
|
3616
|
+
| `Plugin.onMount` + `Cleanup` types | Zero Phase 1 plugins use it. Event subscriptions are better handled via `useEvent` in panels or `events.on()` in routes. macro's clickhouse client is constructed by the host before the plugin is built (factory pattern). |
|
|
3617
|
+
| `RouteRegistration<TOpts>` type | Gone with `routes`. |
|
|
3618
|
+
| `PluginMountCtx` type | Gone with `onMount`. |
|
|
3619
|
+
| `WorkspaceSurface` interface | Gone with `PluginMountCtx`. Plugin-level imperative actions weren't needed; declarative contributions handle everything. |
|
|
3620
|
+
| `MaybePromise` / `Cleanup` types | Gone with `onMount`. |
|
|
3621
|
+
| `tabOrder?: number` | Registration order works. |
|
|
3622
|
+
| `paletteIcon`, `paletteLimit`, `renderRecentRow` | Reasonable defaults. Add when a plugin overrides. |
|
|
3623
|
+
| 5 error subclasses (`PluginValidationError`, `PluginRegistrationError`, `PluginMountError`, `PluginContributionError`, `id collision`) | One `PluginError { kind: '...' }`. |
|
|
3624
|
+
| `<PluginInspector />` | `console.log(registries)` works fine for Phase 1. |
|
|
3625
|
+
| Pre-declared lifecycle events on the bus (`plugin:registered` etc.) | "Events declared on demand" policy. No emitter or consumer in Phase 1; don't declare. |
|
|
3626
|
+
|
|
3627
|
+
### Sections compressed
|
|
3628
|
+
|
|
3629
|
+
- **Order semantics** — entire ~40-line section gone.
|
|
3630
|
+
- **Boot sequence** — collapses from a numbered async two-pass
|
|
3631
|
+
protocol with topo sort to a 6-line single-pass loop.
|
|
3632
|
+
- **Plugin composability — dependencies + file-pattern resolution**
|
|
3633
|
+
→ renamed §"File-pattern resolution + late-wins" (the
|
|
3634
|
+
dependency half disappears).
|
|
3635
|
+
- **Security stance** — collapsed to a one-line non-goal.
|
|
3636
|
+
- **Dev tools** — collapsed to a one-line note about dev-mode
|
|
3637
|
+
warnings.
|
|
3638
|
+
- **Factory pattern** — was a section; now a paragraph.
|
|
3639
|
+
|
|
3640
|
+
### Sections added
|
|
3641
|
+
|
|
3642
|
+
- **Why no chatSuggestions on the contract** — explicit
|
|
3643
|
+
aggregation-honest test.
|
|
3644
|
+
- **Where do routes go?** — substrate vs agent core vs plugin-specific.
|
|
3645
|
+
- **Distribution — Phase 2 sketch** — npm sub-path exports pattern;
|
|
3646
|
+
same shape as Express/Fastify/Vite.
|
|
3647
|
+
- **Relationship to pi-mono ecosystem** — what we adopt verbatim,
|
|
3648
|
+
what we add on top.
|
|
3649
|
+
|
|
3650
|
+
### Net impact
|
|
3651
|
+
|
|
3652
|
+
- Spec line count: ~50% smaller than v5.2.
|
|
3653
|
+
- Implementation surface: ~40% smaller (no lifecycle, no
|
|
3654
|
+
RouteRegistration, no WorkspaceSurface, simpler bootstrap).
|
|
3655
|
+
- Same boring-macro acceptance test.
|
|
3656
|
+
- Same `excludeDefaults` honesty.
|
|
3657
|
+
- Same path-aware resolver.
|
|
3658
|
+
- Same polymorphic Recent.
|
|
3659
|
+
- Same file-ops shared bundle.
|
|
3660
|
+
- Same validateTool extraction (P0 fix preserved).
|
|
3661
|
+
- Same events subpath export (P1 fix preserved).
|
|
3662
|
+
- Same SurfaceShell EmptyFilePanel fallback (P1 fix preserved).
|
|
3663
|
+
|
|
3664
|
+
### What we add back when (not if)
|
|
3665
|
+
|
|
3666
|
+
| Field | Add when |
|
|
3667
|
+
|---|---|
|
|
3668
|
+
| `dependsOn` | Phase 2: npm plugins from different authors → real dep graph emerges. |
|
|
3669
|
+
| `onMount` | First plugin that needs imperative setup not solved by panel-level `useEvent` or factory closure. |
|
|
3670
|
+
| `Plugin.version` + semver in deps | Phase 2 npm distribution. |
|
|
3671
|
+
| Ordering field | If/when registration order proves insufficient (no current evidence). |
|
|
3672
|
+
| `Plugin.routes` | If a Phase 3 hot-reload story makes route-as-plugin-data viable. |
|
|
3673
|
+
| `chatSuggestions` field | If an empty-state-aggregation use case appears that the host can't solve by curating. |
|
|
3674
|
+
| `<PluginInspector />` | When devs ask for it. |
|