@joycostudio/studio 0.1.5 → 0.1.6

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
@@ -258,6 +258,20 @@ Undo is per tool and per page: it restores only the values you changed, leaving
258
258
  in place, and syncs like any other edit. Text fields keep their native undo while focused. History is not persisted.
259
259
  `tool.undo()`, `tool.redo()`, `tool.canUndo`, and `tool.canRedo` expose the same history to host code.
260
260
 
261
+ The inspector footer summarizes unsaved changes on one line. Click it to expand the footer into the full list: each changed
262
+ value shows its saved and current value (with a swatch for colors) and a revert button that resets just that value. A revert
263
+ syncs like any edit and can be undone. The list collapses once nothing is left to save. `tool.diff` lists the same
264
+ `{ path, before, after }` changes, and `tool.revert(path)` resets one from host code.
265
+
266
+ To share a config with a teammate, click the link button in the inspector footer (or **Copy link** in the floating debug
267
+ pane). It copies the current page's URL with the tool's draft encoded in a `studio-share` query parameter; links from
268
+ Studio also select the tool. Opening the link applies that draft as an unsaved edit on top of whatever the recipient has,
269
+ with or without a runtime: it syncs to their previews, other tabs, and runtime like any edit, can be undone, and still needs
270
+ Save to reach the JSON file. The parameter is removed from the address bar once applied, so reloading doesn't reapply it.
271
+ A draft that no longer matches the tool's controls (for example, from another branch) is ignored with a console warning.
272
+ If a configured runtime is unreachable when the link opens, the draft applies locally and the runtime's draft wins once it
273
+ connects. `tool.shareUrl(base?)` builds the same link from host code.
274
+
261
275
  In Studio, **Command+B** (Mac) or **Ctrl+B** slides both side panels out of view; press it again to bring them back.
262
276
  This also works from the same-origin preview iframe. The sidebar's collapse/expand button controls its width separately,
263
277
  and that choice is preserved when hiding and showing the panels. Move the pointer near a hidden panel's screen edge to
@@ -19,9 +19,6 @@ function diff(before, after, path = []) {
19
19
  }
20
20
  return JSON.stringify(before) === JSON.stringify(after) ? [] : [{ path, before, after }];
21
21
  }
22
- function changedPaths(before, after) {
23
- return diff(before, after).map(({ path }) => path.join("."));
24
- }
25
22
 
26
23
  // src/client/bridge.ts
27
24
  function connectBridge(url, handlers) {
@@ -105,7 +102,8 @@ function connectBrowserSync(definition, runtime, handlers) {
105
102
  return {
106
103
  publish(value) {
107
104
  if (closed) return;
108
- latest = { type: "config", clock: ++clock, sender, value: structuredClone(value) };
105
+ clock = Math.max(clock + 1, Date.now());
106
+ latest = { type: "config", clock, sender, value: structuredClone(value) };
109
107
  channel.postMessage(latest);
110
108
  },
111
109
  close() {
@@ -209,6 +207,47 @@ function assignDeep(target, source) {
209
207
  }
210
208
  }
211
209
 
210
+ // src/client/share.ts
211
+ var PARAM = "studio-share";
212
+ var toBase64Url = (text) => {
213
+ let binary = "";
214
+ for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte);
215
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
216
+ };
217
+ var fromBase64Url = (data) => new TextDecoder().decode(
218
+ Uint8Array.from(atob(data.replace(/-/g, "+").replace(/_/g, "/")), (char) => char.charCodeAt(0))
219
+ );
220
+ function shareUrl(id, config, base = location.href) {
221
+ const url = new URL(base);
222
+ url.searchParams.set(PARAM, `${id}.${toBase64Url(JSON.stringify(config))}`);
223
+ return url.href;
224
+ }
225
+ function readShared(id) {
226
+ if (typeof window === "undefined" || !window.location?.search) return null;
227
+ const param = new URLSearchParams(location.search).get(PARAM);
228
+ if (!param?.startsWith(`${id}.`)) return null;
229
+ try {
230
+ return { value: JSON.parse(fromBase64Url(param.slice(id.length + 1))) };
231
+ } catch {
232
+ console.warn(`[studio] ${id}: ignoring a malformed share link`);
233
+ return null;
234
+ }
235
+ }
236
+ function clearShared() {
237
+ const url = new URL(location.href);
238
+ if (!url.searchParams.has(PARAM)) return;
239
+ url.searchParams.delete(PARAM);
240
+ history.replaceState(history.state, "", url);
241
+ }
242
+ async function copyLink(url) {
243
+ if (navigator.clipboard?.writeText) return navigator.clipboard.writeText(url);
244
+ window.prompt("Copy this link", url);
245
+ }
246
+ function afterLoad(apply) {
247
+ if (document.readyState === "complete") setTimeout(apply);
248
+ else addEventListener("load", () => setTimeout(apply), { once: true });
249
+ }
250
+
212
251
  // src/client/studio.ts
213
252
  var COMPARE_MESSAGE = "joyco-studio:compare";
214
253
  function postCompare(target, tool, showSaved) {
@@ -273,9 +312,11 @@ function createStudio(config, options = {}) {
273
312
  let scheduled = false;
274
313
  let saved = definition.initial;
275
314
  let stashed = null;
276
- const history = createHistory();
315
+ const history2 = createHistory();
277
316
  let snapshot = structuredClone(raw);
278
317
  const draft = () => stashed ?? raw;
318
+ let shared = readShared(definition.id);
319
+ let closed = false;
279
320
  const changed = () => {
280
321
  snapshot = structuredClone(raw);
281
322
  handle.version++;
@@ -325,8 +366,28 @@ function createStudio(config, options = {}) {
325
366
  };
326
367
  const edited = () => {
327
368
  if (stashed) return changed();
328
- history.record(snapshot, raw);
369
+ history2.record(snapshot, raw);
370
+ changed();
371
+ sync();
372
+ };
373
+ const hydrate = () => {
374
+ if (!shared) return;
375
+ const { value } = shared;
376
+ shared = null;
377
+ afterLoad(() => {
378
+ if (closed) return;
379
+ clearShared();
380
+ const result = validateConfig(definition, value);
381
+ if (!result.ok) return console.warn(`[studio] ${definition.id}: ignoring shared config: ${result.message}`);
382
+ handle.compare(false);
383
+ commit(() => assignDeep(raw, result.data));
384
+ });
385
+ };
386
+ const commit = (apply) => {
387
+ const before = snapshot;
388
+ apply();
329
389
  changed();
390
+ history2.record(before, raw, { merge: false });
330
391
  sync();
331
392
  };
332
393
  const restore = (patch) => {
@@ -344,8 +405,11 @@ function createStudio(config, options = {}) {
344
405
  get saved() {
345
406
  return saved;
346
407
  },
408
+ get diff() {
409
+ return diff(saved, draft());
410
+ },
347
411
  get changes() {
348
- return changedPaths(saved, draft());
412
+ return handle.diff.map(({ path }) => path.join("."));
349
413
  },
350
414
  get dirty() {
351
415
  return handle.changes.length > 0;
@@ -361,13 +425,13 @@ function createStudio(config, options = {}) {
361
425
  changed();
362
426
  },
363
427
  get canUndo() {
364
- return history.canUndo;
428
+ return history2.canUndo;
365
429
  },
366
430
  get canRedo() {
367
- return history.canRedo;
431
+ return history2.canRedo;
368
432
  },
369
- undo: () => !stashed && restore(history.undo()),
370
- redo: () => !stashed && restore(history.redo()),
433
+ undo: () => !stashed && restore(history2.undo()),
434
+ redo: () => !stashed && restore(history2.redo()),
371
435
  subscribe(listener) {
372
436
  listeners.add(listener);
373
437
  return () => void listeners.delete(listener);
@@ -376,7 +440,8 @@ function createStudio(config, options = {}) {
376
440
  debug() {
377
441
  const actions = {
378
442
  ...runtime ? { save: button("Save", () => handle.save().then(fail), "Saved \u2713") } : {},
379
- discard: button("Discard", () => void handle.discard(), "Discarded")
443
+ discard: button("Discard", () => void handle.discard(), "Discarded"),
444
+ share: button("Copy link", () => copyLink(handle.shareUrl()), "Copied \u2713")
380
445
  };
381
446
  return {
382
447
  title: definition.title ?? definition.id,
@@ -397,22 +462,39 @@ function createStudio(config, options = {}) {
397
462
  if (!handle.dirty) return false;
398
463
  const before = snapshot;
399
464
  replace(saved);
400
- if (!stashed) history.record(before, raw, { merge: false });
465
+ if (!stashed) history2.record(before, raw, { merge: false });
401
466
  sync();
402
467
  return true;
403
468
  },
404
- /** Connection changes: the runtime's draft wins on (re)connect; without one there is no runtime state. */
469
+ revert(path) {
470
+ const change = handle.diff.find(
471
+ (change2) => typeof path === "string" ? change2.path.join(".") === path : change2.path.length === path.length && change2.path.every((key, i) => key === path[i])
472
+ );
473
+ if (stashed || !change) return false;
474
+ commit(() => writePath(raw, change.path, change.before));
475
+ return true;
476
+ },
477
+ shareUrl: (base) => shareUrl(definition.id, draft(), base),
478
+ /**
479
+ * Connection changes: the runtime's draft wins on (re)connect; without one there is no runtime state.
480
+ * A share link applies on top of the runtime's draft, or locally once the runtime proves unreachable.
481
+ */
405
482
  connection() {
406
- if (runtimeOwnsDrafts()) return void resync();
483
+ if (runtimeOwnsDrafts()) return void resync().then(hydrate);
407
484
  handle.state = null;
485
+ if (connection.phase === "offline") hydrate();
408
486
  },
409
487
  receive,
410
- close: () => browserSync?.close()
488
+ close() {
489
+ closed = true;
490
+ browserSync?.close();
491
+ }
411
492
  };
412
493
  const browserSync = connectBrowserSync(definition, runtime, {
413
494
  enabled: () => !runtimeOwnsDrafts(),
414
495
  receive: replace
415
496
  });
497
+ if (!runtime) hydrate();
416
498
  return handle;
417
499
  };
418
500
  const handles = new Map(tools.filter(hasConfig).map((definition) => [definition.id, createHandle(definition)]));
@@ -473,4 +555,4 @@ export {
473
555
  everyFrame,
474
556
  createStudio
475
557
  };
476
- //# sourceMappingURL=chunk-ZH3NEN4V.js.map
558
+ //# sourceMappingURL=chunk-6KZJEKOL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client/studio.ts","../src/core/diff.ts","../src/client/bridge.ts","../src/client/browser-sync.ts","../src/client/history.ts","../src/client/live.ts","../src/client/share.ts"],"sourcesContent":["import {\n button,\n type DebugDefinition,\n type DebugField,\n type DebugFolder,\n type DebugSchema,\n schemaValues,\n} from '@joycostudio/xyz/debug'\nimport { diff, type Change } from '../core/diff'\nimport type { ToolState } from '../core/protocol'\nimport { error, type Result } from '../core/result'\nimport { hasConfig, type StudioTool, type ToolDefinition, type ToolReference } from '../core/tool'\nimport { validateConfig } from '../core/validate'\nimport { connectBridge, type BridgeClient, type Connection } from './bridge'\nimport { connectBrowserSync } from './browser-sync'\nimport { createHistory, writePath } from './history'\nimport { assignDeep, live } from './live'\nimport { afterLoad, clearShared, copyLink, readShared, shareUrl } from './share'\n\nexport type ToolHandle<TConfig extends object = any> = {\n readonly definition: ToolDefinition<TConfig>\n /** The live config. Writes sync to same-origin pages and, when connected, the runtime. */\n readonly config: TConfig\n /** Runtime-owned state; null while no runtime is connected or until it answers. */\n readonly state: ToolState<TConfig> | null\n /** Bumps on every config change. Handy as a React snapshot. */\n readonly version: number\n /** Called after every config change, local or remote. */\n subscribe(listener: (config: TConfig) => void): () => void\n /** The tool's Xyz schema bound to the live config. */\n controls(): DebugSchema\n /** Xyz `Debuggable`: `debugManager.register(handle)` mounts this tool in the app's floating pane. */\n debug(): DebugFolder\n /** The config on disk: the runtime's last reported saved value or, until one answers, the bundled `initial`. */\n readonly saved: TConfig\n /** Dotted paths where the draft differs from `saved`. Tracked locally, so it works without a runtime too. */\n readonly changes: string[]\n /** Each leaf where the draft differs from `saved`: `before` is the saved value, `after` the draft's. */\n readonly diff: Change[]\n /** The draft differs from `saved`. */\n readonly dirty: boolean\n /** True while `compare(true)` shows the saved config in place of the draft. */\n readonly comparing: boolean\n /**\n * Temporarily show `saved` (true) or go back to the draft (false). Local only: nothing\n * reaches the runtime, and edits made while comparing are dropped when it ends.\n */\n compare(showSaved: boolean): void\n /** True when `undo()` has an edit made on this page to step back through. */\n readonly canUndo: boolean\n readonly canRedo: boolean\n /**\n * Step back through edits made on this page (a slider drag counts as one), then sync the result like any\n * other edit. Only the leaves each edit changed are restored. Returns false with nothing to undo or while comparing.\n */\n undo(): boolean\n redo(): boolean\n /** Persist the draft. Needs a connected runtime. */\n save(options?: { force?: boolean }): Promise<Result<ToolState<TConfig>>>\n /**\n * Reset the draft to `saved`, with or without a runtime, then sync it like any other edit.\n * Undoable as its own step. Returns false when there is nothing to discard.\n */\n discard(): boolean\n /**\n * Reset one changed leaf (a dotted path from `changes`, or its keys, which also address keys containing dots) to its saved value, then sync it like any\n * other edit. Undoable as its own step. Returns false when that leaf is unchanged or while comparing.\n */\n revert(path: string | readonly string[]): boolean\n /**\n * A link to `base` (the current page by default) carrying the draft. Opening it on another machine applies\n * that draft as an unsaved, undoable edit; a draft that no longer fits the tool's controls is ignored.\n */\n shareUrl(base?: string | URL): string\n}\n\nexport type Studio = {\n /** Every registered tool in order, including preview-only ones. */\n readonly definitions: readonly StudioTool[]\n /** A handle per tool with a config. Preview-only tools have none. */\n readonly tools: readonly ToolHandle[]\n tool<TConfig extends object>(definition: ToolReference<TConfig>): ToolHandle<TConfig>\n readonly connection: Connection\n /** Bumps on connection or any tool state change. */\n readonly version: number\n subscribe(listener: () => void): () => void\n close(): void\n}\n\nexport type StudioOptions = {\n tools: readonly StudioTool[]\n /**\n * Runtime bridge URL (e.g. `ws://127.0.0.1:5179`). Omit or pass false for browser-only syncing, as in production.\n * Only browsers connect, so a module-scope `createStudio` is safe to evaluate during SSR.\n */\n runtime?: string | false\n}\n\nconst COMPARE_MESSAGE = 'joyco-studio:compare'\ntype CompareMessage = { type: typeof COMPARE_MESSAGE; tool: string; showSaved: boolean }\n\n/** Make the studio running in `target` (e.g. a preview iframe) compare `tool` as well. */\nexport function postCompare(target: Window | null | undefined, tool: string, showSaved: boolean) {\n const message: CompareMessage = { type: COMPARE_MESSAGE, tool, showSaved }\n target?.postMessage(message, location.origin)\n}\n\n/** Mark every value control live so a pane registered with `registerFrame` reflects remote edits. */\nfunction liveSchema(schema: DebugSchema): DebugSchema {\n return Object.fromEntries(\n Object.entries(schema).map(([key, field]): [string, DebugField] => {\n if (field.kind === 'button') return [key, field]\n if (field.kind === 'folder')\n return [\n key,\n { ...field, definition: { ...field.definition, schema: () => liveSchema(field.definition.schema()) } },\n ]\n return [key, { ...field, live: true }]\n }),\n )\n}\n\n/**\n * Xyz `extend` for a folder bound to `handle.controls()` (optionally followed by more fields, as in `debug()`):\n * double-clicking a slider resets it to its saved value.\n */\nexport function resetSlidersOnDoubleClick(handle: ToolHandle): NonNullable<DebugDefinition['extend']> {\n return (folder, { fields }) => {\n const sliders = fields().filter(({ field }) => field.kind === 'slider')\n const onDoubleClick = (event: MouseEvent) => {\n // Double-clicking the number input selects its text instead.\n if (handle.comparing || (event.target as Element).closest('input')) return\n const slider = sliders.find(({ blade }) => blade.element.contains(event.target as Node))\n if (slider?.field.kind !== 'slider') return\n const saved = slider.path.reduce<unknown>(\n (values, key) => (values as Record<string, unknown> | undefined)?.[key],\n schemaValues(handle.definition.controls(structuredClone(handle.saved))),\n )\n if (typeof saved === 'number') slider.field.write(saved)\n }\n folder.element.addEventListener('dblclick', onDoubleClick)\n return () => folder.element.removeEventListener('dblclick', onDoubleClick)\n }\n}\n\n/** A `registerFrame` for Xyz's `DebugManager` / `bindDebugSchema`: refreshes live controls every animation frame. */\nexport function everyFrame(refresh: () => void) {\n let frame = requestAnimationFrame(function tick() {\n refresh()\n frame = requestAnimationFrame(tick)\n })\n return () => cancelAnimationFrame(frame)\n}\n\n/** One studio per runtime URL (or local tool set) per page; replace HMR instances without leaking transports. */\nconst instances: Map<string, Studio> = ((globalThis as { __joycoStudio?: Map<string, Studio> }).__joycoStudio ??=\n new Map())\n\nexport function createStudio(config: StudioOptions, options: Pick<StudioOptions, 'runtime'> = {}): Studio {\n const { tools } = config\n const runtime = options.runtime ?? config.runtime\n const instanceKey = runtime || JSON.stringify(tools.map(({ id, file }) => [id, file]).sort())\n let bridge: BridgeClient | null = null\n let connection: Connection = { phase: runtime ? 'connecting' : 'local' }\n let version = 0\n const studioListeners = new Set<() => void>()\n const touch = () => {\n version++\n for (const listener of studioListeners) listener()\n }\n\n /**\n * Who owns drafts: the runtime while connected, otherwise same-origin pages share them over BroadcastChannel.\n * The only place a draft change depends on the connection; reconnecting hands ownership back to the runtime.\n */\n const runtimeOwnsDrafts = () => connection.phase === 'connected'\n\n const createHandle = <TConfig extends object>(definition: ToolDefinition<TConfig>) => {\n const raw = structuredClone(definition.initial)\n const listeners = new Set<(config: TConfig) => void>()\n let pushing: Promise<unknown> = Promise.resolve()\n let scheduled = false\n /** The last saved config the runtime reported, kept while it is away; the bundled `initial` until one answers. */\n let saved: TConfig = definition.initial\n /** The draft to restore while comparing; null when showing the draft. */\n let stashed: TConfig | null = null\n const history = createHistory()\n /** A copy of `raw` as of the last change, so the next local edit can be diffed against it. */\n let snapshot = structuredClone(raw)\n /** The draft, even while `raw` shows the saved config for comparing. */\n const draft = () => stashed ?? raw\n /** A draft from this page's share link, applied once the runtime (if any) has had its say. */\n let shared = readShared(definition.id)\n let closed = false\n\n const changed = () => {\n snapshot = structuredClone(raw)\n handle.version++\n for (const listener of listeners) listener(handle.config)\n touch()\n }\n const adopt = (state: ToolState) => {\n handle.state = state as ToolState<TConfig>\n saved = state.saved.value as TConfig\n touch()\n }\n /** Replace the whole draft. While comparing, the shown saved config stays put; `changes` reads the new draft. */\n const replace = (value: TConfig) => {\n if (stashed) {\n stashed = structuredClone(value)\n touch()\n } else {\n assignDeep(raw as Record<string, unknown>, value as Record<string, unknown>)\n changed()\n }\n }\n const push = () => {\n if (scheduled || !bridge) return\n scheduled = true\n pushing = Promise.resolve().then(async () => {\n scheduled = false\n const result = await bridge?.request('tool.update', { tool: definition.id, value: draft() })\n if (!result) return\n if (result.ok) adopt(result.data)\n else if (result.error !== 'runtime_unavailable') {\n console.warn(`[studio] ${definition.id}: ${result.message ?? result.error}`)\n await resync()\n }\n })\n }\n /**\n * Send a local draft change on: to the runtime while it owns drafts. Same-origin pages always get a copy,\n * so one opened after the runtime stops starts from it, but they only read it while no runtime is connected.\n */\n const sync = () => {\n browserSync?.publish(draft())\n if (runtimeOwnsDrafts()) push()\n }\n /** The runtime's state, from a broadcast or a reply. */\n const receive = (state: ToolState) => {\n adopt(state)\n replace(state.draft.value as TConfig)\n browserSync?.publish(draft())\n }\n const resync = async () => {\n const result = await bridge?.request('tool.get', { tool: definition.id })\n if (result?.ok) receive(result.data)\n }\n /** Every local change to the live config. */\n const edited = () => {\n // Edits made while comparing stay on this page and are dropped when it ends.\n if (stashed) return changed()\n history.record(snapshot, raw)\n changed()\n sync()\n }\n /** Apply the share link as a local edit, so it syncs and undoes like one. Runs once. */\n const hydrate = () => {\n if (!shared) return\n const { value } = shared\n shared = null\n afterLoad(() => {\n if (closed) return\n clearShared()\n const result = validateConfig(definition, value)\n if (!result.ok) return console.warn(`[studio] ${definition.id}: ignoring shared config: ${result.message}`)\n handle.compare(false)\n commit(() => assignDeep(raw as Record<string, unknown>, result.data as Record<string, unknown>))\n })\n }\n /** Apply a change to the draft as an undo step of its own, never merged with the edits around it. */\n const commit = (apply: () => void) => {\n const before = snapshot\n apply()\n changed()\n history.record(before, raw, { merge: false })\n sync()\n }\n const restore = (patch: ReturnType<typeof history.undo>) => {\n if (!patch) return false\n for (const { path, value } of patch) writePath(raw as Record<string, unknown>, path, value)\n changed()\n sync()\n return true\n }\n\n const handle = {\n definition,\n config: live(raw, edited),\n state: null as ToolState<TConfig> | null,\n version: 0,\n get saved(): TConfig {\n return saved\n },\n get diff() {\n return diff(saved, draft())\n },\n get changes() {\n return handle.diff.map(({ path }) => path.join('.'))\n },\n get dirty() {\n return handle.changes.length > 0\n },\n comparing: false,\n compare(showSaved: boolean) {\n if (showSaved === handle.comparing) return\n if (showSaved) stashed = structuredClone(raw)\n const next = showSaved ? saved : stashed!\n assignDeep(raw as Record<string, unknown>, next as Record<string, unknown>)\n if (!showSaved) stashed = null\n handle.comparing = showSaved\n changed()\n },\n get canUndo() {\n return history.canUndo\n },\n get canRedo() {\n return history.canRedo\n },\n undo: () => !stashed && restore(history.undo()),\n redo: () => !stashed && restore(history.redo()),\n subscribe(listener: (config: TConfig) => void) {\n listeners.add(listener)\n return () => void listeners.delete(listener)\n },\n controls: () => definition.controls(handle.config),\n debug(): DebugFolder {\n const actions: DebugSchema = {\n ...(runtime ? { save: button('Save', () => handle.save().then(fail), 'Saved ✓') } : {}),\n discard: button('Discard', () => void handle.discard(), 'Discarded'),\n share: button('Copy link', () => copyLink(handle.shareUrl()), 'Copied ✓'),\n }\n return {\n title: definition.title ?? definition.id,\n schema: () => ({ ...liveSchema(handle.controls()), ...actions }),\n extend: resetSlidersOnDoubleClick(handle),\n }\n },\n async save({ force = false } = {}) {\n if (!bridge) return error('runtime_unavailable')\n await pushing\n const revision = handle.state?.draft.revision\n if (revision === undefined) return error('runtime_unavailable')\n const result = await bridge.request('tool.save', { tool: definition.id, revision, force })\n if (result.ok) adopt(result.data)\n return result as Result<ToolState<TConfig>>\n },\n discard() {\n if (!handle.dirty) return false\n const before = snapshot\n replace(saved)\n // Undoable, as a step of its own.\n if (!stashed) history.record(before, raw, { merge: false })\n sync()\n return true\n },\n revert(path: string | readonly string[]) {\n const change = handle.diff.find((change) =>\n typeof path === 'string'\n ? change.path.join('.') === path\n : change.path.length === path.length && change.path.every((key, i) => key === path[i]),\n )\n if (stashed || !change) return false\n commit(() => writePath(raw as Record<string, unknown>, change.path, change.before))\n return true\n },\n shareUrl: (base?: string | URL) => shareUrl(definition.id, draft(), base),\n /**\n * Connection changes: the runtime's draft wins on (re)connect; without one there is no runtime state.\n * A share link applies on top of the runtime's draft, or locally once the runtime proves unreachable.\n */\n connection() {\n if (runtimeOwnsDrafts()) return void resync().then(hydrate)\n handle.state = null\n if (connection.phase === 'offline') hydrate()\n },\n receive,\n close() {\n closed = true\n browserSync?.close()\n },\n }\n const browserSync = connectBrowserSync(definition, runtime, {\n enabled: () => !runtimeOwnsDrafts(),\n receive: replace,\n })\n if (!runtime) hydrate()\n return handle\n }\n\n const handles = new Map(tools.filter(hasConfig).map((definition) => [definition.id, createHandle(definition)]))\n\n // /studio asks its same-origin preview iframe to compare too (see `postCompare`).\n const onMessage = (event: MessageEvent) => {\n const message = event.data as CompareMessage | undefined\n if (event.origin !== location.origin || message?.type !== COMPARE_MESSAGE) return\n handles.get(message.tool)?.compare(message.showSaved)\n }\n if (typeof window !== 'undefined') window.addEventListener?.('message', onMessage)\n\n if (runtime && typeof window !== 'undefined') {\n bridge = connectBridge(runtime, {\n state: (state) => handles.get(state.tool)?.receive(state),\n connection(next) {\n connection = next\n for (const handle of handles.values()) handle.connection()\n touch()\n },\n })\n }\n\n const studio: Studio = {\n definitions: tools,\n tools: [...handles.values()],\n tool<TConfig extends object>(definition: ToolDefinition<TConfig>) {\n const handle = handles.get(definition.id)\n if (!handle) throw new Error(`[studio] tool \"${definition.id}\" was not passed to createStudio`)\n return handle as unknown as ToolHandle<TConfig>\n },\n get connection() {\n return connection\n },\n get version() {\n return version\n },\n subscribe(listener) {\n studioListeners.add(listener)\n return () => void studioListeners.delete(listener)\n },\n close() {\n bridge?.close()\n for (const handle of handles.values()) handle.close()\n studioListeners.clear()\n if (typeof window !== 'undefined') window.removeEventListener?.('message', onMessage)\n if (instances.get(instanceKey) === studio) instances.delete(instanceKey)\n },\n }\n if (typeof window !== 'undefined') {\n instances.get(instanceKey)?.close()\n instances.set(instanceKey, studio)\n }\n return studio\n}\n\nfunction fail(result: Result<unknown>) {\n if (!result.ok) throw new Error(result.message ?? result.error)\n}\n","export type Change = { path: string[]; before: unknown; after: unknown }\n\n/** Every leaf that differs between two configs of the same shape. Arrays count as leaves. */\nexport function diff(before: unknown, after: unknown, path: string[] = []): Change[] {\n const nested = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n if (nested(before) && nested(after)) {\n const keys = new Set([...Object.keys(before), ...Object.keys(after)])\n return [...keys].flatMap((key) => diff(before[key], after[key], [...path, key]))\n }\n return JSON.stringify(before) === JSON.stringify(after) ? [] : [{ path, before, after }]\n}\n","import type { Op, Operations, ServerMessage, ToolState } from '../core/protocol'\nimport { error, type Result } from '../core/result'\n\nexport type Connection = { phase: 'local' } | { phase: 'connecting' } | { phase: 'connected' } | { phase: 'offline' }\n\ntype Pending = (result: Result<unknown>) => void\n\n/** A reconnecting WebSocket to the Studio runtime with request/response correlation. */\nexport function connectBridge(\n url: string,\n handlers: { state: (state: ToolState) => void; connection: (connection: Connection) => void },\n) {\n let socket: WebSocket | null = null\n let nextId = 0\n let retry = 0\n let timer: ReturnType<typeof setTimeout> | undefined\n let closed = false\n const pending = new Map<number, Pending>()\n\n const open = () => {\n handlers.connection({ phase: 'connecting' })\n const ws = new WebSocket(url)\n socket = ws\n ws.onopen = () => {\n retry = 0\n handlers.connection({ phase: 'connected' })\n }\n ws.onmessage = (event) => {\n const message = JSON.parse(String(event.data)) as ServerMessage\n if (message.type === 'state') return handlers.state(message.state)\n pending.get(message.id)?.(message.result)\n pending.delete(message.id)\n }\n ws.onclose = () => {\n socket = null\n for (const resolve of pending.values()) resolve(error('runtime_unavailable'))\n pending.clear()\n if (closed) return\n handlers.connection({ phase: 'offline' })\n timer = setTimeout(open, Math.min(500 * 2 ** retry++, 5000))\n }\n }\n open()\n\n return {\n request<TOp extends Op>(op: TOp, params: Operations[TOp]['params']): Promise<Result<Operations[TOp]['result']>> {\n if (socket?.readyState !== WebSocket.OPEN) return Promise.resolve(error('runtime_unavailable'))\n const id = nextId++\n socket.send(JSON.stringify({ id, op, params }))\n return new Promise((resolve) => pending.set(id, resolve as Pending))\n },\n close() {\n closed = true\n clearTimeout(timer)\n socket?.close()\n },\n }\n}\n\nexport type BridgeClient = ReturnType<typeof connectBridge>\n","import type { ToolDefinition } from '../core/tool'\nimport { validateConfig } from '../core/validate'\n\ntype Snapshot = { type: 'config'; clock: number; sender: string; value: unknown }\n\n/** Ephemeral config sharing between same-origin pages, including Studio's preview iframe. */\nexport function connectBrowserSync<T extends object>(\n definition: ToolDefinition<T>,\n runtime: string | false | undefined,\n handlers: { enabled(): boolean; receive(value: T): void },\n) {\n // Also exclude SSR and Node environments that expose their own BroadcastChannel implementation.\n if (typeof window === 'undefined' || !window.location?.origin || !window.BroadcastChannel) return null\n\n let channel: BroadcastChannel\n try {\n channel = new window.BroadcastChannel(\n JSON.stringify(['joyco-studio:config', runtime || null, definition.id, definition.file]),\n )\n } catch {\n return null\n }\n const sender = crypto.getRandomValues(new Uint32Array(4)).join('-')\n let clock = 0\n let latest: Snapshot | undefined\n let closed = false\n\n channel.onmessage = ({ data }) => {\n if (closed || !data || typeof data !== 'object') return\n if (data.type === 'request') {\n if (latest) channel.postMessage(latest)\n return\n }\n if (\n data.type !== 'config' ||\n !Number.isSafeInteger(data.clock) ||\n data.clock < 1 ||\n typeof data.sender !== 'string'\n )\n return\n clock = Math.max(clock, data.clock)\n if (!handlers.enabled()) return\n // A stable tie-break keeps simultaneous edits and multiple snapshot replies from diverging.\n if (latest && (data.clock < latest.clock || (data.clock === latest.clock && data.sender <= latest.sender))) return\n const result = validateConfig(definition, data.value)\n if (!result.ok) return\n latest = { type: 'config', clock: data.clock, sender: data.sender, value: result.data }\n handlers.receive(result.data)\n }\n // A newly mounted/reloaded preview asks existing peers for their current draft.\n channel.postMessage({ type: 'request' })\n\n return {\n publish(value: T) {\n if (closed) return\n // Wall time as a floor (peers share this browser's clock), so an edit made on a page that just opened,\n // such as an applied share link, outranks older drafts it hasn't heard about yet.\n clock = Math.max(clock + 1, Date.now())\n latest = { type: 'config', clock, sender, value: structuredClone(value) }\n channel.postMessage(latest)\n },\n close() {\n closed = true\n channel.onmessage = null\n channel.close()\n },\n }\n}\n","import { diff, type Change } from '../core/diff'\n\ntype Entry = { changes: Change[]; key: string; at: number }\n\nconst LIMIT = 100\n/** Edits to the same leaves this close together (a slider drag) undo as one step. */\nconst MERGE_MS = 500\n\n/**\n * Undo/redo for one page's edits. Entries hold only the leaves an edit changed, so stepping back\n * never reverts what another page, the runtime, or an agent changed in between.\n */\nexport function createHistory() {\n const past: Entry[] = []\n let future: Entry[] = []\n /** The newest entry while later edits to the same leaves may still extend it. */\n let open: Entry | null = null\n\n const step = (from: Entry[], to: Entry[], side: 'before' | 'after') => {\n const entry = from.pop()\n if (!entry) return null\n to.push(entry)\n open = null\n return entry.changes.map((change) => ({ path: change.path, value: change[side] }))\n }\n\n return {\n get canUndo() {\n return past.length > 0\n },\n get canRedo() {\n return future.length > 0\n },\n /** Record the edit between two snapshots. `merge: false` keeps it a step of its own (e.g. Discard). */\n record(before: unknown, after: unknown, { merge = true } = {}) {\n // Cloned so later in-place edits (e.g. to an array leaf) cannot rewrite history.\n const changes = structuredClone(diff(before, after))\n if (!changes.length) return\n future = []\n const key = JSON.stringify(changes.map(({ path }) => path))\n const now = Date.now()\n if (merge && open && open.key === key && now - open.at < MERGE_MS) {\n open.changes = open.changes.map((change, i) => ({ ...change, after: changes[i]!.after }))\n open.at = now\n // Dragged back to where it started: nothing left to undo.\n if (open.changes.every((change) => JSON.stringify(change.before) === JSON.stringify(change.after))) {\n past.pop()\n open = null\n }\n return\n }\n const entry = { changes, key, at: now }\n past.push(entry)\n if (past.length > LIMIT) past.shift()\n open = merge ? entry : null\n },\n /** The leaves to write back, or null when there is nothing to undo. */\n undo: () => step(past, future, 'before'),\n redo: () => step(future, past, 'after'),\n }\n}\n\n/** Write `value` at `path` in place; undefined removes the key (it did not exist before the edit). */\nexport function writePath(target: Record<string, unknown>, path: string[], value: unknown) {\n const key = path.at(-1)!\n let parent = target\n for (const segment of path.slice(0, -1)) {\n const next = parent[segment]\n parent = (typeof next === 'object' && next !== null ? next : (parent[segment] = {})) as Record<string, unknown>\n }\n if (value === undefined) delete parent[key]\n else parent[key] = structuredClone(value)\n}\n","/**\n * A deep proxy that reports every write, so plain `property(config, 'key')`\n * bindings from Xyz feed Studio without the tool author wiring callbacks.\n */\nexport function live<T extends object>(target: T, onWrite: () => void): T {\n const proxies = new WeakMap<object, object>()\n const wrap = <O extends object>(object: O): O => {\n let proxy = proxies.get(object)\n if (!proxy) {\n proxy = new Proxy(object, {\n get(target, key, receiver) {\n const value = Reflect.get(target, key, receiver)\n return typeof value === 'object' && value !== null ? wrap(value) : value\n },\n set(target, key, value, receiver) {\n const changed = Reflect.get(target, key, receiver) !== value\n const ok = Reflect.set(target, key, value, receiver)\n if (changed) onWrite()\n return ok\n },\n })\n proxies.set(object, proxy)\n }\n return proxy as O\n }\n return wrap(target)\n}\n\n/**\n * Make `target` equal `source` in place, keeping nested object identities (and every binding to them) intact.\n * Keys `source` lacks are removed.\n */\nexport function assignDeep(target: Record<string, unknown>, source: Record<string, unknown>) {\n for (const key of Object.keys(target)) if (!(key in source)) delete target[key]\n for (const [key, value] of Object.entries(source)) {\n const current = target[key]\n const nested = (v: unknown) => typeof v === 'object' && v !== null && !Array.isArray(v)\n if (nested(current) && nested(value))\n assignDeep(current as Record<string, unknown>, value as Record<string, unknown>)\n else target[key] = structuredClone(value)\n }\n}\n","/**\n * Share links carry one tool's draft in the URL: `?studio-share=<tool id>.<base64url JSON>`.\n * Opening one applies it as an unsaved edit, so teammates can review, save, or discard it.\n */\nconst PARAM = 'studio-share'\n\nconst toBase64Url = (text: string) => {\n // Byte by byte: spreading a large config into `String.fromCharCode` would exceed the argument limit.\n let binary = ''\n for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte)\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\nconst fromBase64Url = (data: string) =>\n new TextDecoder().decode(\n Uint8Array.from(atob(data.replace(/-/g, '+').replace(/_/g, '/')), (char) => char.charCodeAt(0)),\n )\n\n/** `base` (the current page by default) with `config` for tool `id` encoded into it. */\nexport function shareUrl(id: string, config: unknown, base: string | URL = location.href) {\n const url = new URL(base)\n url.searchParams.set(PARAM, `${id}.${toBase64Url(JSON.stringify(config))}`)\n return url.href\n}\n\n/** The config this page's URL shares for tool `id`, if any. Unvalidated. */\nexport function readShared(id: string): { value: unknown } | null {\n if (typeof window === 'undefined' || !window.location?.search) return null\n const param = new URLSearchParams(location.search).get(PARAM)\n if (!param?.startsWith(`${id}.`)) return null\n try {\n return { value: JSON.parse(fromBase64Url(param.slice(id.length + 1))) }\n } catch {\n console.warn(`[studio] ${id}: ignoring a malformed share link`)\n return null\n }\n}\n\n/** Drop the share from the address bar, so reloading after further edits doesn't reapply it. */\nexport function clearShared() {\n const url = new URL(location.href)\n if (!url.searchParams.has(PARAM)) return\n url.searchParams.delete(PARAM)\n history.replaceState(history.state, '', url)\n}\n\n/** Copy a share link. Plain-HTTP origins other than localhost have no clipboard API, so show it to copy by hand. */\nexport async function copyLink(url: string) {\n if (navigator.clipboard?.writeText) return navigator.clipboard.writeText(url)\n window.prompt('Copy this link', url)\n}\n\n/**\n * Run `apply` once the page has loaded. Routers such as Next.js rewrite the URL while hydrating,\n * which would restore a share parameter cleared any earlier.\n */\nexport function afterLoad(apply: () => void) {\n if (document.readyState === 'complete') setTimeout(apply)\n else addEventListener('load', () => setTimeout(apply), { once: true })\n}\n"],"mappings":"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","names":["target","history","change"]}
package/dist/cli.js CHANGED
@@ -970,7 +970,7 @@ import { parseArgs } from "util";
970
970
  // package.json
971
971
  var package_default = {
972
972
  name: "@joycostudio/studio",
973
- version: "0.1.5",
973
+ version: "0.1.6",
974
974
  description: "Plug project-specific tools into a /studio route, a floating debug pane, and MCP agents.",
975
975
  type: "module",
976
976
  license: "ISC",