dsh-webui-studio 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/PRODUCT.md +11 -7
  2. package/README.md +69 -21
  3. package/README.zh-CN.md +64 -20
  4. package/dist/bridge.js +10 -10
  5. package/dist/studio.css +1 -1
  6. package/dist/studio.js +16691 -10202
  7. package/docs/bidirectional-connection-handoff.md +729 -0
  8. package/docs/harmony-api-requirements.md +17 -13
  9. package/docs/remote-development.md +80 -0
  10. package/lib/bridge/element-style-selector.d.ts +1 -0
  11. package/lib/bridge/element-style-selector.js +53 -0
  12. package/lib/contracts.d.ts +152 -83
  13. package/lib/contracts.js +0 -2
  14. package/lib/host/agent.d.ts +15 -13
  15. package/lib/host/agent.js +213 -56
  16. package/lib/host/automatic-patch.d.ts +9 -0
  17. package/lib/host/automatic-patch.js +433 -0
  18. package/lib/host/backend.d.ts +134 -4
  19. package/lib/host/backend.js +465 -114
  20. package/lib/host/drafts.d.ts +1 -1
  21. package/lib/host/drafts.js +65 -15
  22. package/lib/host/element-source.d.ts +9 -0
  23. package/lib/host/element-source.js +295 -0
  24. package/lib/host/mcp.d.ts +4 -0
  25. package/lib/host/mcp.js +97 -0
  26. package/lib/host/preview-draft.d.ts +22 -0
  27. package/lib/host/preview-draft.js +162 -0
  28. package/lib/host/preview-port.d.ts +8 -0
  29. package/lib/host/preview-port.js +32 -0
  30. package/lib/host/preview-worker.d.ts +65 -2
  31. package/lib/host/preview-worker.js +203 -76
  32. package/lib/host/preview.d.ts +26 -5
  33. package/lib/host/preview.js +130 -49
  34. package/lib/host/readiness.d.ts +2 -2
  35. package/lib/host/readiness.js +14 -17
  36. package/lib/host/routes.d.ts +1 -7
  37. package/lib/host/routes.js +41 -50
  38. package/lib/host/runtime-profile.d.ts +2 -1
  39. package/lib/host/runtime-profile.js +30 -8
  40. package/lib/host/source-resolution.d.ts +11 -1
  41. package/lib/host/source-resolution.js +69 -24
  42. package/lib/host/studio-service.d.ts +129 -0
  43. package/lib/host/studio-service.js +53 -0
  44. package/lib/index.d.ts +5 -0
  45. package/lib/index.js +64 -30
  46. package/lib/studio-remote.d.ts +126 -0
  47. package/lib/studio-remote.js +188 -0
  48. package/lib/variable-tree.d.ts +2 -0
  49. package/lib/variable-tree.js +13 -0
  50. package/package.json +62 -25
  51. package/studio.patch.yml +12 -0
@@ -1,15 +1,14 @@
1
+ import { fileURLToPath } from 'node:url';
1
2
  import { StudioAgentController } from './agent.js';
2
- import { StudioBuildError, StudioBuildRunner } from './build.js';
3
- import { StudioPreviewSupervisor } from './preview.js';
3
+ import { analyzeAutomaticPatch, writeAutomaticPatch } from './automatic-patch.js';
4
+ import { StudioBuildRunner } from './build.js';
5
+ import { readElementsStyles, saveElementsSource } from './element-source.js';
6
+ import { dshPackageModules, StudioPreviewSupervisor } from './preview.js';
4
7
  import { applyProjectPatch, listProjectFiles, readProjectFile, writeProjectFile } from './project-files.js';
5
8
  import { inspectReadiness, StudioPackRunner } from './readiness.js';
6
- import { assertDraftPackageIdentity } from './runtime-profile.js';
7
- function failure(rpcId, code, message, details = {}) {
8
- return { type: 'server-response', rpcId, result: { ok: false, error: { code, message, details } } };
9
- }
10
- function success(rpcId, value) {
11
- return { type: 'server-response', rpcId, result: { ok: true, value } };
12
- }
9
+ import { assertDraftPackageIdentity, installDraftDependencies } from './runtime-profile.js';
10
+ import { StudioSourceResolver } from './source-resolution.js';
11
+ const HARMONY_BIN_ENTRY = fileURLToPath(import.meta.resolve('dsh-harmony/bin'));
13
12
  function objectPayload(payload) {
14
13
  if (typeof payload !== 'object' || payload === null)
15
14
  throw new Error('request payload must be an object');
@@ -21,16 +20,104 @@ function draftId(payload) {
21
20
  throw new Error('draftId is required');
22
21
  return id;
23
22
  }
23
+ function optionalStringList(value, field) {
24
+ if (value === undefined)
25
+ return undefined;
26
+ if (!Array.isArray(value) || value.some(item => typeof item !== 'string' || item.length === 0)) {
27
+ throw new Error(`${field} must be an array of non-empty strings`);
28
+ }
29
+ return value;
30
+ }
31
+ function automaticPatchRequest(payload) {
32
+ const input = objectPayload(payload);
33
+ if (!Array.isArray(input.targets) || input.targets.length === 0) {
34
+ throw new Error('automatic Patch targets must be a non-empty array');
35
+ }
36
+ const targets = input.targets.map((value, index) => {
37
+ if (typeof value !== 'object' || value === null)
38
+ throw new Error(`automatic Patch target ${index} must be an object`);
39
+ const target = value;
40
+ if (typeof target.package !== 'string' || target.package === '' || typeof target.file !== 'string' || target.file === '') {
41
+ throw new Error(`automatic Patch target ${index} requires package and file`);
42
+ }
43
+ return { package: target.package, file: target.file };
44
+ });
45
+ if (input.kind === 'replace-string') {
46
+ if (typeof input.text !== 'string' || typeof input.replacement !== 'string' || typeof input.clientFile !== 'string'
47
+ || typeof input.selector !== 'string' || typeof input.elementId !== 'string' || typeof input.elementLabel !== 'string'
48
+ || typeof input.boundary !== 'object' || input.boundary === null || !Array.isArray(input.boundary.path)) {
49
+ throw new Error('automatic content Patch requires text, replacement, client source, boundary, selector, and element identity');
50
+ }
51
+ const boundary = input.boundary;
52
+ if (typeof boundary.surfaceId !== 'string' || !boundary.path.every(item => typeof item === 'string')) {
53
+ throw new Error('automatic content Patch boundary is invalid');
54
+ }
55
+ if (input.targetSelector !== undefined && (typeof input.targetSelector !== 'string' || input.targetSelector === '')) {
56
+ throw new Error('automatic content Patch target selector is invalid');
57
+ }
58
+ if (input.elementSourceFile !== undefined && (typeof input.elementSourceFile !== 'string' || input.elementSourceFile === '')) {
59
+ throw new Error('automatic content Patch Element source is invalid');
60
+ }
61
+ return {
62
+ kind: input.kind, targets, text: input.text, replacement: input.replacement, clientFile: input.clientFile,
63
+ boundary: { surfaceId: boundary.surfaceId, path: boundary.path },
64
+ ...(input.targetSelector === undefined ? {} : { targetSelector: input.targetSelector }),
65
+ selector: input.selector, elementId: input.elementId, elementLabel: input.elementLabel,
66
+ ...(input.elementSourceFile === undefined ? {} : { elementSourceFile: input.elementSourceFile }),
67
+ };
68
+ }
69
+ if (input.kind !== 'css-style' || typeof input.component !== 'string' || typeof input.clientFile !== 'string'
70
+ || typeof input.selector !== 'string' || typeof input.elementId !== 'string' || typeof input.elementLabel !== 'string'
71
+ || typeof input.boundary !== 'object' || input.boundary === null || !Array.isArray(input.boundary.path)
72
+ || !Array.isArray(input.variables))
73
+ throw new Error('automatic CSS Patch requires component, client source, boundary, selector, element identity, and variables');
74
+ const boundary = input.boundary;
75
+ if (typeof boundary.surfaceId !== 'string' || !boundary.path.every(item => typeof item === 'string')) {
76
+ throw new Error('automatic CSS Patch boundary is invalid');
77
+ }
78
+ if (input.targetSelector !== undefined && (typeof input.targetSelector !== 'string' || input.targetSelector === '')) {
79
+ throw new Error('automatic CSS Patch target selector is invalid');
80
+ }
81
+ return {
82
+ kind: input.kind,
83
+ targets,
84
+ component: input.component,
85
+ clientFile: input.clientFile,
86
+ boundary: { surfaceId: boundary.surfaceId, path: boundary.path },
87
+ ...(input.targetSelector === undefined ? {} : { targetSelector: input.targetSelector }),
88
+ selector: input.selector,
89
+ elementId: input.elementId,
90
+ elementLabel: input.elementLabel,
91
+ variables: input.variables,
92
+ };
93
+ }
94
+ function elementStyleSources(payload) {
95
+ const value = objectPayload(payload).styles;
96
+ if (!Array.isArray(value))
97
+ throw new Error('styles must be an array');
98
+ return value.map((entry, index) => {
99
+ if (typeof entry !== 'object' || entry === null)
100
+ throw new Error(`styles[${index}] must be an object`);
101
+ const input = entry;
102
+ if (typeof input.elementId !== 'string' || !Array.isArray(input.rules))
103
+ throw new Error(`styles[${index}] requires elementId and rules`);
104
+ return { elementId: input.elementId, rules: input.rules };
105
+ });
106
+ }
24
107
  class StudioDraftController {
25
108
  record;
109
+ commands;
110
+ kind = 'draft';
26
111
  projectState;
27
112
  previewState = { connected: false, mode: 'browse' };
28
113
  builds;
29
114
  packs;
30
115
  agent;
116
+ automaticPatchWrites = Promise.resolve();
31
117
  preview;
32
118
  constructor(record, profileDir, parentOrigin, commands, harmonyBinEntry, agents, subprocess) {
33
119
  this.record = record;
120
+ this.commands = commands;
34
121
  this.preview = new StudioPreviewSupervisor(record, profileDir, parentOrigin, commands, harmonyBinEntry);
35
122
  this.builds = new StudioBuildRunner(subprocess);
36
123
  this.packs = new StudioPackRunner(subprocess);
@@ -51,7 +138,7 @@ class StudioDraftController {
51
138
  return this.view();
52
139
  }
53
140
  async stop() {
54
- await this.agent.dispose();
141
+ await this.agent.leave();
55
142
  await this.builds.cancel();
56
143
  await this.preview.stop();
57
144
  this.projectState = undefined;
@@ -59,7 +146,7 @@ class StudioDraftController {
59
146
  return this.view();
60
147
  }
61
148
  async dispose() {
62
- await this.agent.dispose();
149
+ await this.agent.leave();
63
150
  await this.builds.dispose();
64
151
  await this.packs.dispose();
65
152
  await this.preview.dispose();
@@ -80,6 +167,42 @@ class StudioDraftController {
80
167
  selection() {
81
168
  return this.previewState.selection;
82
169
  }
170
+ async context() {
171
+ const selection = this.selection();
172
+ const refs = new Map();
173
+ for (const patch of selection?.react?.patches ?? []) {
174
+ const key = `${patch.target.package}\0${patch.target.file}`;
175
+ refs.set(key, patch.target);
176
+ }
177
+ const source = selection?.react?.source?.resolved;
178
+ if (source?.package !== undefined) {
179
+ const key = `${source.package}\0${source.file}`;
180
+ refs.set(key, { package: source.package, file: source.file });
181
+ }
182
+ const allTargetRefs = [...refs.values()];
183
+ const targetRefs = allTargetRefs.slice(0, 8);
184
+ const inspections = await Promise.all(targetRefs.map(ref => this.inspectHarmony(ref)));
185
+ const inspectedHarmony = inspections.length === 0 ? null : {
186
+ patches: [...new Map(inspections.flatMap(item => item.patches).map(patch => [patch.key, patch])).values()],
187
+ targets: [...new Map(inspections.flatMap(item => item.targets).map(target => [`${target.package}\0${target.file}`, target])).values()],
188
+ };
189
+ const harmony = inspectedHarmony !== null && Buffer.byteLength(JSON.stringify(inspectedHarmony)) <= 256 * 1024
190
+ ? inspectedHarmony : null;
191
+ const readiness = await this.readiness();
192
+ return {
193
+ target: 'draft',
194
+ readOnly: false,
195
+ selection: selection ?? null,
196
+ project: this.project(),
197
+ preview: this.previewStatus(),
198
+ projectFiles: await listProjectFiles(this.record.root),
199
+ profile: await this.harmonyProfile(),
200
+ harmony,
201
+ targetRefs,
202
+ targetRefsTruncated: targetRefs.length < allTargetRefs.length,
203
+ readiness: { findings: readiness.findings },
204
+ };
205
+ }
83
206
  updatePreview(update) {
84
207
  const next = { ...this.previewState, ...update };
85
208
  if (next.selection === null)
@@ -101,9 +224,18 @@ class StudioDraftController {
101
224
  async inspectHarmony(input) {
102
225
  return (await this.preview.inspect(input)).harmony;
103
226
  }
227
+ harmonyProfile() {
228
+ return this.preview.profile();
229
+ }
230
+ profile() {
231
+ return this.preview.profile();
232
+ }
233
+ updateProfile(input) {
234
+ return this.preview.updateProfile(input);
235
+ }
104
236
  async readiness() {
105
237
  const inspection = await this.preview.inspect();
106
- return inspectReadiness(this.record.root, this.record.name, inspection.harmony, `${this.record.runtimeHome}/profiles/web`, inspection.dependencies);
238
+ return inspectReadiness(this.record.root, this.record.name, inspection.harmony, `${this.record.runtimeHome}/profiles/web`);
107
239
  }
108
240
  async pack() {
109
241
  const report = await this.readiness();
@@ -116,6 +248,33 @@ class StudioDraftController {
116
248
  async applyPatch(path, before, after) {
117
249
  return applyProjectPatch(this.record.root, path, before, after);
118
250
  }
251
+ draftElements() {
252
+ const elements = this.previewStatus().registry?.elements.filter(item => item.owner === this.record.name) ?? [];
253
+ if (elements.length === 0)
254
+ throw new Error('No Elements are registered by the active Draft');
255
+ return elements;
256
+ }
257
+ async readElementStyles() {
258
+ return readElementsStyles(this.record.root, this.draftElements());
259
+ }
260
+ async saveElementSource(styles) {
261
+ return saveElementsSource(this.record.root, this.draftElements(), styles);
262
+ }
263
+ async analyzeAutomaticPatch(request) {
264
+ const sources = await Promise.all(request.targets.map(target => this.preview.readPatchTarget(target.package, target.file)));
265
+ return analyzeAutomaticPatch(request, sources, this.record.name);
266
+ }
267
+ async createAutomaticPatch(request) {
268
+ const run = this.automaticPatchWrites.then(async () => {
269
+ const plan = await this.analyzeAutomaticPatch(request);
270
+ const result = await writeAutomaticPatch(this.record.root, plan);
271
+ if (plan.client !== undefined)
272
+ await installDraftDependencies(this.record, this.commands);
273
+ return result;
274
+ });
275
+ this.automaticPatchWrites = run.then(() => undefined, () => undefined);
276
+ return run;
277
+ }
119
278
  async build(signal) {
120
279
  const current = this.project();
121
280
  if (current.state !== 'active')
@@ -131,8 +290,118 @@ class StudioDraftController {
131
290
  createAgent(agentPreset) {
132
291
  return this.agent.create(agentPreset);
133
292
  }
134
- async disposeAgent() {
135
- await this.agent.dispose();
293
+ attachAgent(sessionId) {
294
+ return this.agent.attach(sessionId);
295
+ }
296
+ async leaveAgent() {
297
+ await this.agent.leave();
298
+ return this.view();
299
+ }
300
+ }
301
+ class StudioCurrentInstanceController {
302
+ harmony;
303
+ previewUrl;
304
+ bridgeCapability;
305
+ kind = 'current-instance';
306
+ previewState = { connected: false, mode: 'browse' };
307
+ agent;
308
+ sources;
309
+ constructor(harmony, agents, previewUrl, bridgeCapability) {
310
+ this.harmony = harmony;
311
+ this.previewUrl = previewUrl;
312
+ this.bridgeCapability = bridgeCapability;
313
+ this.agent = new StudioAgentController(agents, this);
314
+ const profileDir = harmony.profile().dir;
315
+ this.sources = new StudioSourceResolver(undefined, profileDir, [dshPackageModules(HARMONY_BIN_ENTRY)]);
316
+ }
317
+ view() {
318
+ const agent = this.agent.snapshot();
319
+ return {
320
+ previewUrl: this.previewUrl,
321
+ bridgeCapability: this.bridgeCapability,
322
+ ...(agent === undefined ? {} : { agent }),
323
+ };
324
+ }
325
+ project() {
326
+ const profile = this.harmony.profile();
327
+ return {
328
+ name: 'current-webui',
329
+ root: profile.dir,
330
+ state: 'active',
331
+ graphRev: this.previewState.graphRev ?? String(profile.revision),
332
+ };
333
+ }
334
+ selection() {
335
+ return this.previewState.selection;
336
+ }
337
+ previewStatus() {
338
+ return this.previewState;
339
+ }
340
+ updatePreview(update) {
341
+ const next = { ...this.previewState, ...update };
342
+ if (next.selection === null)
343
+ delete next.selection;
344
+ if (next.registry === null)
345
+ delete next.registry;
346
+ this.previewState = next;
347
+ return this.previewState;
348
+ }
349
+ resolveSource(source) {
350
+ return this.sources.resolve(source);
351
+ }
352
+ harmonyProfile() {
353
+ return Promise.resolve(this.harmony.profile());
354
+ }
355
+ inspectHarmony(input) {
356
+ return Promise.resolve(this.harmony.inspect(input));
357
+ }
358
+ readDependencySource(packageName, file) {
359
+ return this.sources.readDependency(packageName, file);
360
+ }
361
+ async context() {
362
+ const selection = this.selection();
363
+ const refs = new Map();
364
+ for (const patch of selection?.react?.patches ?? []) {
365
+ refs.set(`${patch.target.package}\0${patch.target.file}`, patch.target);
366
+ }
367
+ const source = selection?.react?.source?.resolved;
368
+ if (source?.package !== undefined)
369
+ refs.set(`${source.package}\0${source.file}`, { package: source.package, file: source.file });
370
+ const allTargetRefs = [...refs.values()];
371
+ const targetRefs = allTargetRefs.slice(0, 8);
372
+ const inspections = await Promise.all(targetRefs.map(ref => this.inspectHarmony(ref)));
373
+ const inspectedHarmony = inspections.length === 0 ? null : {
374
+ patches: [...new Map(inspections.flatMap(item => item.patches).map(patch => [patch.key, patch])).values()],
375
+ targets: [...new Map(inspections.flatMap(item => item.targets).map(target => [`${target.package}\0${target.file}`, target])).values()],
376
+ };
377
+ const harmony = inspectedHarmony !== null && Buffer.byteLength(JSON.stringify(inspectedHarmony)) <= 256 * 1024
378
+ ? inspectedHarmony : null;
379
+ return {
380
+ target: 'current-instance',
381
+ readOnly: true,
382
+ selection: selection ?? null,
383
+ project: this.project(),
384
+ preview: this.previewStatus(),
385
+ projectFiles: [],
386
+ profile: this.harmony.profile(),
387
+ harmony,
388
+ targetRefs,
389
+ targetRefsTruncated: targetRefs.length < allTargetRefs.length,
390
+ readiness: { findings: [] },
391
+ };
392
+ }
393
+ createAgent(agentPreset) {
394
+ return this.agent.create(agentPreset);
395
+ }
396
+ attachAgent(sessionId) {
397
+ return this.agent.attach(sessionId);
398
+ }
399
+ async leaveAgent() {
400
+ await this.agent.leave();
401
+ return this.view();
402
+ }
403
+ dispose() {
404
+ return this.agent.leave();
136
405
  }
137
406
  }
138
407
  /** Stable-Host control plane for persistent, isolated Draft Preview runtimes. */
@@ -146,7 +415,8 @@ export class StudioBackend {
146
415
  parentOrigin;
147
416
  controllers = new Map();
148
417
  controllerCreations = new Map();
149
- constructor(harmony, agents, subprocess, registry, workspace, commands, parentOrigin) {
418
+ current;
419
+ constructor(harmony, agents, subprocess, registry, workspace, commands, parentOrigin, currentBridgeCapability = 'current-instance') {
150
420
  this.harmony = harmony;
151
421
  this.agents = agents;
152
422
  this.subprocess = subprocess;
@@ -154,109 +424,189 @@ export class StudioBackend {
154
424
  this.workspace = workspace;
155
425
  this.commands = commands;
156
426
  this.parentOrigin = parentOrigin;
427
+ this.current = new StudioCurrentInstanceController(harmony, agents, `${parentOrigin}/#dsh-studio-preview=${encodeURIComponent(currentBridgeCapability)}`, currentBridgeCapability);
157
428
  }
158
- async call(message) {
159
- const { method, payload, rpcId } = message;
160
- try {
161
- if (method === 'studio.drafts.list')
162
- return success(rpcId, await this.list());
163
- if (method === 'studio.drafts.create')
164
- return success(rpcId, await this.create(payload));
165
- if (method === 'studio.workspace.get') {
166
- const records = await this.registry.list();
167
- return success(rpcId, await this.workspace.read(records.map(record => record.id)));
168
- }
169
- if (method === 'studio.workspace.update') {
170
- const records = await this.registry.list();
171
- return success(rpcId, await this.workspace.write(objectPayload(payload), records.map(record => record.id)));
172
- }
173
- const controller = await this.controller(draftId(payload));
174
- if (method === 'studio.drafts.rename') {
175
- const label = objectPayload(payload).label;
176
- if (typeof label !== 'string')
177
- throw new Error('Draft name is required');
178
- const record = await this.registry.rename(controller.record.id, label);
179
- controller.record = record;
180
- return success(rpcId, controller.view());
181
- }
182
- if (method === 'studio.drafts.export') {
183
- const record = await this.registry.export(controller.record.id);
184
- controller.record = record;
185
- return success(rpcId, controller.view());
186
- }
187
- if (method === 'studio.drafts.start')
188
- return success(rpcId, await controller.start());
189
- if (method === 'studio.drafts.stop')
190
- return success(rpcId, await controller.stop());
191
- if (method === 'studio.project.state')
192
- return success(rpcId, await controller.refreshProject());
193
- if (method === 'studio.project.activate') {
194
- const graphRev = objectPayload(payload).graphRev;
195
- if (typeof graphRev !== 'string')
196
- throw new Error('graphRev is required');
197
- return success(rpcId, await controller.activate(graphRev));
198
- }
199
- if (method === 'studio.project.files')
200
- return success(rpcId, await listProjectFiles(controller.record.root));
201
- if (method === 'studio.project.readFile') {
202
- const path = objectPayload(payload).path;
203
- if (typeof path !== 'string')
204
- throw new Error('path is required');
205
- return success(rpcId, { path, content: await controller.readFile(path) });
206
- }
207
- if (method === 'studio.project.writeFile') {
208
- const { path, content } = objectPayload(payload);
209
- if (typeof path !== 'string' || typeof content !== 'string')
210
- throw new Error('path and content are required');
211
- await writeProjectFile(controller.record.root, path, content);
212
- return success(rpcId, { path, saved: true });
213
- }
214
- if (method === 'studio.project.build')
215
- return success(rpcId, await controller.build(new AbortController().signal));
216
- if (method === 'studio.project.cancelBuild')
217
- return success(rpcId, { canceled: await controller.cancelBuild() });
218
- if (method === 'studio.readiness.inspect')
219
- return success(rpcId, await controller.readiness());
220
- if (method === 'studio.readiness.pack')
221
- return success(rpcId, await controller.pack());
222
- if (method === 'studio.harmony.inspect') {
223
- const input = objectPayload(payload);
224
- return success(rpcId, await controller.inspectHarmony({
225
- ...(typeof input.package === 'string' ? { package: input.package } : {}),
226
- ...(typeof input.file === 'string' ? { file: input.file } : {}),
227
- }));
228
- }
229
- if (method === 'studio.preview.status')
230
- return success(rpcId, controller.previewStatus());
231
- if (method === 'studio.preview.update')
232
- return success(rpcId, controller.updatePreview(this.previewStatus(payload)));
233
- if (method === 'studio.preview.resolveSource') {
234
- const source = objectPayload(payload).source;
235
- if (typeof source?.file !== 'string')
236
- throw new Error('source is required');
237
- return success(rpcId, await controller.resolveSource(source));
238
- }
239
- if (method === 'studio.agent.create') {
240
- const preset = objectPayload(payload).agentPreset;
241
- if (preset !== undefined && typeof preset !== 'string')
242
- throw new Error('agentPreset must be a string');
243
- return success(rpcId, await controller.createAgent(preset));
244
- }
245
- if (method === 'studio.agent.dispose') {
246
- await controller.disposeAgent();
247
- return success(rpcId, { disposed: true });
248
- }
249
- return failure(rpcId, 'studio-method-forbidden', `method ${method} is not exposed by Studio`);
429
+ currentGet() {
430
+ return this.current.view();
431
+ }
432
+ currentPreviewStatus() {
433
+ return this.current.previewStatus();
434
+ }
435
+ currentProjectState() {
436
+ return this.current.project();
437
+ }
438
+ currentContext() {
439
+ return this.current.context();
440
+ }
441
+ currentHarmonyProfile() {
442
+ return this.current.harmonyProfile();
443
+ }
444
+ currentHarmonyInspect(input) {
445
+ return this.current.inspectHarmony(input);
446
+ }
447
+ currentReadDependencySource(input) {
448
+ return this.current.readDependencySource(input.package, input.file);
449
+ }
450
+ currentPreviewUpdate(input) {
451
+ return this.current.updatePreview(this.parsePreviewStatus(input));
452
+ }
453
+ currentResolveSource(input) {
454
+ if (typeof input.source?.file !== 'string')
455
+ throw new Error('source is required');
456
+ return this.current.resolveSource(input.source);
457
+ }
458
+ currentAgentCreate(input) {
459
+ if (input.agentPreset !== undefined && typeof input.agentPreset !== 'string')
460
+ throw new Error('agentPreset must be a string');
461
+ return this.current.createAgent(input.agentPreset);
462
+ }
463
+ currentAgentAttach(input) {
464
+ if (typeof input.sessionId !== 'string' || input.sessionId.trim() === '')
465
+ throw new Error('sessionId is required');
466
+ if ([...this.controllers.values()].some(controller => controller.view().agent?.sessionId === input.sessionId)) {
467
+ throw new Error('the selected session is already attached to a Draft');
250
468
  }
251
- catch (error) {
252
- const code = error instanceof StudioBuildError ? error.code : 'studio-request-failed';
253
- const details = error instanceof StudioBuildError ? error.output : undefined;
254
- return failure(rpcId, code, error instanceof Error ? error.message : String(error), details);
469
+ return this.current.attachAgent(input.sessionId);
470
+ }
471
+ currentAgentLeave() {
472
+ return this.current.leaveAgent();
473
+ }
474
+ draftsList() {
475
+ return this.list();
476
+ }
477
+ draftsCreate(input) {
478
+ return this.create(input);
479
+ }
480
+ async workspaceGet() {
481
+ const records = await this.registry.list();
482
+ return this.workspace.read(records.map(record => record.id));
483
+ }
484
+ async workspaceUpdate(input) {
485
+ const records = await this.registry.list();
486
+ return this.workspace.write(input, records.map(record => record.id));
487
+ }
488
+ async harmonyProfile(input) {
489
+ return (await this.controller(draftId(input))).profile();
490
+ }
491
+ async harmonyInspect(input) {
492
+ const controller = await this.controller(draftId(input));
493
+ return controller.inspectHarmony({
494
+ ...(input.package === undefined ? {} : { package: input.package }),
495
+ ...(input.file === undefined ? {} : { file: input.file }),
496
+ });
497
+ }
498
+ async harmonyUpdateProfile(input) {
499
+ const controller = await this.controller(draftId(input));
500
+ const order = optionalStringList(input.order, 'order');
501
+ const patchOrder = optionalStringList(input.patchOrder, 'patchOrder');
502
+ const disabled = optionalStringList(input.disabled, 'disabled');
503
+ return controller.updateProfile({
504
+ ...(order === undefined ? {} : { order }),
505
+ ...(patchOrder === undefined ? {} : { patchOrder }),
506
+ ...(disabled === undefined ? {} : { disabled }),
507
+ });
508
+ }
509
+ async draftsRename(input) {
510
+ const controller = await this.controller(draftId(input));
511
+ if (typeof input.label !== 'string')
512
+ throw new Error('Draft name is required');
513
+ controller.record = await this.registry.rename(controller.record.id, input.label);
514
+ return controller.view();
515
+ }
516
+ async draftsExport(input) {
517
+ const controller = await this.controller(draftId(input));
518
+ controller.record = await this.registry.export(controller.record.id);
519
+ return controller.view();
520
+ }
521
+ async draftsStart(input) {
522
+ return (await this.controller(draftId(input))).start();
523
+ }
524
+ async draftsStop(input) {
525
+ return (await this.controller(draftId(input))).stop();
526
+ }
527
+ async projectState(input) {
528
+ return (await this.controller(draftId(input))).refreshProject();
529
+ }
530
+ async projectActivate(input) {
531
+ if (typeof input.graphRev !== 'string')
532
+ throw new Error('graphRev is required');
533
+ return (await this.controller(draftId(input))).activate(input.graphRev);
534
+ }
535
+ async projectFiles(input) {
536
+ const controller = await this.controller(draftId(input));
537
+ return listProjectFiles(controller.record.root);
538
+ }
539
+ async projectReadFile(input) {
540
+ if (typeof input.path !== 'string')
541
+ throw new Error('path is required');
542
+ const controller = await this.controller(draftId(input));
543
+ return { path: input.path, content: await controller.readFile(input.path) };
544
+ }
545
+ async projectWriteFile(input) {
546
+ if (typeof input.path !== 'string' || typeof input.content !== 'string')
547
+ throw new Error('path and content are required');
548
+ const controller = await this.controller(draftId(input));
549
+ await writeProjectFile(controller.record.root, input.path, input.content);
550
+ return { path: input.path, saved: true };
551
+ }
552
+ async elementsStyles(input) {
553
+ return (await this.controller(draftId(input))).readElementStyles();
554
+ }
555
+ async elementsSaveSource(input) {
556
+ return (await this.controller(draftId(input))).saveElementSource(elementStyleSources(input));
557
+ }
558
+ async patchesAnalyzeAutomatic(input) {
559
+ return (await this.controller(draftId(input))).analyzeAutomaticPatch(automaticPatchRequest(input));
560
+ }
561
+ async patchesCreateAutomatic(input) {
562
+ return (await this.controller(draftId(input))).createAutomaticPatch(automaticPatchRequest(input));
563
+ }
564
+ async projectBuild(input, signal) {
565
+ return (await this.controller(draftId(input))).build(signal);
566
+ }
567
+ async projectCancelBuild(input) {
568
+ return { canceled: await (await this.controller(draftId(input))).cancelBuild() };
569
+ }
570
+ async readinessInspect(input) {
571
+ return (await this.controller(draftId(input))).readiness();
572
+ }
573
+ async readinessPack(input) {
574
+ return (await this.controller(draftId(input))).pack();
575
+ }
576
+ async previewStatus(input) {
577
+ return (await this.controller(draftId(input))).previewStatus();
578
+ }
579
+ async previewUpdate(input) {
580
+ return (await this.controller(draftId(input))).updatePreview(this.parsePreviewStatus(input));
581
+ }
582
+ async previewResolveSource(input) {
583
+ if (typeof input.source?.file !== 'string')
584
+ throw new Error('source is required');
585
+ return (await this.controller(draftId(input))).resolveSource(input.source);
586
+ }
587
+ async agentCreate(input) {
588
+ if (input.agentPreset !== undefined && typeof input.agentPreset !== 'string')
589
+ throw new Error('agentPreset must be a string');
590
+ return (await this.controller(draftId(input))).createAgent(input.agentPreset);
591
+ }
592
+ async agentAttach(input) {
593
+ const controller = await this.controller(draftId(input));
594
+ if (typeof input.sessionId !== 'string' || input.sessionId.trim() === '')
595
+ throw new Error('sessionId is required');
596
+ if (this.current.view().agent?.sessionId === input.sessionId) {
597
+ throw new Error('the selected session is already attached to the current instance');
255
598
  }
599
+ const other = [...this.controllers.entries()].find(([id, candidate]) => (id !== controller.record.id && candidate.view().agent?.sessionId === input.sessionId));
600
+ if (other !== undefined)
601
+ throw new Error('the selected session is already attached to another Draft');
602
+ return controller.attachAgent(input.sessionId);
603
+ }
604
+ async agentLeave(input) {
605
+ return (await this.controller(draftId(input))).leaveAgent();
256
606
  }
257
607
  async dispose() {
258
608
  await Promise.all([...this.controllerCreations.values()].map(creation => creation.catch(() => undefined)));
259
- await Promise.all([...this.controllers.values()].map(controller => controller.dispose()));
609
+ await Promise.all([this.current.dispose(), ...[...this.controllers.values()].map(controller => controller.dispose())]);
260
610
  this.controllers.clear();
261
611
  this.controllerCreations.clear();
262
612
  }
@@ -272,6 +622,7 @@ export class StudioBackend {
272
622
  if ((candidate.profileMode !== 'main-home' && candidate.profileMode !== 'custom')
273
623
  || typeof candidate.source !== 'object' || candidate.source === null
274
624
  || (candidate.source.kind !== 'new' && candidate.source.kind !== 'existing')
625
+ || (candidate.profileDirectory !== undefined && typeof candidate.profileDirectory !== 'string')
275
626
  || (candidate.destinationDirectory !== undefined && typeof candidate.destinationDirectory !== 'string')) {
276
627
  throw new Error('Draft source and profileMode are invalid');
277
628
  }
@@ -296,11 +647,11 @@ export class StudioBackend {
296
647
  }
297
648
  }
298
649
  makeController(record) {
299
- const controller = new StudioDraftController(record, this.harmony.profileDir, this.parentOrigin, this.commands, this.harmony.binEntry, this.agents, this.subprocess);
650
+ const controller = new StudioDraftController(record, this.harmony.profile().dir, this.parentOrigin, this.commands, HARMONY_BIN_ENTRY, this.agents, this.subprocess);
300
651
  this.controllers.set(record.id, controller);
301
652
  return controller;
302
653
  }
303
- previewStatus(payload) {
654
+ parsePreviewStatus(payload) {
304
655
  const candidate = objectPayload(payload);
305
656
  if (typeof candidate.connected !== 'boolean' || (candidate.mode !== 'browse' && candidate.mode !== 'inspect')
306
657
  || (candidate.graphRev !== undefined && typeof candidate.graphRev !== 'string')) {