@joycostudio/studio 0.0.0-stage → 0.0.2

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/README.md CHANGED
@@ -1,3 +1,204 @@
1
- # Temporary Holding Version
1
+ # @joycostudio/studio
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Plug project-specific tools into a `/studio` route, the app's floating debug pane, and MCP agents, with one contract per tool.
4
+
5
+ For contributor instructions on stable releases, betas, and commit previews, see the
6
+ [release workflow guide](../../.changeset/README.md).
7
+
8
+ A tool's controls are an [Xyz](https://hub.joyco.studio/toolbox/xyz) debug schema (Tweakpane). That same schema:
9
+
10
+ - renders the inspector in `/studio`,
11
+ - mounts in the app's floating pane (`?debug` / <kbd>Option</kbd>+<kbd>D</kbd>) through Xyz's `DebugManager`,
12
+ - validates every write in the runtime (slider bounds, select options, types, and the shape of `initial`).
13
+
14
+ ## 1. Define a tool
15
+
16
+ Tool files are loaded by the runtime in Node too, so keep them free of DOM/three imports and `@/` aliases.
17
+
18
+ ```ts
19
+ // src/tools/grading.ts
20
+ import { defineTool } from '@joycostudio/studio'
21
+ import { property, select, slider } from '@joycostudio/xyz/debug'
22
+ import initial from './grading.json'
23
+
24
+ export const grading = defineTool({
25
+ id: 'color-grading',
26
+ file: 'src/tools/grading.json', // where saves go, repo-relative
27
+ initial, // the same file, imported: production ships the saved config
28
+ preview: '/', // route shown next to the inspector in /studio
29
+ controls: (config) => ({
30
+ exposure: slider(property(config, 'exposure'), { min: 0, max: 3 }),
31
+ tonemap: select(property(config, 'tonemap'), { ACES: 'aces', Filmic: 'filmic' }),
32
+ }),
33
+ })
34
+ ```
35
+
36
+ ## 2. Register tools with the runtime
37
+
38
+ ```ts
39
+ // studio.config.ts (its directory is the project root)
40
+ import { defineConfig } from '@joycostudio/studio'
41
+ import { grading } from './src/tools/grading'
42
+
43
+ export default defineConfig({ tools: [grading] })
44
+ ```
45
+
46
+ ```json
47
+ { "scripts": { "dev": "studio dev -- next dev" } }
48
+ ```
49
+
50
+ `studio dev` starts the WebSocket bridge on `ws://127.0.0.1:5179` and the HTTP MCP endpoint on
51
+ `http://127.0.0.1:5179/mcp` (`--port` changes both). It stops when the wrapped command exits.
52
+
53
+ ## 3. Use it in the app
54
+
55
+ ```ts
56
+ // src/studio.ts
57
+ import { createStudio } from '@joycostudio/studio/client'
58
+
59
+ export const studio = createStudio({
60
+ tools: [grading],
61
+ runtime: process.env.NODE_ENV === 'development' && 'ws://127.0.0.1:5179',
62
+ })
63
+ ```
64
+
65
+ ```ts
66
+ const tool = studio.tool(grading)
67
+ apply(tool.config) // a live object: read it in your render loop…
68
+ tool.subscribe(apply) // …or react to every change, local or remote
69
+ ```
70
+
71
+ Floating debug pane, with the same controls (plus save/discard when the runtime is up):
72
+
73
+ ```ts
74
+ import { everyFrame } from '@joycostudio/studio/client'
75
+ import { DebugManager } from '@joycostudio/xyz/debug'
76
+ import { Pane } from 'tweakpane'
77
+
78
+ const debug = new DebugManager({ createPane: () => new Pane(), binding: { registerFrame: everyFrame } })
79
+ for (const tool of studio.tools) debug.register(tool)
80
+ ```
81
+
82
+ The `/studio` route:
83
+
84
+ ```tsx
85
+ // app/studio/page.tsx
86
+ 'use client'
87
+ import { Studio } from '@joycostudio/studio/react'
88
+ import { studio } from '@/studio'
89
+
90
+ export default function StudioPage() {
91
+ return <Studio studio={studio} />
92
+ }
93
+ ```
94
+
95
+ In Studio, **Command+B** (Mac) or **Ctrl+B** slides both side panels out of view; press it again to bring them back.
96
+ This also works from the same-origin preview iframe. The sidebar's collapse/expand button controls its width separately,
97
+ and that choice is preserved when hiding and showing the panels. Move the pointer near either screen edge to reveal a small
98
+ tab; click it to bring both panels back. The tabs also appear on keyboard focus and stay visible on touch devices.
99
+ With reduced motion enabled, panels toggle instantly.
100
+
101
+ ### Jump to tools from anywhere
102
+
103
+ Mount `StudioCommandMenu` once in the host app's root layout so it stays available on every route, including `/studio`:
104
+
105
+ ```tsx
106
+ // src/command-menu.tsx
107
+ 'use client'
108
+ import '@joycostudio/studio/command-menu.css'
109
+ import { StudioCommandMenu } from '@joycostudio/studio/react'
110
+ import { studio } from './studio'
111
+
112
+ export function CommandMenu() {
113
+ return <StudioCommandMenu studio={studio} />
114
+ }
115
+ ```
116
+
117
+ Render `<CommandMenu />` alongside the layout's `{children}`. The host owns mounting it, just like the floating debug pane.
118
+ Its stylesheet is scoped to the menu and can be imported globally without applying Studio's page styles to the host app.
119
+
120
+ Press **Command+K** (Mac) or **Ctrl+K** to open or close it, type a tool name or ID to search, use **↑/↓** and **Enter** to
121
+ select, or **Escape** to dismiss. Clicking a result also opens it. Results link to `/studio#tool-id`; switching tools from
122
+ inside Studio changes the hash without reloading the page. The shortcut also works while focus is in Studio's same-origin
123
+ preview iframe, opening the menu above Studio rather than inside the preview.
124
+
125
+ Pass `studioPath="/custom/studio"` for another route. For client-side navigation with a host router, pass its navigation
126
+ function, for example `<StudioCommandMenu studio={studio} navigate={router.push} />` with Next.js `useRouter()`.
127
+ Without a router callback, navigation from another route uses the browser. The menu works without a runtime connection.
128
+
129
+ To run its browser tests, start the playground and run
130
+ `STUDIO_TEST_APP_URL=http://localhost:3000 pnpm --filter @joycostudio/studio exec vitest run test/command-menu.test.ts`.
131
+ Install Chromium first with `pnpm exec studio install-browser` if needed.
132
+
133
+ ## 4. Connect an MCP agent
134
+
135
+ Start the app with `studio dev -- <your dev command>` as above. Add a **Streamable HTTP** MCP server to your agent with
136
+ this URL:
137
+
138
+ ```text
139
+ http://127.0.0.1:5179/mcp
140
+ ```
141
+
142
+ Use the URL printed by the CLI when running on a different port. Each project's runtime needs its own port when several
143
+ projects run at once. The endpoint is local to the machine running Studio; a remote/cloud agent cannot reach it through
144
+ its own `localhost`.
145
+
146
+ The MCP endpoint shares the running app's drafts. It supports both the 2025 MCP initialization flow and the 2026 MCP
147
+ discovery flow, with no separate runtime or MCP process to launch.
148
+
149
+ | MCP tool | Arguments | Result |
150
+ | ------------------------ | ---------------------------------------------- | ---------------------------------------------------------------- |
151
+ | `studio_list_tools` | none | Registered IDs, titles, file paths, and preview routes |
152
+ | `studio_get_tool` | `tool` | Complete draft, revision, saved value, dirty and conflict status |
153
+ | `studio_update_tool` | `tool`, `revision`, `value` | Validated draft; all connected previews update immediately |
154
+ | `studio_save_tool` | `tool`, `revision`, optional `force` | Draft persisted to its registered JSON file |
155
+ | `studio_discard_tool` | `tool`, `revision` | Draft reset to the runtime's latest saved value |
156
+ | `studio_capture_preview` | `tool`, optional `revision`, `width`, `height` | PNG image and draft revision from a fresh preview page |
157
+
158
+ Read before editing, copy `data.draft.value`, change the intended fields, then pass the **complete config** and
159
+ `data.draft.revision` to `studio_update_tool`. Saving is separate. Use the new revision returned by the update to save or
160
+ discard. If the playground or another agent edits in between, a `stale_revision` error tells you to read again and reconcile
161
+ the changes. `force` bypasses a disk conflict, never a stale revision.
162
+
163
+ Every tool returns the runtime's `{ ok: true, data }` or `{ ok: false, error, message? }` as structured content and JSON text.
164
+ Runtime failures also set MCP's `isError`, so invalid configs, unknown tools, stale revisions, and disk conflicts remain
165
+ visible to the agent. Expected failures leave the draft intact.
166
+
167
+ ### Preview images
168
+
169
+ Set the running app's URL once in `studio.config.ts` (or override it with `studio dev --app-url <url>`):
170
+
171
+ ```ts
172
+ export default defineConfig({ tools: [grading], appUrl: 'http://localhost:3000' })
173
+ ```
174
+
175
+ Install Chromium once with `pnpm exec studio install-browser`. Then call `studio_capture_preview` with a tool ID.
176
+ The existing `preview` route must use the Studio client connected to this runtime. No capture code or readiness hooks
177
+ are needed in tool definitions, HTML components, or canvas renderers.
178
+
179
+ Capture starts a shared headless Chromium browser on first use and opens an isolated page for each request. It reads the
180
+ live unsaved draft, waits for the client to receive that revision, then waits for fonts, current HTML images, and two
181
+ animation frames. CSS animations are disabled for the screenshot. This is automatic settling, not a guarantee that
182
+ arbitrary async work, delayed UI, or WebGL assets have finished. A draft change during capture returns `stale_revision`
183
+ instead of an image. Simultaneous captures return `capture_busy`; retry after the active capture finishes.
184
+
185
+ The result includes a PNG as MCP image content and metadata with `tool`, `revision`, `url`, `width`, `height`, and
186
+ `settling: 'automatic'`. The viewport defaults to 1200×800 at 1× scale; width accepts 320–2560 and height accepts 240–1600.
187
+ The page starts fresh: it does not inherit your open tab's login, scroll, or interaction state. Preview routes and
188
+ top-level navigation must stay on the configured app origin. Pages that never connect or settle time out.
189
+
190
+ For the browser integration tests, run `pnpm exec studio install-browser` and
191
+ `pnpm --filter @joycostudio/studio test:capture`. The regular test suite runs setup validation without requiring Chromium.
192
+
193
+ The server binds to `127.0.0.1` by default and the MCP endpoint checks localhost Host headers and the same browser Origin
194
+ allowlist as the WebSocket bridge. Native agent clients do not need an Origin header. This is a local development endpoint
195
+ without authentication; do not expose it through a public proxy.
196
+
197
+ ## How it fits together
198
+
199
+ - **Runtime** (`studio dev`) is the only writer. It validates every update, keeps a draft revision per tool, writes atomically,
200
+ refuses paths outside the project (symlinks included), only accepts localhost browser origins, and watches files for
201
+ external edits (an external edit over unsaved work becomes a `conflict` that needs discard or force save).
202
+ - **Bridge**: WebSocket (`tools.list`, `tool.get`, `tool.update`, `tool.save`, `tool.discard` + `state` broadcasts) and HTTP MCP
203
+ on the same port. `/studio`, every app tab, and MCP all go through these same runtime operations.
204
+ - **Without a runtime** (production) tools stay local: the floating pane still tweaks, nothing persists.
@@ -0,0 +1,13 @@
1
+ // src/core/result.ts
2
+ var success = (data) => ({ ok: true, data });
3
+ var error = (code, message) => ({
4
+ ok: false,
5
+ error: code,
6
+ ...message === void 0 ? {} : { message }
7
+ });
8
+
9
+ export {
10
+ success,
11
+ error
12
+ };
13
+ //# sourceMappingURL=chunk-QKEAOM2U.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/result.ts"],"sourcesContent":["export type Success<T> = { ok: true; data: T }\n\nexport type Failure = {\n ok: false\n /** snake_case wire code, e.g. `invalid_config`. */\n error: string\n message?: string\n}\n\nexport type Result<T> = Success<T> | Failure\n\nexport const success = <T>(data: T): Success<T> => ({ ok: true, data })\n\nexport const error = (code: string, message?: string): Failure => ({\n ok: false,\n error: code,\n ...(message === undefined ? {} : { message }),\n})\n"],"mappings":";AAWO,IAAM,UAAU,CAAI,UAAyB,EAAE,IAAI,MAAM,KAAK;AAE9D,IAAM,QAAQ,CAAC,MAAc,aAA+B;AAAA,EACjE,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,GAAI,YAAY,SAAY,CAAC,IAAI,EAAE,QAAQ;AAC7C;","names":[]}
@@ -0,0 +1,75 @@
1
+ import {
2
+ error,
3
+ success
4
+ } from "./chunk-QKEAOM2U.js";
5
+
6
+ // src/core/validate.ts
7
+ function validateConfig(tool, candidate) {
8
+ const mismatch = shapeMismatch(tool.initial, candidate, "config");
9
+ if (mismatch) return error("invalid_config", mismatch);
10
+ const config = structuredClone(candidate);
11
+ const invalid = invalidControl(tool.controls(config));
12
+ return invalid ? error("invalid_config", invalid) : success(config);
13
+ }
14
+ function kind(value) {
15
+ if (value === null) return "null";
16
+ if (Array.isArray(value)) return "array";
17
+ return typeof value;
18
+ }
19
+ function shapeMismatch(reference, value, path) {
20
+ const expected = kind(reference);
21
+ if (kind(value) !== expected) return `${path}: expected ${expected}, got ${kind(value)}`;
22
+ if (expected === "number" && !Number.isFinite(value)) return `${path}: expected a finite number`;
23
+ if (expected === "array") {
24
+ const [first] = reference;
25
+ if (first === void 0) return null;
26
+ for (const [index, item] of value.entries()) {
27
+ const nested = shapeMismatch(first, item, `${path}[${index}]`);
28
+ if (nested) return nested;
29
+ }
30
+ }
31
+ if (expected === "object") {
32
+ const ref = reference;
33
+ const obj = value;
34
+ for (const key of Object.keys(obj)) if (!(key in ref)) return `${path}.${key}: unknown key`;
35
+ for (const key of Object.keys(ref)) {
36
+ if (!(key in obj)) return `${path}.${key}: missing`;
37
+ const nested = shapeMismatch(ref[key], obj[key], `${path}.${key}`);
38
+ if (nested) return nested;
39
+ }
40
+ }
41
+ return null;
42
+ }
43
+ var isRgb = (value) => typeof value === "object" && value !== null && ["r", "g", "b"].every((channel) => Number.isFinite(value[channel]));
44
+ function invalidControl(schema, path = "") {
45
+ for (const [key, field] of Object.entries(schema)) {
46
+ if (field.kind === "button") continue;
47
+ const name = `${path}${field.label ?? key}`;
48
+ if (field.kind === "folder") {
49
+ const nested = invalidControl(field.definition.schema(), `${name}/`);
50
+ if (nested) return nested;
51
+ continue;
52
+ }
53
+ const value = field.read();
54
+ if (field.kind === "slider") {
55
+ if (typeof value !== "number" || value < field.min || value > field.max)
56
+ return `${name}: expected a number in [${field.min}, ${field.max}], got ${JSON.stringify(value)}`;
57
+ } else if (field.kind === "bool") {
58
+ if (typeof value !== "boolean") return `${name}: expected a boolean`;
59
+ } else if (field.kind === "select") {
60
+ const options = Object.values(field.options);
61
+ if (!options.includes(value))
62
+ return `${name}: expected one of ${JSON.stringify(options)}, got ${JSON.stringify(value)}`;
63
+ } else if (field.kind === "color-string") {
64
+ if (typeof value !== "string") return `${name}: expected a color string`;
65
+ } else if (field.kind === "color") {
66
+ if (!isRgb(value)) return `${name}: expected an { r, g, b } color`;
67
+ }
68
+ }
69
+ return null;
70
+ }
71
+
72
+ export {
73
+ validateConfig
74
+ };
75
+ //# sourceMappingURL=chunk-VYNEG5Z7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/validate.ts"],"sourcesContent":["import type { DebugSchema } from '@joycostudio/xyz/debug'\nimport { error, success, type Result } from './result'\nimport type { ToolDefinition } from './tool'\n\n/**\n * Accept `candidate` as a config for `tool` only if it has the exact shape of\n * `tool.initial` and every control in `tool.controls` accepts its value.\n * Returns a detached copy, never the input.\n */\nexport function validateConfig<TConfig extends object>(\n tool: ToolDefinition<TConfig>,\n candidate: unknown,\n): Result<TConfig> {\n const mismatch = shapeMismatch(tool.initial, candidate, 'config')\n if (mismatch) return error('invalid_config', mismatch)\n const config = structuredClone(candidate) as TConfig\n const invalid = invalidControl(tool.controls(config))\n return invalid ? error('invalid_config', invalid) : success(config)\n}\n\nfunction kind(value: unknown) {\n if (value === null) return 'null'\n if (Array.isArray(value)) return 'array'\n return typeof value\n}\n\nfunction shapeMismatch(reference: unknown, value: unknown, path: string): string | null {\n const expected = kind(reference)\n if (kind(value) !== expected) return `${path}: expected ${expected}, got ${kind(value)}`\n if (expected === 'number' && !Number.isFinite(value)) return `${path}: expected a finite number`\n if (expected === 'array') {\n const [first] = reference as unknown[]\n if (first === undefined) return null\n for (const [index, item] of (value as unknown[]).entries()) {\n const nested = shapeMismatch(first, item, `${path}[${index}]`)\n if (nested) return nested\n }\n }\n if (expected === 'object') {\n const ref = reference as Record<string, unknown>\n const obj = value as Record<string, unknown>\n for (const key of Object.keys(obj)) if (!(key in ref)) return `${path}.${key}: unknown key`\n for (const key of Object.keys(ref)) {\n if (!(key in obj)) return `${path}.${key}: missing`\n const nested = shapeMismatch(ref[key], obj[key], `${path}.${key}`)\n if (nested) return nested\n }\n }\n return null\n}\n\nconst isRgb = (value: unknown) =>\n typeof value === 'object' &&\n value !== null &&\n ['r', 'g', 'b'].every((channel) => Number.isFinite((value as Record<string, unknown>)[channel]))\n\nfunction invalidControl(schema: DebugSchema, path = ''): string | null {\n for (const [key, field] of Object.entries(schema)) {\n if (field.kind === 'button') continue\n const name = `${path}${field.label ?? key}`\n if (field.kind === 'folder') {\n const nested = invalidControl(field.definition.schema(), `${name}/`)\n if (nested) return nested\n continue\n }\n const value: unknown = field.read()\n if (field.kind === 'slider') {\n if (typeof value !== 'number' || value < field.min || value > field.max)\n return `${name}: expected a number in [${field.min}, ${field.max}], got ${JSON.stringify(value)}`\n } else if (field.kind === 'bool') {\n if (typeof value !== 'boolean') return `${name}: expected a boolean`\n } else if (field.kind === 'select') {\n const options = Object.values(field.options)\n if (!options.includes(value as string | number))\n return `${name}: expected one of ${JSON.stringify(options)}, got ${JSON.stringify(value)}`\n } else if (field.kind === 'color-string') {\n if (typeof value !== 'string') return `${name}: expected a color string`\n } else if (field.kind === 'color') {\n if (!isRgb(value)) return `${name}: expected an { r, g, b } color`\n }\n }\n return null\n}\n"],"mappings":"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","names":[]}