@hachej/boring-workspace 0.1.23 → 0.1.26
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/dist/{FileTree-D8Rmj8Bo.js → FileTree-BZGu5Ap6.js} +114 -104
- package/dist/{MarkdownEditor-DKC4gNT4.js → MarkdownEditor-DshmttZM.js} +9 -9
- package/dist/{WorkspaceLoadingState-hKrnYCL3.js → WorkspaceLoadingState-DVCLcOQu.js} +167 -146
- package/dist/WorkspaceProvider-DQ-325Qs.js +6367 -0
- package/dist/app-front.d.ts +114 -2
- package/dist/app-front.js +787 -333
- package/dist/app-server.d.ts +10 -3
- package/dist/app-server.js +744 -579
- package/dist/createInMemoryBridge--ZFPAgXy.d.ts +161 -0
- package/dist/events.d.ts +3 -0
- package/dist/{manifest-CyNNdfYz.d.ts → manifest-C2vVgH_e.d.ts} +2 -0
- package/dist/plugin.d.ts +8 -3
- package/dist/plugin.js +3 -2
- package/dist/server.d.ts +50 -70
- package/dist/server.js +192 -44
- package/dist/shared.d.ts +2 -2
- package/dist/{surface-COYagY2m.d.ts → surface-CEEkd81D.d.ts} +1 -0
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +409 -404
- package/dist/{ui-bridge-CT18yqwN.d.ts → ui-bridge-Bdgl2hR8.d.ts} +2 -0
- package/dist/workspace.css +73 -0
- package/dist/workspace.d.ts +228 -6
- package/dist/workspace.js +188 -179
- package/docs/INTERFACES.md +6 -0
- package/docs/plans/FULL_PAGE_PANEL_ROUTE_SPEC.md +633 -0
- package/package.json +6 -6
- package/dist/WorkspaceProvider-Cn0sPgaB.js +0 -5976
- package/dist/createInMemoryBridge-CYNW1h_o.d.ts +0 -61
package/docs/INTERFACES.md
CHANGED
|
@@ -21,6 +21,12 @@ commands, and default workspace plugins.
|
|
|
21
21
|
`WorkspaceAgentFront` and `createWorkspaceAgentServer`, where workspace app
|
|
22
22
|
code may compose with documented `@hachej/boring-agent/server` APIs.
|
|
23
23
|
|
|
24
|
+
## Chat-first workspace boot
|
|
25
|
+
|
|
26
|
+
The default core-composed workspace route is chat-first after identity match: `WorkspaceAgentFront` mounts immediately, while `WorkspaceBackgroundBoot` warms tree/session/runtime readiness in the background. `WorkspaceBootGate` remains available for shells that want blocking boot.
|
|
27
|
+
|
|
28
|
+
Workbench surfaces are locally gated by warmup state. File tree/editor/plugin/left-tab surfaces do not mount while the current workspace is preparing or failed; chat remains visible. See [`packages/core/docs/CHAT_FIRST_WORKSPACE_BOOT.md`](../../core/docs/CHAT_FIRST_WORKSPACE_BOOT.md) for the product contract and stable readiness errors.
|
|
29
|
+
|
|
24
30
|
## Core Contracts
|
|
25
31
|
|
|
26
32
|
- Plugin contributions: `src/shared/plugins/frontFactory.ts`
|
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
# Dedicated Full-Page Pane Route Spec
|
|
2
|
+
|
|
3
|
+
Last updated: 2026-05-27
|
|
4
|
+
|
|
5
|
+
## Summary
|
|
6
|
+
|
|
7
|
+
Add one dedicated generic Workspace route for rendering an existing panel in a
|
|
8
|
+
new browser tab as a full-page surface.
|
|
9
|
+
|
|
10
|
+
This is intentionally the same simple mental model boring-macro used for decks
|
|
11
|
+
with `/present`, but generalized so any opted-in plugin panel can use it.
|
|
12
|
+
|
|
13
|
+
Primary use case now:
|
|
14
|
+
|
|
15
|
+
- `@hachej/boring-deck` should open the same deck panel in a new tab and let
|
|
16
|
+
that panel default into present/full-page behavior.
|
|
17
|
+
|
|
18
|
+
Secondary use cases this should unlock without feature-specific routes:
|
|
19
|
+
|
|
20
|
+
- HTML viewer open-in-new-tab
|
|
21
|
+
- report/artifact panels that benefit from more screen real estate
|
|
22
|
+
- future custom plugin panels that want a permalink/full-page view
|
|
23
|
+
|
|
24
|
+
The key design points are:
|
|
25
|
+
|
|
26
|
+
- one dedicated generic route
|
|
27
|
+
- reuse the same panel component
|
|
28
|
+
- avoid feature-specific page routes
|
|
29
|
+
- let the panel detect that it is being rendered full-page
|
|
30
|
+
|
|
31
|
+
## Problem
|
|
32
|
+
|
|
33
|
+
Today there is no generic Workspace concept of:
|
|
34
|
+
|
|
35
|
+
- “open this registered panel in another browser tab”
|
|
36
|
+
- “render this panel full-page outside Dockview”
|
|
37
|
+
- “let a panel know it is in full-page mode”
|
|
38
|
+
|
|
39
|
+
`boring-macro` previously solved this for decks with an app-owned route:
|
|
40
|
+
|
|
41
|
+
- `/present?path=...`
|
|
42
|
+
|
|
43
|
+
That worked because macro owned a dedicated app route and the deck feature was
|
|
44
|
+
special-cased. It is not a good generic answer for Workspace because:
|
|
45
|
+
|
|
46
|
+
- generic plugins should not hardcode app routes
|
|
47
|
+
- every new artifact type would otherwise add its own custom route
|
|
48
|
+
- the Workspace package does not own the host app’s router/pathnames
|
|
49
|
+
|
|
50
|
+
## Goals
|
|
51
|
+
|
|
52
|
+
- Reuse an existing registered panel component full-page in a new tab.
|
|
53
|
+
- Keep the capability generic across deck/html/report/custom artifact panels.
|
|
54
|
+
- Avoid feature-specific `/present`, `/html`, `/report`, etc. routes by
|
|
55
|
+
standardizing on one dedicated generic pane route.
|
|
56
|
+
- Let opted-in panels know they are being rendered full-page.
|
|
57
|
+
- Keep host apps in control of the actual route path they mount.
|
|
58
|
+
- Keep the v1 design small and reviewable.
|
|
59
|
+
|
|
60
|
+
## Non-goals
|
|
61
|
+
|
|
62
|
+
- Making every panel full-page capable by default.
|
|
63
|
+
- Replacing Dockview or normal pane rendering.
|
|
64
|
+
- Adding server persistence/permalinks/history in v1.
|
|
65
|
+
- Solving cross-session/shareable public URLs in v1.
|
|
66
|
+
- Requiring Workspace core/shared layers to depend on a specific router.
|
|
67
|
+
- Inventing a deck-only route once the generic path exists.
|
|
68
|
+
|
|
69
|
+
## Core decision
|
|
70
|
+
|
|
71
|
+
Use one dedicated generic full-page pane route.
|
|
72
|
+
|
|
73
|
+
Not:
|
|
74
|
+
|
|
75
|
+
- a deck-only route
|
|
76
|
+
- an HTML-only route
|
|
77
|
+
- a different route shape per artifact type
|
|
78
|
+
|
|
79
|
+
Yes:
|
|
80
|
+
|
|
81
|
+
- one Workspace-level full-page pane route
|
|
82
|
+
- panel id + params decide what renders there
|
|
83
|
+
|
|
84
|
+
This is deliberately the boring-macro `/present` idea transported upward into a
|
|
85
|
+
single reusable Workspace capability.
|
|
86
|
+
|
|
87
|
+
The generic capability should be split across two Workspace boundaries:
|
|
88
|
+
|
|
89
|
+
- **root package exports (`@hachej/boring-workspace`)** for plugin-safe front
|
|
90
|
+
helpers/hooks
|
|
91
|
+
- **app/front composition exports** for route/page helpers
|
|
92
|
+
|
|
93
|
+
Why this split:
|
|
94
|
+
|
|
95
|
+
- the Workspace package does not own the host router
|
|
96
|
+
- full-page rendering is app composition behavior
|
|
97
|
+
- plugin panels still need a safe place to read `dock` vs `full-page`
|
|
98
|
+
- base/shared code should stay router-agnostic
|
|
99
|
+
|
|
100
|
+
So Workspace should provide:
|
|
101
|
+
|
|
102
|
+
1. a **full-page panel page component** in `app/front`
|
|
103
|
+
2. a **root-exported href builder/hook** plus an `app/front` route parser
|
|
104
|
+
3. an **opt-in panel capability flag** on the normal panel contract
|
|
105
|
+
4. a **render-mode context/hook** on the root package export surface
|
|
106
|
+
5. a small **provider-level route-base config seam** so plugins do not hardcode
|
|
107
|
+
pp paths
|
|
108
|
+
|
|
109
|
+
The host app still mounts the dedicated route wherever it wants, e.g.:
|
|
110
|
+
|
|
111
|
+
- `/full-page`
|
|
112
|
+
- `/panel`
|
|
113
|
+
- `/artifact`
|
|
114
|
+
|
|
115
|
+
## Proposed API
|
|
116
|
+
|
|
117
|
+
## 1) Plugin-facing panel contract change
|
|
118
|
+
|
|
119
|
+
The opt-in belongs on the existing plugin-facing panel contract:
|
|
120
|
+
|
|
121
|
+
- `PanelConfig`
|
|
122
|
+
- `definePanel(...)`
|
|
123
|
+
- declarative `definePlugin({ panels: [...] })`
|
|
124
|
+
- imperative `setup(api => api.registerPanel(...))`
|
|
125
|
+
- `BoringFrontPanelRegistration`
|
|
126
|
+
- captured/registered front panel metadata
|
|
127
|
+
|
|
128
|
+
Proposed addition:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
interface PanelConfig<T = unknown> {
|
|
132
|
+
id: string
|
|
133
|
+
title: string
|
|
134
|
+
component: ComponentType<PaneProps<T>> | LazyFactory<PaneProps<T>>
|
|
135
|
+
// ...existing fields...
|
|
136
|
+
supportsFullPage?: boolean
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
And the mirrored front registration shape should carry the same field after
|
|
141
|
+
capture/bootstrap.
|
|
142
|
+
|
|
143
|
+
Rules:
|
|
144
|
+
|
|
145
|
+
- default `false`
|
|
146
|
+
- only `true` panels can be rendered by the dedicated generic full-page route
|
|
147
|
+
- if omitted, panel continues to work exactly as today
|
|
148
|
+
|
|
149
|
+
Opt-in usage:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
const deckPanel = definePanel<{ path?: string }>({
|
|
153
|
+
id: "deck",
|
|
154
|
+
title: "Deck",
|
|
155
|
+
component: DeckPane,
|
|
156
|
+
placement: "center",
|
|
157
|
+
supportsFullPage: true,
|
|
158
|
+
})
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Equivalent declarative plugin example:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
definePlugin({
|
|
165
|
+
id: "deck-plugin",
|
|
166
|
+
panels: [
|
|
167
|
+
{
|
|
168
|
+
id: "deck",
|
|
169
|
+
label: "Deck",
|
|
170
|
+
component: DeckPane,
|
|
171
|
+
placement: "center",
|
|
172
|
+
supportsFullPage: true,
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
})
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Equivalent imperative plugin example:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
definePlugin({
|
|
182
|
+
id: "deck-plugin",
|
|
183
|
+
setup(api) {
|
|
184
|
+
api.registerPanel({
|
|
185
|
+
id: "deck",
|
|
186
|
+
label: "Deck",
|
|
187
|
+
component: DeckPane,
|
|
188
|
+
placement: "center",
|
|
189
|
+
supportsFullPage: true,
|
|
190
|
+
})
|
|
191
|
+
},
|
|
192
|
+
})
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Runtime behavior:
|
|
196
|
+
|
|
197
|
+
1. route parses `component=<id>`
|
|
198
|
+
2. full-page renderer resolves that registered panel component from the registry
|
|
199
|
+
3. renderer checks `supportsFullPage`
|
|
200
|
+
4. `true` => render panel full-page
|
|
201
|
+
5. `false`/missing => show a not-supported error state
|
|
202
|
+
|
|
203
|
+
Why opt-in:
|
|
204
|
+
|
|
205
|
+
- many panels assume Dockview chrome/group/container behavior
|
|
206
|
+
- some panels are meaningless outside the workspace shell
|
|
207
|
+
- some panels may depend on `api`/`containerApi` methods that do not make
|
|
208
|
+
sense full-page
|
|
209
|
+
- plugin authors should opt in at the same place they already define panels,
|
|
210
|
+
not through an app-only API
|
|
211
|
+
|
|
212
|
+
## 2) Full-page route params
|
|
213
|
+
|
|
214
|
+
Keep params tiny and explicit.
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
interface FullPagePanelRouteState {
|
|
218
|
+
componentId: string
|
|
219
|
+
params?: Record<string, unknown>
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
URL shape in v1:
|
|
224
|
+
|
|
225
|
+
- `?component=<panel-component-id>¶ms=<urlencoded-json>`
|
|
226
|
+
|
|
227
|
+
Examples:
|
|
228
|
+
|
|
229
|
+
- `/full-page?component=deck¶ms=%7B%22path%22%3A%22deck%2Fintro.md%22%7D`
|
|
230
|
+
- `/full-page?component=html-viewer¶ms=%7B%22path%22%3A%22reports%2Fplan.html%22%7D`
|
|
231
|
+
|
|
232
|
+
The path `/full-page` is just an example. The important part is that there is
|
|
233
|
+
one dedicated generic route, not one route per feature.
|
|
234
|
+
|
|
235
|
+
Why JSON-in-query for v1:
|
|
236
|
+
|
|
237
|
+
- generic across panel types
|
|
238
|
+
- avoids route-per-feature explosion
|
|
239
|
+
- easy to build and parse
|
|
240
|
+
- does not require every panel to flatten params into query keys
|
|
241
|
+
|
|
242
|
+
Guardrails:
|
|
243
|
+
|
|
244
|
+
- reject invalid JSON
|
|
245
|
+
- reject non-object params
|
|
246
|
+
- parser does **not** validate panel existence/capability; that belongs in the
|
|
247
|
+
renderer where the registry is available
|
|
248
|
+
- show a simple full-page error state instead of crashing
|
|
249
|
+
|
|
250
|
+
## 3) URL helpers
|
|
251
|
+
|
|
252
|
+
Split the helpers by who needs them.
|
|
253
|
+
|
|
254
|
+
### Provider-level route-base config
|
|
255
|
+
|
|
256
|
+
Host apps configure the mounted route path once at the provider/app-shell level.
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
interface WorkspaceProviderProps {
|
|
260
|
+
fullPageBasePath?: string // e.g. "/full-page"
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
This replaces feature-specific seams like deck's current `getPresentHref(path)`.
|
|
265
|
+
Plugins do not invent paths; the app declares one generic full-page route base.
|
|
266
|
+
|
|
267
|
+
### Root-package helpers
|
|
268
|
+
|
|
269
|
+
Expose both a pure builder and a context-backed hook from the root package
|
|
270
|
+
export surface (`@hachej/boring-workspace`).
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
interface BuildFullPagePanelHrefInput {
|
|
274
|
+
componentId: string
|
|
275
|
+
params?: Record<string, unknown>
|
|
276
|
+
basePath: string
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function buildFullPagePanelHref(input: BuildFullPagePanelHrefInput): string
|
|
280
|
+
|
|
281
|
+
function useFullPagePanelHref(input: {
|
|
282
|
+
componentId: string
|
|
283
|
+
params?: Record<string, unknown>
|
|
284
|
+
}): string | null
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Behavior:
|
|
288
|
+
|
|
289
|
+
- `buildFullPagePanelHref(...)` is pure and explicit
|
|
290
|
+
- `useFullPagePanelHref(...)` reads `fullPageBasePath` from provider context
|
|
291
|
+
- if the host did not configure a full-page route, the hook returns `null`
|
|
292
|
+
|
|
293
|
+
### App/front helper
|
|
294
|
+
|
|
295
|
+
Route parser exported from `@hachej/boring-workspace/app/front` for host route
|
|
296
|
+
handling.
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
// Add one canonical app/front error-code module, e.g.
|
|
300
|
+
// packages/workspace/src/app/front/fullPageRouteErrors.ts
|
|
301
|
+
// exported from @hachej/boring-workspace/app/front as:
|
|
302
|
+
// - type WorkspaceFullPageRouteErrorCode = ...
|
|
303
|
+
// - FULL_PAGE_PANEL_MISSING_COMPONENT
|
|
304
|
+
// - FULL_PAGE_PANEL_INVALID_PARAMS_JSON
|
|
305
|
+
// - FULL_PAGE_PANEL_PARAMS_NOT_OBJECT
|
|
306
|
+
// - FULL_PAGE_PANEL_UNKNOWN_COMPONENT
|
|
307
|
+
// - FULL_PAGE_PANEL_NOT_SUPPORTED
|
|
308
|
+
// - FULL_PAGE_PANEL_RENDER_FAILED
|
|
309
|
+
|
|
310
|
+
function parseFullPagePanelLocation(search: string): {
|
|
311
|
+
componentId: string | null
|
|
312
|
+
params: Record<string, unknown>
|
|
313
|
+
error?: {
|
|
314
|
+
code: WorkspaceFullPageRouteErrorCode
|
|
315
|
+
message: string
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Host app still decides the pathname. Workspace only standardizes the query
|
|
321
|
+
payload.
|
|
322
|
+
|
|
323
|
+
## 4) Full-page page component
|
|
324
|
+
|
|
325
|
+
Expose a route/page component from `@hachej/boring-workspace/app/front`.
|
|
326
|
+
|
|
327
|
+
This is an **app-shell-facing** API, distinct from the plugin-facing panel
|
|
328
|
+
contract above.
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
interface WorkspaceFullPagePanelProps {
|
|
332
|
+
componentId: string
|
|
333
|
+
params?: Record<string, unknown>
|
|
334
|
+
notFoundFallback?: ReactNode
|
|
335
|
+
invalidRequestFallback?: ReactNode
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function WorkspaceFullPagePanel(props: WorkspaceFullPagePanelProps): JSX.Element
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Responsibilities:
|
|
342
|
+
|
|
343
|
+
- consume existing `WorkspaceProvider` context already mounted by the host app
|
|
344
|
+
- resolve the registered panel from the panel registry
|
|
345
|
+
- verify `supportsFullPage === true`
|
|
346
|
+
- validate that `params` is URL-serializable object data already parsed from
|
|
347
|
+
the route
|
|
348
|
+
- preserve normal plugin safety expectations (lazy loading, suspense, and panel
|
|
349
|
+
error isolation equivalent to normal panel rendering)
|
|
350
|
+
- supply a full-page render-mode context
|
|
351
|
+
- render the panel outside Dockview with lightweight shims for `api` and
|
|
352
|
+
`containerApi`
|
|
353
|
+
|
|
354
|
+
Non-responsibilities:
|
|
355
|
+
|
|
356
|
+
- mounting `WorkspaceProvider`
|
|
357
|
+
- choosing the browser pathname
|
|
358
|
+
- owning a router implementation
|
|
359
|
+
- inventing feature-specific chrome
|
|
360
|
+
|
|
361
|
+
## 5) Render-mode context
|
|
362
|
+
|
|
363
|
+
Do **not** widen every panel prop shape just to add `fullPage: boolean`.
|
|
364
|
+
|
|
365
|
+
`PaneProps` is currently the Dockview-owned contract. Changing that ripples into
|
|
366
|
+
many existing panels and tests. The smaller approach is a context hook.
|
|
367
|
+
|
|
368
|
+
This hook belongs on the **root package export surface**, not in `app/front`,
|
|
369
|
+
because plugin panels themselves need to consume it.
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
type PanelRenderMode = "dock" | "full-page"
|
|
373
|
+
|
|
374
|
+
interface PanelRenderContextValue {
|
|
375
|
+
mode: PanelRenderMode
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function usePanelRenderMode(): PanelRenderMode
|
|
379
|
+
function useIsFullPagePanel(): boolean
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Default outside the provider:
|
|
383
|
+
|
|
384
|
+
- `mode = "dock"`
|
|
385
|
+
|
|
386
|
+
Why context over prop in v1:
|
|
387
|
+
|
|
388
|
+
- no mass prop signature churn
|
|
389
|
+
- same panel component can opt in incrementally
|
|
390
|
+
- easy to use in deck/html without forcing all panels to care
|
|
391
|
+
|
|
392
|
+
## 6) Full-page panel shims
|
|
393
|
+
|
|
394
|
+
A full-page render will not have real Dockview APIs. Provide narrow no-op/
|
|
395
|
+
minimal adapters.
|
|
396
|
+
|
|
397
|
+
Because `PaneProps` currently exposes raw `DockviewPanelApi` / `DockviewApi`
|
|
398
|
+
types, this will likely require a typed shim object plus a narrow cast/proxy at
|
|
399
|
+
the render boundary. Call that out explicitly so the implementation stays honest
|
|
400
|
+
about the cost.
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
function createFullPagePanelApi(panelId: string): PaneProps["api"]
|
|
404
|
+
function createFullPageContainerApi(): PaneProps["containerApi"]
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Required minimum:
|
|
408
|
+
|
|
409
|
+
- `api.id`
|
|
410
|
+
- `api.setTitle()` should update document title or local page chrome title
|
|
411
|
+
- `api.close()` can call `window.close()` best-effort, or no-op if blocked
|
|
412
|
+
- other methods should be safe no-ops unless a better behavior is obvious
|
|
413
|
+
|
|
414
|
+
Important constraint:
|
|
415
|
+
|
|
416
|
+
- panels that truly require rich Dockview behavior should not opt in to
|
|
417
|
+
`supportsFullPage`
|
|
418
|
+
|
|
419
|
+
## Deck behavior on top of this
|
|
420
|
+
|
|
421
|
+
Once the Workspace full-page capability exists, workspace-hosted deck panels
|
|
422
|
+
should use `useFullPagePanelHref(...)` against the host-configured generic
|
|
423
|
+
route.
|
|
424
|
+
|
|
425
|
+
Important scope note:
|
|
426
|
+
|
|
427
|
+
- this generic route primarily replaces the current workspace/plugin path
|
|
428
|
+
- standalone `DeckPane` / `StandaloneDeckRoute` consumers may still need an
|
|
429
|
+
explicit present-link seam temporarily until a separate standalone story is
|
|
430
|
+
finalized
|
|
431
|
+
- so v1 should remove `getPresentHref` from `CreateDeckPluginOptions` and stop
|
|
432
|
+
routing workspace-hosted deck links through that plugin option
|
|
433
|
+
- if standalone deck consumers still need a custom present link after that,
|
|
434
|
+
move the seam to standalone-only deck props instead of leaving it on the
|
|
435
|
+
workspace plugin builder
|
|
436
|
+
|
|
437
|
+
Deck panel behavior:
|
|
438
|
+
|
|
439
|
+
- dock mode:
|
|
440
|
+
- normal read/edit/present toggle
|
|
441
|
+
- full-page mode:
|
|
442
|
+
- default to present mode
|
|
443
|
+
- keep keyboard slide navigation
|
|
444
|
+
- keep the same parser/widgets/content logic
|
|
445
|
+
- optionally reduce non-essential chrome
|
|
446
|
+
|
|
447
|
+
This preserves the user’s original macro UX goal while keeping the solution
|
|
448
|
+
fully generic.
|
|
449
|
+
|
|
450
|
+
## HTML viewer behavior on top of this
|
|
451
|
+
|
|
452
|
+
HTML viewer can opt into the same route later.
|
|
453
|
+
|
|
454
|
+
Expected behavior:
|
|
455
|
+
|
|
456
|
+
- same `HtmlViewerPane`
|
|
457
|
+
- same file/path params
|
|
458
|
+
- rendered full-page via the generic route
|
|
459
|
+
- no HTML-specific new-tab system needed
|
|
460
|
+
|
|
461
|
+
This is the main proof that the abstraction is not actually deck-specific.
|
|
462
|
+
|
|
463
|
+
## Route ownership model
|
|
464
|
+
|
|
465
|
+
Workspace should provide the page component and helpers, but the host app should
|
|
466
|
+
still mount the dedicated route.
|
|
467
|
+
|
|
468
|
+
Important constraint: `WorkspaceAgentFront` is the **full shell** today. It does
|
|
469
|
+
not accept arbitrary children and always renders the normal top bar + chat +
|
|
470
|
+
dock layout. So the full-page route cannot simply nest `WorkspaceFullPagePanel`
|
|
471
|
+
inside `WorkspaceAgentFront`.
|
|
472
|
+
|
|
473
|
+
For v1, keep this simple: mount the dedicated full-page pane route under the
|
|
474
|
+
already-public `WorkspaceProvider` directly.
|
|
475
|
+
|
|
476
|
+
Example host wiring:
|
|
477
|
+
|
|
478
|
+
```tsx
|
|
479
|
+
function App() {
|
|
480
|
+
if (window.location.pathname === "/full-page") {
|
|
481
|
+
const parsed = parseFullPagePanelLocation(window.location.search)
|
|
482
|
+
if (!parsed.componentId) return <InvalidRequest />
|
|
483
|
+
|
|
484
|
+
return (
|
|
485
|
+
<WorkspaceProvider {...sharedWorkspaceProviderProps}>
|
|
486
|
+
<WorkspaceFullPagePanel componentId={parsed.componentId} params={parsed.params} />
|
|
487
|
+
</WorkspaceProvider>
|
|
488
|
+
)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return <WorkspaceAgentFront {...sharedWorkspaceAgentFrontProps} />
|
|
492
|
+
}
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
This keeps routing decisions in the app, keeps the first version small, and
|
|
496
|
+
reuses the existing provider/registry bootstrap that already exists.
|
|
497
|
+
|
|
498
|
+
## Error handling
|
|
499
|
+
|
|
500
|
+
The full-page panel route must fail softly.
|
|
501
|
+
|
|
502
|
+
Cases:
|
|
503
|
+
|
|
504
|
+
- missing `component`
|
|
505
|
+
- malformed `params`
|
|
506
|
+
- unknown component id
|
|
507
|
+
- panel not opted into `supportsFullPage`
|
|
508
|
+
- panel render crash
|
|
509
|
+
|
|
510
|
+
Expected behavior:
|
|
511
|
+
|
|
512
|
+
- show a simple full-page `ErrorState`
|
|
513
|
+
- never crash the whole app shell
|
|
514
|
+
- route parser returns stable coded failure data instead of throwing raw JSON
|
|
515
|
+
parse errors into React
|
|
516
|
+
|
|
517
|
+
## Security / trust boundaries
|
|
518
|
+
|
|
519
|
+
This route should not expand trust boundaries beyond what normal Workspace panes
|
|
520
|
+
already have.
|
|
521
|
+
|
|
522
|
+
Notes:
|
|
523
|
+
|
|
524
|
+
- params come from the URL, so panel code must still validate/normalize them
|
|
525
|
+
- file-backed panes should continue using existing path validation/storage APIs
|
|
526
|
+
- this route should not add a new arbitrary code-loading mechanism
|
|
527
|
+
- only already-registered panels can render
|
|
528
|
+
|
|
529
|
+
## Implementation plan
|
|
530
|
+
|
|
531
|
+
### Phase 1 — Workspace route contract
|
|
532
|
+
|
|
533
|
+
- add `supportsFullPage?: boolean` to `PanelConfig` / `definePanel` / captured
|
|
534
|
+
panel registrations
|
|
535
|
+
- add `fullPageBasePath?: string` to `WorkspaceProviderProps` (and pass-through
|
|
536
|
+
from `WorkspaceAgentFront`)
|
|
537
|
+
- add root-exported href builder + `useFullPagePanelHref(...)`
|
|
538
|
+
- add `app/front/fullPageRouteErrors.ts` and export stable full-page route
|
|
539
|
+
error codes
|
|
540
|
+
- add route parser in `app/front`
|
|
541
|
+
- add full-page render-mode context/hooks on the root package export surface
|
|
542
|
+
- add full-page panel component with panel lookup + shims + error states
|
|
543
|
+
|
|
544
|
+
### Phase 2 — Deck migration
|
|
545
|
+
|
|
546
|
+
- mark deck panel as `supportsFullPage: true`
|
|
547
|
+
- switch workspace-hosted deck panel links to `useFullPagePanelHref({ componentId, params })`
|
|
548
|
+
- remove `getPresentHref` from `CreateDeckPluginOptions`
|
|
549
|
+
- if needed, add a standalone-only present-link prop on `DeckPane` /
|
|
550
|
+
`StandaloneDeckRoute` instead of keeping that seam on the plugin builder
|
|
551
|
+
- deck uses render-mode hook to default to present mode when full-page
|
|
552
|
+
|
|
553
|
+
### Phase 3 — Playground proof
|
|
554
|
+
|
|
555
|
+
- mount one dedicated generic host route in `apps/workspace-playground`
|
|
556
|
+
- add “open in new tab” using that route
|
|
557
|
+
- verify real browser-tab behavior
|
|
558
|
+
|
|
559
|
+
### Phase 4 — Optional second consumer
|
|
560
|
+
|
|
561
|
+
- opt HTML viewer into `supportsFullPage`
|
|
562
|
+
- prove the abstraction is not deck-only
|
|
563
|
+
|
|
564
|
+
## Testing
|
|
565
|
+
|
|
566
|
+
### Unit
|
|
567
|
+
|
|
568
|
+
- URL builder/parser round-trips
|
|
569
|
+
- invalid query shapes fail cleanly
|
|
570
|
+
- panel opt-in enforcement
|
|
571
|
+
- full-page render-mode hook defaults/overrides
|
|
572
|
+
- `api.setTitle()` updates page title in full-page mode
|
|
573
|
+
|
|
574
|
+
### Integration
|
|
575
|
+
|
|
576
|
+
- `WorkspaceFullPagePanel` renders a registered opted-in test panel
|
|
577
|
+
- non-opted-in panel is rejected with a stable code
|
|
578
|
+
- unknown component is rejected with a stable code
|
|
579
|
+
- panel receives params intact
|
|
580
|
+
- render-mode hook reports `full-page`
|
|
581
|
+
- lazy panel rendering still works
|
|
582
|
+
- full-page panel crash is isolated to the page error state, not an app crash
|
|
583
|
+
|
|
584
|
+
### Deck-specific
|
|
585
|
+
|
|
586
|
+
- deck defaults to present mode in full-page render mode
|
|
587
|
+
- keyboard nav still works in full-page deck mode
|
|
588
|
+
- edit mode remains normal in dock mode
|
|
589
|
+
|
|
590
|
+
### Playground / e2e
|
|
591
|
+
|
|
592
|
+
- open deck in workspace
|
|
593
|
+
- click open-in-new-tab
|
|
594
|
+
- new tab opens generic full-page route
|
|
595
|
+
- same deck content renders there
|
|
596
|
+
- keyboard navigation works there
|
|
597
|
+
- original workspace tab remains stable
|
|
598
|
+
|
|
599
|
+
## Migration / compatibility
|
|
600
|
+
|
|
601
|
+
This should be additive.
|
|
602
|
+
|
|
603
|
+
- existing panels continue unchanged
|
|
604
|
+
- existing app routes continue unchanged
|
|
605
|
+
- macro can keep `/present` temporarily until migrated
|
|
606
|
+
- deck can support both old seam and generic route briefly if needed, but the
|
|
607
|
+
desired end state is to remove the deck-specific route seam once the generic
|
|
608
|
+
route is proven
|
|
609
|
+
|
|
610
|
+
## Locked decisions / follow-ups
|
|
611
|
+
|
|
612
|
+
1. Public export ownership
|
|
613
|
+
- export `buildFullPagePanelHref`, `useFullPagePanelHref`,
|
|
614
|
+
`usePanelRenderMode`, and `useIsFullPagePanel` from the root package
|
|
615
|
+
(`@hachej/boring-workspace`)
|
|
616
|
+
- keep the route parser/page in `@hachej/boring-workspace/app/front`
|
|
617
|
+
- follow-up: update docs/examples to show the new imports clearly
|
|
618
|
+
2. Should params be JSON-in-query or flat query keys?
|
|
619
|
+
- Recommendation: JSON-in-query in v1 for simplicity and genericity.
|
|
620
|
+
- Constraint: full-page-capable panels must treat params as URL-serializable
|
|
621
|
+
data, not callbacks/classes/functions.
|
|
622
|
+
3. Should deck keep a custom label like “Present” in full-page mode?
|
|
623
|
+
- Recommendation: yes in its own panel UI, but the route stays generic.
|
|
624
|
+
4. Do we need shareable/stable URLs across sessions right now?
|
|
625
|
+
- Recommendation: no. Keep this local/full-page only in v1.
|
|
626
|
+
|
|
627
|
+
## Recommendation
|
|
628
|
+
|
|
629
|
+
Build the generic Workspace full-page panel route.
|
|
630
|
+
|
|
631
|
+
It is small, matches the desired UX, avoids deck-specific routing, and creates a
|
|
632
|
+
reusable capability for deck, HTML viewer, and future artifact panes without
|
|
633
|
+
pushing router ownership into the base Workspace package.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hachej/boring-workspace",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Workspace UI, plugin, and bridge package for composing chat, files, catalogs, editors, and app-specific panes.",
|
|
@@ -130,8 +130,8 @@
|
|
|
130
130
|
"tailwind-merge": "^2.0.0",
|
|
131
131
|
"zod": "^3.23.0",
|
|
132
132
|
"zustand": "^5.0.0",
|
|
133
|
-
"@hachej/boring-agent": "0.1.
|
|
134
|
-
"@hachej/boring-ui-kit": "0.1.
|
|
133
|
+
"@hachej/boring-agent": "0.1.26",
|
|
134
|
+
"@hachej/boring-ui-kit": "0.1.26"
|
|
135
135
|
},
|
|
136
136
|
"devDependencies": {
|
|
137
137
|
"@tailwindcss/postcss": "^4.0.0",
|
|
@@ -159,9 +159,9 @@
|
|
|
159
159
|
},
|
|
160
160
|
"scripts": {
|
|
161
161
|
"dev": "vite",
|
|
162
|
-
"build": "
|
|
163
|
-
"typecheck": "
|
|
164
|
-
"test": "
|
|
162
|
+
"build": "tsup && vite build && node ./scripts/build-workspace-css.mjs && node ./scripts/assert-build-artifacts.mjs",
|
|
163
|
+
"typecheck": "tsc --noEmit -p tsconfig.front.json && tsc --noEmit -p tsconfig.server.json",
|
|
164
|
+
"test": "vitest run",
|
|
165
165
|
"check:bundle-size": "node ./scripts/check-bundle-size.mjs",
|
|
166
166
|
"lint:plugin-invariants": "node ./scripts/check-plugin-invariants.mjs",
|
|
167
167
|
"lint": "pnpm run typecheck"
|