@arronqzy/react-view 1.0.3 → 1.0.7

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 (43) hide show
  1. package/dist/styles.css +555 -15
  2. package/package.json +14 -7
  3. package/readme.md +74 -5
  4. package/src/core/template.tsx +12 -2
  5. package/src/panel/ReactViewOnlinePreview.tsx +18 -8
  6. package/src/panel/ReactViewPanel.tsx +403 -177
  7. package/src/panel/ai/AssistantChatPanel.tsx +895 -0
  8. package/src/panel/ai/AssistantMarkdown.tsx +256 -0
  9. package/src/panel/ai/applyAssistantAction.ts +652 -0
  10. package/src/panel/ai/revealConfigField.ts +137 -0
  11. package/src/panel/components/ConfigHintIcon.tsx +50 -0
  12. package/src/panel/components/ElementsLayer.tsx +39 -22
  13. package/src/panel/components/MaterialSidebar.tsx +157 -109
  14. package/src/panel/components/MoveableLayer.tsx +75 -61
  15. package/src/panel/components/PanelCanvas.tsx +81 -64
  16. package/src/panel/components/PanelConfigSidebar.tsx +471 -448
  17. package/src/panel/components/SelectLayer.tsx +1 -0
  18. package/src/panel/components/ViewElementScopePanel.tsx +13 -12
  19. package/src/panel/components/WorkspaceConfigSidebar.tsx +5 -2
  20. package/src/panel/components/WorkspaceProjectNav.tsx +25 -14
  21. package/src/panel/components/WorkspaceStageSplit.tsx +3 -1
  22. package/src/panel/components/scope-config/ScopeConfigContext.tsx +9 -0
  23. package/src/panel/components/scope-config/ScopeTemplatePreviewHost.tsx +242 -0
  24. package/src/panel/components/scope-config/ScopeTemplateUsageHint.tsx +12 -0
  25. package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.tsx +6 -3
  26. package/src/panel/components/table/ConditionEditor.tsx +205 -0
  27. package/src/panel/components/table/JsonCodeEditor.tsx +115 -0
  28. package/src/panel/components/table/PanelConfigTableSection.tsx +2192 -0
  29. package/src/panel/components/table/TableNodeContent.tsx +544 -0
  30. package/src/panel/constants/messages.ts +76 -34
  31. package/src/panel/hooks/usePanelElements.ts +64 -52
  32. package/src/panel/hooks/useRafThrottledScroll.ts +30 -0
  33. package/src/panel/hooks/useWorkspaceProjects.ts +11 -8
  34. package/src/panel/types.ts +6 -0
  35. package/src/panel/utils/chartOptionBuilder.ts +12 -1
  36. package/src/panel/utils/panelElementDefaults.ts +69 -39
  37. package/src/panel/utils/scope-field-labels.ts +51 -37
  38. package/src/panel/utils/scope-template-chart.ts +92 -0
  39. package/src/panel/utils/scope-template-preview.ts +128 -0
  40. package/src/panel/utils/scope-template-spread.ts +252 -0
  41. package/src/panel/utils/scope-template-warnings.ts +131 -11
  42. package/src/panel/utils/scope-template.ts +105 -3
  43. package/src/tailwind.css +36 -15
@@ -0,0 +1,652 @@
1
+ import type { AssistantAction, CompactLayer, CompactWorkspace } from "@arronqzy/webllm-assistant";
2
+ import {
3
+ isKnownMaterialType,
4
+ normalizeBlueprintNodeAlias,
5
+ normalizeMaterialType,
6
+ } from "@arronqzy/webllm-assistant";
7
+ import {
8
+ BlueprintGraph,
9
+ patchNodeConfigSource,
10
+ type BlueprintConfigSource,
11
+ } from "@arronqzy/react-blueprint";
12
+ import { store } from "@arronqzy/rx-store";
13
+ import type { PanelElement, PanelLayer } from "../types";
14
+ import { isPanelElementNode } from "../utils/panelElementNodes";
15
+ import { DEFAULT_LAYER, DEFAULT_LAYER_ID } from "../utils/panelElementDefaults";
16
+
17
+ export type ApplyAssistantDeps = {
18
+ /** @returns created element id, or null if layer locked / failed */
19
+ addElementFromMaterial: (
20
+ materialType: string,
21
+ x: number,
22
+ y: number
23
+ ) => string | null;
24
+ updateElement: (
25
+ id: string,
26
+ patch: Partial<PanelElement>,
27
+ options?: { batchId?: string; meta?: Record<string, unknown> }
28
+ ) => void;
29
+ deleteElement: (id: string) => void;
30
+ duplicateElement: (id: string) => void;
31
+ /** Select on canvas + switch right sidebar to view config */
32
+ setSelectedIds: (ids: string[]) => void;
33
+ getSelectedIds: () => string[];
34
+ /** Scroll/highlight config fields matching a panel.update patch */
35
+ revealConfigFromPatch?: (patch: Record<string, unknown>) => void;
36
+ focusWorkspace?: (area: "view" | "blueprint") => void;
37
+
38
+ /** Live reads from store when possible */
39
+ getElementIds: () => Set<string>;
40
+ getElement: (id: string) => PanelElement | undefined;
41
+ listElements: () => PanelElement[];
42
+ getLayers: () => CompactLayer[];
43
+ getActiveLayerId: () => string | null;
44
+ setActiveLayer: (id: string) => void;
45
+ addLayer: () => void;
46
+ renameLayer: (id: string, name: string) => void;
47
+ /** Set absolute lock; toggle if current differs */
48
+ setLayerLocked: (id: string, locked: boolean) => void;
49
+ deleteLayer: (id: string) => { ok: boolean; reason?: string };
50
+ bringElementsToFront: (ids: string[]) => void;
51
+ sendElementsToBack: (ids: string[]) => void;
52
+ bringElementsForward: (ids: string[]) => void;
53
+ sendElementsBackward: (ids: string[]) => void;
54
+ getZoom: () => number;
55
+ setZoomAbsolute: (value: number) => void;
56
+ adjustZoomDelta: (delta: number) => void;
57
+ fitViewport: () => void;
58
+ undo: () => void;
59
+ redo: () => void;
60
+ setBlueprintGraph: (
61
+ updater: (graph: BlueprintGraph) => BlueprintGraph
62
+ ) => void;
63
+ getBlueprintGraph: () => BlueprintGraph;
64
+ setBlueprintOpen: (open: boolean) => void;
65
+ getBlueprintOpen: () => boolean;
66
+ runBlueprintAll: () => void | Promise<void>;
67
+ getWorkspace: () => CompactWorkspace;
68
+ workspaceSave: () => Promise<{ id: string; name: string }>;
69
+ workspaceSync: () => Promise<string>;
70
+ workspaceCreate: (name?: string) => Promise<{ id: string; name: string }>;
71
+ workspaceOpen: (id: string) => Promise<void>;
72
+ workspacePreview: (id?: string) => Promise<void>;
73
+ exportPanelJson: () => string;
74
+ importPanelJson: (json: string) => boolean;
75
+ setTheme: (theme: "light" | "dark") => void;
76
+ setLocale: (locale: "zh-CN" | "en-US") => void;
77
+ setPanelFontSize: (size: "sm" | "md" | "lg") => void;
78
+ };
79
+
80
+ export type ApplyAssistantResult = {
81
+ ok: boolean;
82
+ message: string;
83
+ createdIds?: string[];
84
+ };
85
+
86
+ const NESTED_MERGE_KEYS = new Set(["table", "style", "chart"]);
87
+
88
+ export function listPanelElementsFromStore(): PanelElement[] {
89
+ const state = store.getState();
90
+ return (state.root.children ?? [])
91
+ .filter((n) => isPanelElementNode(n) && n.props)
92
+ .map((n) => {
93
+ const props = n.props as PanelElement;
94
+ return {
95
+ ...props,
96
+ zIndex: typeof props.zIndex === "number" ? props.zIndex : 1,
97
+ };
98
+ });
99
+ }
100
+
101
+ export function listLayersFromStore(): CompactLayer[] {
102
+ const state = store.getState();
103
+ const list =
104
+ (state.variables?.layers as PanelLayer[] | undefined) ?? [DEFAULT_LAYER];
105
+ const active =
106
+ (state.variables?.activeLayerId as string | undefined) ?? DEFAULT_LAYER_ID;
107
+ return list.map((l) => ({
108
+ id: l.id,
109
+ name: l.name,
110
+ locked: !!l.locked,
111
+ active: l.id === active,
112
+ }));
113
+ }
114
+
115
+ export function getActiveLayerIdFromStore(): string | null {
116
+ const state = store.getState();
117
+ return (
118
+ (state.variables?.activeLayerId as string | undefined) ?? DEFAULT_LAYER_ID
119
+ );
120
+ }
121
+
122
+ function deepMergePatch(
123
+ existing: PanelElement | undefined,
124
+ patch: Record<string, unknown>
125
+ ): Partial<PanelElement> {
126
+ const next: Record<string, unknown> = { ...patch };
127
+ for (const key of NESTED_MERGE_KEYS) {
128
+ if (!isPlainObject(patch[key])) continue;
129
+ const prev = existing?.[key as keyof PanelElement];
130
+ if (isPlainObject(prev)) {
131
+ next[key] = { ...prev, ...(patch[key] as Record<string, unknown>) };
132
+ }
133
+ }
134
+ return next as Partial<PanelElement>;
135
+ }
136
+
137
+ function isPlainObject(v: unknown): v is Record<string, unknown> {
138
+ return !!v && typeof v === "object" && !Array.isArray(v);
139
+ }
140
+
141
+ function resolveElementId(
142
+ requestedId: string,
143
+ deps: ApplyAssistantDeps
144
+ ): string | null {
145
+ const ids = deps.getElementIds();
146
+ if (ids.has(requestedId)) return requestedId;
147
+ const selected = deps.getSelectedIds() ?? [];
148
+ if (selected.length === 1 && ids.has(selected[0]!)) return selected[0]!;
149
+ for (const id of ids) {
150
+ const el = deps.getElement(id);
151
+ if (el?.name && el.name === requestedId) return id;
152
+ }
153
+ return null;
154
+ }
155
+
156
+ function ensureBlueprintParent(graph: BlueprintGraph): {
157
+ graph: BlueprintGraph;
158
+ parentId: string;
159
+ } {
160
+ const existing = graph.document.nodes.find((n) => n.role === "blueprint");
161
+ if (existing) return { graph, parentId: existing.id };
162
+ const next = graph.addBlueprintNode({ x: 80, y: 80 }, "Main");
163
+ const parent = next.document.nodes.find((n) => n.role === "blueprint");
164
+ if (!parent) throw new Error("failed_create_blueprint_root");
165
+ return { graph: next, parentId: parent.id };
166
+ }
167
+
168
+ function applyBlueprintAddNode(
169
+ graph: BlueprintGraph,
170
+ action: Extract<AssistantAction, { type: "blueprint.addNode" }>
171
+ ): { graph: BlueprintGraph; message: string } {
172
+ const alias = normalizeBlueprintNodeAlias(action.nodeType);
173
+ if (!alias) {
174
+ throw new Error(`unknown_blueprint_node_type:${action.nodeType}`);
175
+ }
176
+ const position = action.position ?? {
177
+ x: 180 + Math.round(Math.random() * 80),
178
+ y: 120 + Math.round(Math.random() * 80),
179
+ };
180
+
181
+ if (alias === "blueprint") {
182
+ const next = graph.addBlueprintNode(position, action.label);
183
+ return { graph: next, message: `added blueprint node` };
184
+ }
185
+
186
+ const ensured = ensureBlueprintParent(graph);
187
+ let next = ensured.graph;
188
+ const parentId = ensured.parentId;
189
+
190
+ if (alias === "lifecycle") {
191
+ next = next.addLifecycleNode(parentId, position, "mounted", action.label);
192
+ return { graph: next, message: `added lifecycle node` };
193
+ }
194
+
195
+ next = next.addLogicNode(parentId, position, action.label ?? alias);
196
+ const created = next.document.nodes[next.document.nodes.length - 1];
197
+ if (!created) throw new Error("failed_add_logic_node");
198
+
199
+ const configSourceMap: Record<string, BlueprintConfigSource | undefined> = {
200
+ logic: "logic",
201
+ clock: "clock",
202
+ fetch: "fetch",
203
+ json: "json",
204
+ and: "and",
205
+ };
206
+ const configSource = configSourceMap[alias];
207
+ if (configSource && configSource !== "logic") {
208
+ next = next.updateNode(created.id, patchNodeConfigSource(created, configSource));
209
+ }
210
+ if (typeof action.label === "string" && action.label) {
211
+ next = next.updateNode(created.id, { label: action.label });
212
+ }
213
+ return { graph: next, message: `added ${alias} node ${created.id}` };
214
+ }
215
+
216
+ async function applyOne(
217
+ action: AssistantAction,
218
+ deps: ApplyAssistantDeps,
219
+ depth = 0
220
+ ): Promise<ApplyAssistantResult> {
221
+ if (depth > 8) {
222
+ return { ok: false, message: "batch_too_deep" };
223
+ }
224
+
225
+ switch (action.type) {
226
+ case "reply":
227
+ case "agent.done":
228
+ case "agent.fail":
229
+ return { ok: true, message: action.message };
230
+
231
+ case "panel.add": {
232
+ const materialType =
233
+ normalizeMaterialType(action.materialType) ?? action.materialType;
234
+ if (!isKnownMaterialType(materialType)) {
235
+ return {
236
+ ok: false,
237
+ message: `unknown_material:${action.materialType}`,
238
+ };
239
+ }
240
+ const x = action.x ?? 220;
241
+ const y = action.y ?? 160;
242
+ const createdId = deps.addElementFromMaterial(materialType, x, y);
243
+ if (!createdId) {
244
+ return {
245
+ ok: false,
246
+ message: "layer_locked_or_add_failed",
247
+ };
248
+ }
249
+ if (action.patch && Object.keys(action.patch).length) {
250
+ const existing = deps.getElement(createdId);
251
+ deps.updateElement(createdId, deepMergePatch(existing, action.patch));
252
+ }
253
+ deps.setSelectedIds([createdId]);
254
+ deps.focusWorkspace?.("view");
255
+ if (action.patch && Object.keys(action.patch).length) {
256
+ deps.revealConfigFromPatch?.(action.patch);
257
+ }
258
+ return {
259
+ ok: true,
260
+ message: action.message ?? `added ${materialType}`,
261
+ createdIds: [createdId],
262
+ };
263
+ }
264
+
265
+ case "panel.update": {
266
+ const id = resolveElementId(action.id, deps);
267
+ if (!id) {
268
+ return { ok: false, message: `missing_element:${action.id}` };
269
+ }
270
+ const existing = deps.getElement(id);
271
+ deps.updateElement(id, deepMergePatch(existing, action.patch));
272
+ deps.setSelectedIds([id]);
273
+ deps.focusWorkspace?.("view");
274
+ deps.revealConfigFromPatch?.(action.patch);
275
+ return {
276
+ ok: true,
277
+ message:
278
+ action.message ??
279
+ (id === action.id
280
+ ? `updated ${id}`
281
+ : `updated ${id} (resolved from ${action.id})`),
282
+ };
283
+ }
284
+
285
+ case "panel.remove": {
286
+ if (!deps.getElementIds().has(action.id)) {
287
+ return { ok: false, message: `missing_element:${action.id}` };
288
+ }
289
+ deps.deleteElement(action.id);
290
+ return { ok: true, message: action.message ?? `removed ${action.id}` };
291
+ }
292
+
293
+ case "panel.select": {
294
+ const ids = action.ids.filter((id) => deps.getElementIds().has(id));
295
+ if (!ids.length && action.ids.length) {
296
+ return { ok: false, message: `missing_elements:${action.ids.join(",")}` };
297
+ }
298
+ deps.setSelectedIds(ids);
299
+ deps.focusWorkspace?.("view");
300
+ return {
301
+ ok: true,
302
+ message: action.message ?? `selected ${ids.join(",") || "(none)"}`,
303
+ };
304
+ }
305
+
306
+ case "panel.duplicate": {
307
+ if (!deps.getElementIds().has(action.id)) {
308
+ return { ok: false, message: `missing_element:${action.id}` };
309
+ }
310
+ const before = deps.getElementIds();
311
+ deps.duplicateElement(action.id);
312
+ const created = [...deps.getElementIds()].find((id) => !before.has(id));
313
+ if (created) deps.setSelectedIds([created]);
314
+ return {
315
+ ok: true,
316
+ message: action.message ?? `duplicated ${action.id}`,
317
+ createdIds: created ? [created] : undefined,
318
+ };
319
+ }
320
+
321
+ case "panel.lock": {
322
+ const id = resolveElementId(action.id, deps);
323
+ if (!id) return { ok: false, message: `missing_element:${action.id}` };
324
+ deps.updateElement(id, { locked: action.locked });
325
+ return {
326
+ ok: true,
327
+ message:
328
+ action.message ??
329
+ (action.locked ? `locked ${id}` : `unlocked ${id}`),
330
+ };
331
+ }
332
+
333
+ case "panel.zOrder": {
334
+ const ids =
335
+ action.ids?.filter((id) => deps.getElementIds().has(id)) ??
336
+ deps.getSelectedIds();
337
+ if (!ids.length) return { ok: false, message: "no_selection" };
338
+ if (action.action === "front") deps.bringElementsToFront(ids);
339
+ else if (action.action === "back") deps.sendElementsToBack(ids);
340
+ else if (action.action === "forward") deps.bringElementsForward(ids);
341
+ else deps.sendElementsBackward(ids);
342
+ return {
343
+ ok: true,
344
+ message: action.message ?? `zOrder ${action.action}`,
345
+ };
346
+ }
347
+
348
+ case "panel.batch": {
349
+ const notes: string[] = [];
350
+ const createdIds: string[] = [];
351
+ for (const child of action.actions) {
352
+ const result = await applyOne(child, deps, depth + 1);
353
+ notes.push(result.message);
354
+ if (result.createdIds) createdIds.push(...result.createdIds);
355
+ if (!result.ok) {
356
+ return {
357
+ ok: false,
358
+ message: action.message
359
+ ? `${action.message}\n${notes.join("\n")}`
360
+ : notes.join("\n"),
361
+ createdIds,
362
+ };
363
+ }
364
+ }
365
+ return {
366
+ ok: true,
367
+ message: action.message ?? notes.join("\n"),
368
+ createdIds,
369
+ };
370
+ }
371
+
372
+ case "layer.setActive": {
373
+ const layers = deps.getLayers();
374
+ if (!layers.some((l) => l.id === action.id)) {
375
+ return { ok: false, message: `missing_layer:${action.id}` };
376
+ }
377
+ deps.setActiveLayer(action.id);
378
+ return { ok: true, message: action.message ?? `active layer ${action.id}` };
379
+ }
380
+
381
+ case "layer.add": {
382
+ deps.addLayer();
383
+ const activeId = deps.getActiveLayerId();
384
+ if (action.name && activeId) {
385
+ deps.renameLayer(activeId, action.name);
386
+ }
387
+ return {
388
+ ok: true,
389
+ message: action.message ?? `added layer ${activeId ?? ""}`,
390
+ };
391
+ }
392
+
393
+ case "layer.rename": {
394
+ if (!deps.getLayers().some((l) => l.id === action.id)) {
395
+ return { ok: false, message: `missing_layer:${action.id}` };
396
+ }
397
+ deps.renameLayer(action.id, action.name);
398
+ return { ok: true, message: action.message ?? `renamed layer ${action.id}` };
399
+ }
400
+
401
+ case "layer.lock": {
402
+ if (!deps.getLayers().some((l) => l.id === action.id)) {
403
+ return { ok: false, message: `missing_layer:${action.id}` };
404
+ }
405
+ deps.setLayerLocked(action.id, action.locked);
406
+ return {
407
+ ok: true,
408
+ message:
409
+ action.message ??
410
+ (action.locked ? `locked layer ${action.id}` : `unlocked layer ${action.id}`),
411
+ };
412
+ }
413
+
414
+ case "layer.delete": {
415
+ const result = deps.deleteLayer(action.id);
416
+ if (!result.ok) {
417
+ return { ok: false, message: result.reason ?? `delete_layer_failed:${action.id}` };
418
+ }
419
+ return { ok: true, message: action.message ?? `deleted layer ${action.id}` };
420
+ }
421
+
422
+ case "viewport.zoom": {
423
+ if (action.mode === "delta") {
424
+ deps.adjustZoomDelta(action.value);
425
+ } else {
426
+ deps.setZoomAbsolute(action.value);
427
+ }
428
+ return {
429
+ ok: true,
430
+ message: action.message ?? `zoom ${deps.getZoom()}`,
431
+ };
432
+ }
433
+
434
+ case "viewport.fit": {
435
+ deps.fitViewport();
436
+ return { ok: true, message: action.message ?? "viewport fit" };
437
+ }
438
+
439
+ case "history.undo": {
440
+ deps.undo();
441
+ return { ok: true, message: action.message ?? "undo" };
442
+ }
443
+
444
+ case "history.redo": {
445
+ deps.redo();
446
+ return { ok: true, message: action.message ?? "redo" };
447
+ }
448
+
449
+ case "blueprint.addNode": {
450
+ try {
451
+ let message = "";
452
+ deps.setBlueprintOpen(true);
453
+ deps.focusWorkspace?.("blueprint");
454
+ deps.setBlueprintGraph((graph) => {
455
+ const result = applyBlueprintAddNode(graph, action);
456
+ message = result.message;
457
+ return result.graph;
458
+ });
459
+ return { ok: true, message: action.message ?? message };
460
+ } catch (err) {
461
+ return {
462
+ ok: false,
463
+ message: err instanceof Error ? err.message : String(err),
464
+ };
465
+ }
466
+ }
467
+
468
+ case "blueprint.connect": {
469
+ const graph = deps.getBlueprintGraph();
470
+ if (!graph.getNode(action.sourceId) || !graph.getNode(action.targetId)) {
471
+ return {
472
+ ok: false,
473
+ message: `missing_blueprint_endpoint:${action.sourceId}->${action.targetId}`,
474
+ };
475
+ }
476
+ deps.setBlueprintGraph((g) =>
477
+ g.addEdge({
478
+ source: action.sourceId,
479
+ target: action.targetId,
480
+ sourceHandle: action.sourcePort,
481
+ targetHandle: action.targetPort,
482
+ })
483
+ );
484
+ return {
485
+ ok: true,
486
+ message:
487
+ action.message ??
488
+ `connected ${action.sourceId} -> ${action.targetId}`,
489
+ };
490
+ }
491
+
492
+ case "blueprint.removeNode": {
493
+ if (!deps.getBlueprintGraph().getNode(action.id)) {
494
+ return { ok: false, message: `missing_blueprint_node:${action.id}` };
495
+ }
496
+ deps.setBlueprintGraph((g) => g.removeNode(action.id));
497
+ return { ok: true, message: action.message ?? `removed node ${action.id}` };
498
+ }
499
+
500
+ case "blueprint.updateNode": {
501
+ if (!deps.getBlueprintGraph().getNode(action.id)) {
502
+ return { ok: false, message: `missing_blueprint_node:${action.id}` };
503
+ }
504
+ deps.setBlueprintGraph((g) =>
505
+ g.updateNode(action.id, action.patch as Parameters<BlueprintGraph["updateNode"]>[1])
506
+ );
507
+ return { ok: true, message: action.message ?? `updated node ${action.id}` };
508
+ }
509
+
510
+ case "blueprint.removeEdge": {
511
+ const edges = deps.getBlueprintGraph().document.edges;
512
+ if (!edges.some((e) => e.id === action.id)) {
513
+ return { ok: false, message: `missing_blueprint_edge:${action.id}` };
514
+ }
515
+ deps.setBlueprintGraph((g) => g.removeEdge(action.id));
516
+ return { ok: true, message: action.message ?? `removed edge ${action.id}` };
517
+ }
518
+
519
+ case "blueprint.open": {
520
+ const open = action.open !== false;
521
+ deps.setBlueprintOpen(open);
522
+ deps.focusWorkspace?.(open ? "blueprint" : "view");
523
+ return {
524
+ ok: true,
525
+ message:
526
+ action.message ??
527
+ (open ? "blueprint opened" : "blueprint closed"),
528
+ };
529
+ }
530
+
531
+ case "blueprint.runAll": {
532
+ deps.setBlueprintOpen(true);
533
+ deps.focusWorkspace?.("blueprint");
534
+ await deps.runBlueprintAll();
535
+ return { ok: true, message: action.message ?? "blueprint runAll" };
536
+ }
537
+
538
+ case "workspace.save": {
539
+ try {
540
+ const r = await deps.workspaceSave();
541
+ return {
542
+ ok: true,
543
+ message: action.message ?? `saved workspace ${r.name} (${r.id})`,
544
+ };
545
+ } catch (err) {
546
+ return {
547
+ ok: false,
548
+ message: err instanceof Error ? err.message : String(err),
549
+ };
550
+ }
551
+ }
552
+
553
+ case "workspace.sync": {
554
+ try {
555
+ const name = await deps.workspaceSync();
556
+ return { ok: true, message: action.message ?? `synced ${name}` };
557
+ } catch (err) {
558
+ return {
559
+ ok: false,
560
+ message: err instanceof Error ? err.message : String(err),
561
+ };
562
+ }
563
+ }
564
+
565
+ case "workspace.create": {
566
+ try {
567
+ const r = await deps.workspaceCreate(action.name);
568
+ return {
569
+ ok: true,
570
+ message: action.message ?? `created workspace ${r.name} (${r.id})`,
571
+ };
572
+ } catch (err) {
573
+ return {
574
+ ok: false,
575
+ message: err instanceof Error ? err.message : String(err),
576
+ };
577
+ }
578
+ }
579
+
580
+ case "workspace.open": {
581
+ try {
582
+ await deps.workspaceOpen(action.id);
583
+ return { ok: true, message: action.message ?? `opened ${action.id}` };
584
+ } catch (err) {
585
+ return {
586
+ ok: false,
587
+ message: err instanceof Error ? err.message : String(err),
588
+ };
589
+ }
590
+ }
591
+
592
+ case "workspace.preview": {
593
+ try {
594
+ await deps.workspacePreview(action.id);
595
+ return { ok: true, message: action.message ?? "opened preview" };
596
+ } catch (err) {
597
+ return {
598
+ ok: false,
599
+ message: err instanceof Error ? err.message : String(err),
600
+ };
601
+ }
602
+ }
603
+
604
+ case "io.exportPanel": {
605
+ const json = deps.exportPanelJson();
606
+ return {
607
+ ok: true,
608
+ message:
609
+ action.message ??
610
+ `exported panel JSON (${json.length} chars)\n${json.slice(0, 400)}${json.length > 400 ? "…" : ""}`,
611
+ };
612
+ }
613
+
614
+ case "io.importPanelJson": {
615
+ try {
616
+ const ok = deps.importPanelJson(action.json);
617
+ if (!ok) return { ok: false, message: "import_panel_failed" };
618
+ return { ok: true, message: action.message ?? "imported panel JSON" };
619
+ } catch (err) {
620
+ return {
621
+ ok: false,
622
+ message: err instanceof Error ? err.message : String(err),
623
+ };
624
+ }
625
+ }
626
+
627
+ case "ui.setTheme": {
628
+ deps.setTheme(action.theme);
629
+ return { ok: true, message: action.message ?? `theme ${action.theme}` };
630
+ }
631
+
632
+ case "ui.setLocale": {
633
+ deps.setLocale(action.locale);
634
+ return { ok: true, message: action.message ?? `locale ${action.locale}` };
635
+ }
636
+
637
+ case "ui.setPanelFontSize": {
638
+ deps.setPanelFontSize(action.size);
639
+ return { ok: true, message: action.message ?? `font ${action.size}` };
640
+ }
641
+
642
+ default:
643
+ return { ok: false, message: "unsupported_action" };
644
+ }
645
+ }
646
+
647
+ export async function applyAssistantAction(
648
+ action: AssistantAction,
649
+ deps: ApplyAssistantDeps
650
+ ): Promise<ApplyAssistantResult> {
651
+ return applyOne(action, deps);
652
+ }