Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
488 lines
17 KiB
Markdown
488 lines
17 KiB
Markdown
# Workbench Contributions
|
|
|
|
This document defines the current compatible extension model for
|
|
`@tutti-os/workbench-surface`.
|
|
|
|
The goal is to let hosts move from one large, centralized workbench host
|
|
assembly layer toward feature-owned workbench contributions without breaking
|
|
existing consumers that already integrate `WorkbenchHost` directly.
|
|
|
|
## Problem Summary
|
|
|
|
`WorkbenchHost` currently exposes a flat host API:
|
|
|
|
- `nodes`
|
|
- `dockEntries`
|
|
- `externalStateSource`
|
|
- `onLaunchRequest`
|
|
- `onNodeCloseRequest`
|
|
- `snapshotRepository`
|
|
- other host shell inputs such as `workspaceId`, `missionControl`, and layout
|
|
constraints
|
|
|
|
That API is still valid and should remain the primary public integration model
|
|
for current consumers. It works well for small hosts and for hosts that want
|
|
one composition root.
|
|
|
|
The pressure appears when a host grows multiple reusable workbench capabilities:
|
|
|
|
- browser node integration
|
|
- terminal node integration
|
|
- issue-manager integration
|
|
- files node integration
|
|
- future agent, preview, search, or provider-owned nodes
|
|
|
|
Without another composition layer, hosts tend to accumulate one increasingly
|
|
large assembly service that owns:
|
|
|
|
- node definitions
|
|
- dock entry wiring
|
|
- external state composition
|
|
- launch dispatch
|
|
- close-policy dispatch
|
|
- package-specific host adapters
|
|
|
|
That assembly style becomes harder to maintain, but replacing the current
|
|
`WorkbenchHost` props with a new required model would break downstream
|
|
consumers.
|
|
|
|
## Decision
|
|
|
|
`WorkbenchHost` accepts an optional `contributions` input while preserving the
|
|
explicit host props and their behavior.
|
|
|
|
`contributions` are an additive composition mechanism, not a replacement host
|
|
API.
|
|
|
|
Compatibility rule:
|
|
|
|
- existing `WorkbenchHost` integrations must continue to work without code
|
|
changes
|
|
- existing explicit props remain supported and keep their current semantics
|
|
- `contributions` are optional and may be adopted incrementally
|
|
|
|
New workbench modules should default to
|
|
contribution-based integration. Direct top-level `nodes`, `dockEntries`,
|
|
`externalStateSource`, `onLaunchRequest`, and `onNodeCloseRequest` wiring should
|
|
be reserved for compatibility, host-wide policy, or deliberately simple shells.
|
|
|
|
## Design Goals
|
|
|
|
The contribution model should:
|
|
|
|
1. preserve backward compatibility for current `WorkbenchHost` consumers
|
|
2. let feature-owning packages participate in workbench integration without
|
|
forcing all assembly back into one app-level host service
|
|
3. keep host-specific business policy, preload access, and daemon adapters in
|
|
the consuming host
|
|
4. make merge behavior deterministic and easy to explain to downstream users
|
|
5. keep `workbench-surface` free of product-specific workflow knowledge
|
|
|
|
## Non-Goals
|
|
|
|
This change does not try to:
|
|
|
|
- remove or deprecate the flat `WorkbenchHost` prop model
|
|
- move preload APIs, Electron globals, `tuttid` clients, or host-specific
|
|
transport wiring into shared packages
|
|
- make workbench contributions responsible for top-level shell concerns such as
|
|
`snapshotRepository`, `workspaceId`, `missionControl`, or layout constraints
|
|
- force simple or compatibility consumers to use contributions
|
|
|
|
## Ownership Boundaries
|
|
|
|
### `packages/workbench/surface`
|
|
|
|
`workbench-surface` should own:
|
|
|
|
- the contribution type contract
|
|
- normalization and merge helpers
|
|
- deterministic composition rules between explicit host props and
|
|
contributions
|
|
- deterministic composition for host-wide close preparation contributed by
|
|
workbench modules
|
|
- default runtime behavior after configuration is resolved
|
|
|
|
It should not own:
|
|
|
|
- desktop-specific preload or Electron details
|
|
- `tuttid` client construction
|
|
- product copy overrides
|
|
- app-specific diagnostics sinks
|
|
- host-specific business close or reuse policy
|
|
|
|
### Capability-owning packages
|
|
|
|
Packages such as:
|
|
|
|
- `@tutti-os/browser-node`
|
|
- `@tutti-os/workspace-terminal`
|
|
- `@tutti-os/workspace-issue-manager`
|
|
|
|
may expose feature-specific workbench contribution factories when the shared
|
|
capability already owns reusable node or dock behavior.
|
|
|
|
Those packages may own:
|
|
|
|
- workbench node definitions for the capability
|
|
- dock entries for the capability
|
|
- capability-local external state shaping
|
|
- capability-local launch helpers
|
|
- capability-local close-policy hooks
|
|
- custom-header presentation declared through
|
|
`WorkbenchHostNodeWindowCapabilities.header`
|
|
|
|
They must not own:
|
|
|
|
- host globals such as `window.tutti`
|
|
- preload surface assumptions
|
|
- app-specific daemon client creation
|
|
- product-specific enablement or routing policy
|
|
|
|
### Window header presentation
|
|
|
|
A node definition declares structural custom-header state through
|
|
`window.header`: `heightPx`, `layout: "overlay"`, `overflow: "visible"`, or
|
|
`border: "none"`. `WorkbenchHost` projects that state onto the
|
|
`.workbench-window` subject as data attributes and, for height, a CSS custom
|
|
property.
|
|
|
|
Do not discover these states with `.workbench-window:has(...)` or by inspecting
|
|
rendered header descendants. A descendant mutation inside an editor, browser,
|
|
or other live node can otherwise make the full window subtree a style
|
|
invalidation candidate. Component-local relational selectors remain acceptable
|
|
only when both the subject and mutation scope are small and bounded.
|
|
|
|
### External State Subscription
|
|
|
|
External state is node-scoped. `WorkbenchHostExternalStateSource` exposes
|
|
`subscribeNodeState(input, listener)` and, when a node renderer consumes shared
|
|
workspace state, `subscribeWorkspaceState(input, listener)`. The host binds
|
|
those subscriptions at node-renderer leaves; it does not maintain a host-wide
|
|
external-state revision.
|
|
|
|
Source snapshots returned by `getNodeState(...)` and `getWorkspaceState(...)`
|
|
must preserve reference identity until their observable slice changes. A
|
|
contribution must not aggregate unrelated capability, room, or shell state into
|
|
one workspace snapshot. If a capability has no live state, it omits the
|
|
corresponding subscription method.
|
|
|
|
### Consuming hosts such as `apps/desktop`
|
|
|
|
The consuming host should continue to own:
|
|
|
|
- host adapter construction
|
|
- daemon clients and preload wiring
|
|
- product policy such as feature enablement or access rules
|
|
- top-level shell inputs such as `snapshotRepository`, `workspaceId`,
|
|
`missionControl`, and layout constraints
|
|
- any product-local contributions that do not yet have a real shared boundary
|
|
|
|
## Public Shape
|
|
|
|
The current `WorkbenchHostProps` shape includes one optional composition field:
|
|
|
|
```ts
|
|
export interface WorkbenchContribution {
|
|
id: string;
|
|
nodes?: readonly WorkbenchHostNodeDefinition[];
|
|
dockEntries?: readonly WorkbenchHostDockEntry[];
|
|
externalStateSource?: WorkbenchHostExternalStateSource;
|
|
onLaunchRequest?: (
|
|
request: WorkbenchHostLaunchRequest
|
|
) =>
|
|
| Promise<WorkbenchHostLaunchResult | null | void>
|
|
| WorkbenchHostLaunchResult
|
|
| null
|
|
| void;
|
|
onNodeCloseRequest?: (
|
|
request: WorkbenchHostNodeCloseRequest
|
|
) =>
|
|
| Promise<WorkbenchHostNodeCloseDecision | void>
|
|
| WorkbenchHostNodeCloseDecision
|
|
| void;
|
|
prepareHostClose?: (
|
|
context: WorkbenchHostClosePreparationContext
|
|
) => Promise<boolean> | boolean;
|
|
}
|
|
|
|
export interface WorkbenchHostProps {
|
|
// existing fields remain supported
|
|
contributions?: readonly WorkbenchContribution[];
|
|
}
|
|
```
|
|
|
|
The model stays intentionally small. It mirrors the parts of
|
|
the existing host API that are already repeated across feature integrations.
|
|
|
|
## Configuration Resolution
|
|
|
|
`WorkbenchHost` normalizes all configuration through one internal
|
|
resolution step before runtime wiring.
|
|
|
|
Illustrative shape:
|
|
|
|
```ts
|
|
interface ResolvedWorkbenchHostConfig {
|
|
nodes: readonly WorkbenchHostNodeDefinition[];
|
|
dockEntries: readonly WorkbenchHostDockEntry[];
|
|
externalStateSource?: WorkbenchHostExternalStateSource;
|
|
onLaunchRequest?: WorkbenchHostProps["onLaunchRequest"];
|
|
onNodeCloseRequest?: WorkbenchHostProps["onNodeCloseRequest"];
|
|
}
|
|
|
|
function resolveWorkbenchHostConfig(
|
|
props: WorkbenchHostProps
|
|
): ResolvedWorkbenchHostConfig;
|
|
```
|
|
|
|
This resolver is the compatibility boundary.
|
|
|
|
Rules:
|
|
|
|
- old-style explicit host props remain first-class public API
|
|
- contributions are folded into the same resolved shape
|
|
- runtime code below the resolver should not need to care whether a capability
|
|
came from explicit props or a contribution
|
|
|
|
## Merge Rules
|
|
|
|
Merge rules must be deterministic and documented because external consumers may
|
|
depend on current behavior.
|
|
|
|
### Nodes
|
|
|
|
- contribution-provided nodes are collected first in contribution order
|
|
- explicit `nodes` prop is appended last
|
|
- if multiple entries share the same `typeId`, the later one wins
|
|
- therefore an explicit top-level `nodes` entry overrides contribution-provided
|
|
nodes with the same `typeId`
|
|
|
|
Rationale:
|
|
|
|
- additive contributions stay simple
|
|
- existing direct consumers keep override authority
|
|
|
|
### Dock Entries
|
|
|
|
- contribution-provided dock entries are collected first in contribution order
|
|
- explicit `dockEntries` prop is appended last
|
|
- if multiple entries share the same `id`, the later one wins
|
|
- therefore an explicit top-level dock entry overrides a contribution-provided
|
|
entry with the same `id`
|
|
|
|
### External State Source
|
|
|
|
- if the explicit top-level `externalStateSource` prop is present, it keeps its
|
|
current meaning and contribution-provided external-state sources are ignored
|
|
- otherwise contribution-provided sources are combined in contribution order
|
|
- when combining contribution sources, the first non-null, non-undefined state
|
|
result wins for each lookup
|
|
|
|
This preserves current direct-consumer behavior while still allowing a purely
|
|
contribution-based host to avoid hand-writing a combiner.
|
|
|
|
If a host wants custom composition on top of contribution sources, it should
|
|
build and pass one explicit top-level `externalStateSource` intentionally rather
|
|
than rely on hidden fallback behavior.
|
|
|
|
### Launch Request
|
|
|
|
- if an explicit top-level `onLaunchRequest` is present, it runs first
|
|
- if it returns a non-null result, resolution stops
|
|
- if it returns `null` or `void`, contribution launch handlers run in
|
|
contribution order until one returns a non-null result
|
|
|
|
This keeps current host launch behavior authoritative while allowing
|
|
contributions to act as fallbacks.
|
|
|
|
### Node Close Request
|
|
|
|
- if an explicit top-level `onNodeCloseRequest` is present, it runs first
|
|
- if it returns a close decision, resolution stops
|
|
- if it returns `void`, contribution close handlers run in contribution order
|
|
until one returns a close decision
|
|
|
|
### Top-Level Single-Owner Inputs
|
|
|
|
The following remain top-level host inputs and are not part of the first
|
|
contribution contract:
|
|
|
|
- `snapshotRepository`
|
|
- `workspaceId`
|
|
- `missionControl`
|
|
- `layoutConstraints`
|
|
- top-level shell renderers such as `renderTopChrome` and `renderBottomChrome`
|
|
- wallpaper and shortcut policy
|
|
|
|
These are host-shell concerns, not feature contributions.
|
|
|
|
## Compatibility Model
|
|
|
|
The compatibility model is the main reason to introduce contributions as an
|
|
additive API.
|
|
|
|
### Existing Consumers
|
|
|
|
Existing consumers may continue using:
|
|
|
|
```ts
|
|
<WorkbenchHost
|
|
nodes={...}
|
|
dockEntries={...}
|
|
externalStateSource={...}
|
|
onLaunchRequest={...}
|
|
onNodeCloseRequest={...}
|
|
snapshotRepository={...}
|
|
workspaceId={...}
|
|
/>
|
|
```
|
|
|
|
No migration is required in the first landing.
|
|
|
|
### Mixed Consumers
|
|
|
|
Hosts that want gradual adoption may mix both styles:
|
|
|
|
```ts
|
|
<WorkbenchHost
|
|
contributions={[
|
|
browserContribution,
|
|
terminalContribution,
|
|
issueManagerContribution
|
|
]}
|
|
nodes={[filesNode]}
|
|
onLaunchRequest={hostLaunchOverride}
|
|
snapshotRepository={repository}
|
|
workspaceId={workspaceId}
|
|
/>
|
|
```
|
|
|
|
This mixed mode is intentional and should be supported, not treated as a
|
|
temporary edge case.
|
|
|
|
### Contribution-first Consumers
|
|
|
|
New hosts may adopt a mostly contribution-based model while still providing the
|
|
required top-level shell inputs:
|
|
|
|
```ts
|
|
<WorkbenchHost
|
|
contributions={contributions}
|
|
snapshotRepository={repository}
|
|
workspaceId={workspaceId}
|
|
/>
|
|
```
|
|
|
|
## Downstream Package Guidance
|
|
|
|
When a shared capability package adds contribution support, prefer a narrow
|
|
factory such as:
|
|
|
|
```ts
|
|
createTerminalWorkbenchContribution(...)
|
|
createIssueManagerWorkbenchContribution(...)
|
|
createBrowserWorkbenchContribution(...)
|
|
```
|
|
|
|
Those factories should accept host-provided adapters and policy inputs rather
|
|
than discovering globals or constructing product clients internally.
|
|
|
|
If a package cannot expose a contribution without learning host-specific
|
|
behavior, that is a sign the boundary is not yet ready to move into the
|
|
package.
|
|
|
|
## Desktop Adoption Boundaries
|
|
|
|
`apps/desktop` should adopt workbench contributions through feature-local
|
|
services, not through React components.
|
|
|
|
The desktop workspace workbench currently uses these modules:
|
|
|
|
- `@tutti-os/workbench-host` owns the public capability descriptor and registry
|
|
contracts. The registry resolves by `order` then `id` and rejects duplicate
|
|
factory, contribution, node type, and dock entry ownership before host input
|
|
is published. `services/internal/workbenchProductProfile.ts` keeps Tutti's
|
|
product ID and scope kind private because the shared kernel does not yet
|
|
consume or enforce those fields. Desktop production code and lifecycle tests
|
|
import the shared package directly; there is no private registry,
|
|
coordinator, or session compatibility path.
|
|
- `services/internal/tuttiWorkbenchProductProfile.ts` owns the Tutti capability
|
|
selection. It binds each desktop contribution factory to a newly projected,
|
|
capability-specific context so factories do not receive the full product
|
|
context or discover services through a locator.
|
|
- `services/internal/workspaceWorkbenchContributionFactory.ts` defines the
|
|
desktop-local adapter input and generic binding helper. Concrete factories
|
|
declare only the subset of host adapters, i18n runtimes, renderer callbacks,
|
|
and workspace context their capability uses, then return the existing
|
|
`WorkbenchContribution` contract.
|
|
- `@tutti-os/workbench-host` owns the lifecycle-diagnostics contract used by
|
|
coordinator/session disposal. `services/internal/workbenchHostPorts.ts`
|
|
retains the private snapshot repository port because snapshot transport is
|
|
still product-owned and is not consumed by the shared kernel. Desktop
|
|
implementations remain under `services/internal/adapters`; Tutti transport
|
|
and diagnostic payload mapping do not move into the neutral kernel.
|
|
- `services/internal/contributions/*` owns concrete desktop-local adapters for
|
|
browser, terminal, issue-manager, files, and other workspace capabilities
|
|
that have not yet moved into a shared package factory.
|
|
- `workspaceWorkbenchHostInputResolver.ts` owns product host-input assembly,
|
|
including contribution factories, dynamic dock entries, snapshot repository
|
|
wiring, close policy, and node-preview capture.
|
|
- `workspaceWorkbenchHostService.ts` remains the desktop facade for session
|
|
leases, wallpaper/onboarding persistence, external bridges, and desktop IPC
|
|
adapters. It delegates host-input assembly to the resolver instead of
|
|
becoming a second feature business core.
|
|
- `services/workspaceWorkbenchShellRuntimeController.ts` owns shell-level
|
|
runtime state that is independent of DOM rendering: mission control, close
|
|
guard dialog state, host close requests, wallpaper selection, and the
|
|
resolved host input.
|
|
- UI hooks such as `useWorkspaceWorkbenchShellRuntime.tsx` and
|
|
`useWorkspaceChromeState.ts` are React adapters. They may subscribe to
|
|
controllers and pass browser events or DI services into them, but they should
|
|
not own feature orchestration.
|
|
|
|
This split keeps the module interfaces deep:
|
|
|
|
- contribution factories hide feature-specific workbench wiring behind a small
|
|
`createContribution(...)` call
|
|
- the registry hides ordering and availability policy
|
|
- the host service hides desktop adapter construction
|
|
- shell controllers hide state transitions and side effects that do not need
|
|
React
|
|
- React files stay shallow and mostly render snapshots, wire DOM events, and
|
|
connect browser lifecycle to controllers
|
|
|
|
When adding a new desktop workbench capability, prefer this path:
|
|
|
|
1. add a contribution factory under the owning feature or
|
|
`workspace-workbench/services/internal/contributions`
|
|
2. declare its narrow context and bind it in `tuttiWorkbenchProductProfile.ts`
|
|
3. let the neutral capability registry resolve and validate the profile
|
|
4. keep daemon clients, preload calls, and product enablement in desktop-owned
|
|
adapters
|
|
5. expose only render callbacks or browser event subscriptions through the UI
|
|
adapter when React is actually required
|
|
|
|
Do not add new workbench capability wiring directly to `WorkspaceWorkbench.tsx`,
|
|
`WorkspaceChrome.tsx`, or settings panels unless the behavior is purely
|
|
presentational. If the code decides what to launch, close, persist, refresh, or
|
|
subscribe to, it belongs in a feature-local service or controller first.
|
|
|
|
## Relationship To Existing Workbench Documents
|
|
|
|
This contribution model complements the current workbench architecture:
|
|
|
|
- [Workbench Dock Model](./workbench-dock-model.md) still defines dock identity,
|
|
grouping, and launch semantics
|
|
- [Workbench Node Lifecycle](./workbench-node-lifecycle.md) still defines node
|
|
definition, projected presence, launch, activation, and snapshot purity
|
|
|
|
Contributions are only a host configuration mechanism. They do not change the
|
|
underlying node lifecycle or snapshot rules.
|
|
|
|
## Current Contract Summary
|
|
|
|
The workbench contribution model is a backward-compatible optional composition
|
|
layer. It improves module ownership without forcing simple or downstream hosts
|
|
to rewrite explicit `WorkbenchHost` integrations.
|