@schlenkr/ragents 0.1.0 → 0.1.1

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 (94) hide show
  1. package/apps/server/src/plugin-support/language-server/contract.ts +6 -1
  2. package/apps/server/src/plugin-support/language-server/tools.ts +29 -4
  3. package/apps/server/src/plugin-support/run-scripts.ts +4 -1
  4. package/apps/server/src/plugin-support/start-entries-contract.ts +4 -3
  5. package/apps/server/src/plugin-support/workspace-sandbox-host.ts +2 -0
  6. package/apps/web/src/App.tsx +0 -10
  7. package/apps/web/src/PluginChat.tsx +8 -8
  8. package/apps/web/src/PluginRegistry.tsx +0 -12
  9. package/apps/web/src/StartSurface.tsx +2 -2
  10. package/apps/web/src/ThemeSettings.tsx +1 -1
  11. package/apps/web/src/column/ColumnApp.tsx +83 -7
  12. package/apps/web/src/column/clipboard.ts +64 -0
  13. package/apps/web/src/column/host-contract.ts +10 -0
  14. package/apps/web/src/column.tsx +2 -0
  15. package/apps/web/src/panel/LoginForm.tsx +9 -3
  16. package/apps/web/src/panel/OverviewPage.tsx +187 -62
  17. package/apps/web/src/panel/PanelDialogs.tsx +115 -0
  18. package/apps/web/src/panel/PanelPage.tsx +1 -1
  19. package/apps/web/src/panel/SettingsPage.tsx +75 -79
  20. package/apps/web/src/panel/contract.ts +17 -9
  21. package/apps/web/src/panel/target-state.ts +27 -2
  22. package/apps/web/src/ui/theme.css +4 -0
  23. package/package.json +2 -2
  24. package/packages/ragents/src/plugin-host.ts +5 -2
  25. package/packages/ragents/src/plugin-types.ts +3 -2
  26. package/packages/workspace-executor/src/context.ts +4 -0
  27. package/packages/workspace-executor/src/executor.ts +9 -2
  28. package/packages/workspace-executor/src/index.ts +2 -0
  29. package/packages/workspace-executor/src/language-server/adapters/fsharp.ts +1 -0
  30. package/packages/workspace-executor/src/language-server/adapters/roslyn.ts +1 -0
  31. package/packages/workspace-executor/src/language-server/adapters/typescript.ts +34 -6
  32. package/packages/workspace-executor/src/language-server/contract.ts +7 -3
  33. package/packages/workspace-executor/src/language-server/host.ts +275 -138
  34. package/plugins/ragents.actor-programs/contract.ts +1 -1
  35. package/plugins/ragents.actor-programs/prompt.hbs +4 -4
  36. package/plugins/ragents.actor-programs/server/app-project.ts +0 -2
  37. package/plugins/ragents.actor-programs/server/runtime.ts +1 -1
  38. package/plugins/ragents.actor-programs/web/AppsPanel.tsx +3 -5
  39. package/plugins/ragents.actor-programs/web/EmbeddedApps.tsx +7 -39
  40. package/plugins/ragents.actor-programs/web/api.ts +1 -10
  41. package/plugins/ragents.lsp-roslyn/web/language-server-plugin.tsx +73 -44
  42. package/plugins/ragents.orchestration/canvas.hbs +28 -44
  43. package/plugins/ragents.orchestration/contract.ts +17 -287
  44. package/plugins/ragents.orchestration/server/canvas-tool.ts +18 -123
  45. package/plugins/ragents.orchestration/tiled-layout.ts +12 -4
  46. package/plugins/ragents.orchestration/web/ActorCanvasControls.tsx +23 -47
  47. package/plugins/ragents.orchestration/web/ActorChatPreview.tsx +3 -9
  48. package/plugins/ragents.orchestration/web/ActorShortcut.tsx +3 -5
  49. package/plugins/ragents.orchestration/web/ActorTile.tsx +3 -2
  50. package/plugins/ragents.orchestration/web/CanvasTiles.tsx +3 -3
  51. package/plugins/ragents.orchestration/web/Center.tsx +28 -463
  52. package/plugins/ragents.orchestration/web/MaterialBody.tsx +6 -87
  53. package/plugins/ragents.orchestration/web/TiledCanvas.tsx +2 -3
  54. package/plugins/ragents.orchestration/web/canvas-view-settings.ts +7 -13
  55. package/plugins/ragents.orchestration/web/column/RunColumn.tsx +44 -19
  56. package/plugins/ragents.orchestration/web/column/column-state.ts +13 -4
  57. package/plugins/ragents.orchestration/web/constants.ts +0 -2
  58. package/plugins/ragents.orchestration/web/index.tsx +0 -6
  59. package/plugins/ragents.orchestration/web/tile-docking.ts +7 -1
  60. package/plugins/ragents.orchestration/web/tiled-view-settings.ts +7 -9
  61. package/plugins/ragents.overseer/server/coordinator-prompt.ts +1 -1
  62. package/plugins/ragents.reference/examples.ts +1 -2
  63. package/plugins/ragents.reference/run-scripts/balcony-wizard/RUN.md +1 -1
  64. package/plugins/ragents.reference/run-scripts/balcony-wizard/actors/balcony-app/package.json +1 -1
  65. package/plugins/ragents.reference/run-scripts/balcony-wizard/src/server.ts +1 -1
  66. package/plugins/ragents.reference/run-scripts/balcony-wizard/tests/program.test.ts +2 -2
  67. package/plugins/ragents.reference/run-scripts/conversation-circle/RUN.md +3 -4
  68. package/plugins/ragents.reference/run-scripts/conversation-circle/src/server.ts +9 -15
  69. package/plugins/ragents.reference/run-scripts/conversation-circle/tests/program.test.ts +5 -3
  70. package/plugins/ragents.reference/run-scripts/learning-afternoon/package.json +1 -1
  71. package/plugins/ragents.reference/run-scripts/learning-afternoon/src/server.ts +14 -4
  72. package/plugins/ragents.reference/run-scripts/word-game/src/server.ts +1 -1
  73. package/plugins/ragents.reference/skills/210-moderator-handover/SKILL.md +1 -1
  74. package/plugins/ragents.reference/walkthroughs.ts +14 -14
  75. package/plugins/ragents.workspace/client/workspace-client.ts +4 -1
  76. package/scripts/package/publish-package.ts +4 -5
  77. package/scripts/workspace-client/run-workspace-client.ts +3 -1
  78. package/apps/web/src/MaterialSettings.tsx +0 -40
  79. package/apps/web/src/material-settings.ts +0 -104
  80. package/plugins/ragents.orchestration/web/ActorNet.tsx +0 -607
  81. package/plugins/ragents.orchestration/web/CardSizeSettings.tsx +0 -42
  82. package/plugins/ragents.orchestration/web/ScriptActorCard.tsx +0 -56
  83. package/plugins/ragents.orchestration/web/ZoomSettings.tsx +0 -38
  84. package/plugins/ragents.orchestration/web/canvas-layout-storage.ts +0 -39
  85. package/plugins/ragents.orchestration/web/canvas-layout.ts +0 -326
  86. package/plugins/ragents.orchestration/web/canvas-scene.ts +0 -311
  87. package/plugins/ragents.orchestration/web/card-size-settings.ts +0 -36
  88. package/plugins/ragents.orchestration/web/keyboard-navigation.ts +0 -55
  89. package/plugins/ragents.orchestration/web/material-layout.ts +0 -14
  90. package/plugins/ragents.orchestration/web/useCanvasMeasurements.ts +0 -78
  91. package/plugins/ragents.orchestration/web/zoom-settings.ts +0 -28
  92. package/plugins/ragents.reference/skills/120-canvas-conversation-circle/SKILL.md +0 -12
  93. package/plugins/ragents.reference/skills/130-canvas-pipeline-with-approval/SKILL.md +0 -12
  94. package/plugins/ragents.reference/skills/140-canvas-team-tree-and-topic-grid/SKILL.md +0 -12
@@ -1,34 +1,20 @@
1
1
  import { useAccess } from "@aicontainer/web/AccessContext";
2
- import { useMaterialSettings } from "@aicontainer/web/material-settings";
3
- import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
3
  import { createPortal } from "react-dom";
5
- import { Scan } from "lucide-react";
6
4
  import { CanvasTiles } from "./CanvasTiles";
5
+ import type { CanvasTileNode } from "../tiled-layout";
7
6
  import { CanvasStartup } from "./CanvasStartup";
8
7
  import { canvasStartupState } from "./canvas-startup";
9
8
  import { tileEntities } from "./tile-docking";
10
9
  import { publishStageEntities } from "./canvas-stage";
11
10
  import { initialTileLayout, saveCanvasPresentation, useCanvasPresentation } from "./tiled-view-settings";
12
11
  import { StatusGroup } from "@aicontainer/web/StatusGroup";
13
- import { Alert, AlertDescription } from "@aicontainer/web/ui";
14
- import { NetLayer, type NetBounds } from "./ActorNet";
15
- import { ORCHESTRATION_TAB_ID, statusControlClass, statusOutputClass } from "./constants";
12
+ import { ORCHESTRATION_TAB_ID, statusControlClass } from "./constants";
16
13
  import { runViewFrom } from "./run-view";
17
- import { usePinchZoomSensitivity } from "./zoom-settings";
18
14
  import { ActorCanvasControls, ActorHeaderModeControl } from "./ActorCanvasControls";
19
15
  import { actorVisibleOnCanvas, saveCanvasViewPreferences, useCanvasViewPreferences } from "./canvas-view-settings";
20
16
  import { useCanvasController, type CanvasCenterContext, type EntityReference } from "@aicontainer/web/PluginRegistry";
21
17
 
22
- import { canvasLayoutStorageKey, parseCanvasCamera, type CanvasCamera as Camera } from "./canvas-layout-storage";
23
- import { createKeyboardNavigation, nextCanvasElement, type CanvasNavigationBox } from "./keyboard-navigation";
24
-
25
- const INITIAL_INSET = 32;
26
- const ZOOM_MIN = 0.1;
27
- const ZOOM_MAX = 2.5;
28
-
29
- const wheelPixels = (deltaY: number, deltaMode: number): number =>
30
- deltaY * (deltaMode === 1 ? 18 : deltaMode === 2 ? 120 : 1);
31
-
32
18
  export function OrchestrationCenter(props: CanvasCenterContext) {
33
19
  return <RunOrchestrationCenter key={props.session.session.id} {...props} />;
34
20
  }
@@ -40,23 +26,18 @@ function RunOrchestrationCenter({
40
26
  onCenterModeChange,
41
27
  session,
42
28
  statusContainer,
43
- toolbarContainer,
44
29
  runToolbarContainer,
45
30
  }: CanvasCenterContext) {
46
31
  const inspect = useAccess().can("runs.inspect");
47
32
  const canvas = useCanvasController();
48
- const pinchZoomSensitivity = usePinchZoomSensitivity();
49
33
  if (canvas?.tabId !== ORCHESTRATION_TAB_ID) {
50
34
  throw new Error("Canvas-Beitrag und Canvas-Provider passen nicht zusammen");
51
35
  }
52
- const selectionHandler = useRef(canvas.acceptSelection);
53
- useLayoutEffect(() => { selectionHandler.current = canvas.acceptSelection; }, [canvas.acceptSelection]);
54
36
  const lastRunView = useRef<ReturnType<typeof runViewFrom>>(undefined);
55
37
  const parsedRunView = runViewFrom(session.runView);
56
38
  if (parsedRunView !== undefined) lastRunView.current = parsedRunView;
57
39
  const runView = parsedRunView ?? lastRunView.current;
58
40
  const { programLayout, personalLayout, presentation, programBasis } = useCanvasPresentation(session.session.id, runView);
59
- const tiled = presentation.mode === "tiled";
60
41
  const [presentationError, setPresentationError] = useState<string>();
61
42
  const preferences = useCanvasViewPreferences(session.session.id);
62
43
  const appActorIds = useMemo(() => new Set(canvasElements.flatMap((contribution) => contribution.select(session)
@@ -71,54 +52,25 @@ function RunOrchestrationCenter({
71
52
  const visibleEntities = new Set([
72
53
  ...visibleApps,
73
54
  ...(runView?.actors.filter((actor) => actor.kind !== "human" && actor.lifecycle?.kind !== "stopped"
74
- && (tiled ? inspect || actor.kind === "agent" : !hiddenActorIds.includes(actor.id))).map((actor) => `@${actor.handle}`) ?? []),
55
+ && (inspect || actor.kind === "agent")).map((actor) => `@${actor.handle}`) ?? []),
75
56
  ]);
76
- const hasVisibleContent = tiled ? tileEntities(presentation.root).some((entity) => visibleEntities.has(entity)) : visibleEntities.size > 0;
77
- const stageEntities = useMemo(() => new Set(tiled ? tileEntities(presentation.root) : [
78
- ...visibleApps,
79
- ...(runView?.actors.filter((actor) => actor.kind !== "human" && !hiddenActorIds.includes(actor.id)).map((actor) => `@${actor.handle}`) ?? []),
80
- ]), [tiled, presentation.root, visibleApps, runView, hiddenActorIds]);
57
+ /** Without any stored tree the visible participants are split once; only a real change is saved. */
58
+ const fallbackRoot = useMemo(() => personalLayout === null && programLayout.layout?.root === undefined
59
+ ? initialTileLayout([
60
+ ...visibleApps,
61
+ ...(runView?.actors.filter((actor) => actor.kind !== "human" && actor.lifecycle?.kind !== "stopped" && !hiddenActorIds.includes(actor.id)).map((actor) => `@${actor.handle}`) ?? []),
62
+ ])
63
+ : null, [personalLayout, programLayout, visibleApps, runView, hiddenActorIds]);
64
+ const root = presentation.root ?? fallbackRoot;
65
+ const hasVisibleContent = tileEntities(root).some((entity) => visibleEntities.has(entity));
66
+ const stageEntities = useMemo(() => new Set(tileEntities(root)), [root]);
81
67
  useEffect(() => publishStageEntities(session.session.id, stageEntities), [session.session.id, stageEntities]);
82
68
  useEffect(() => () => publishStageEntities(session.session.id, undefined), [session.session.id]);
83
69
  const startupState = canvasStartupState({ view: runView, startup: session.startup, connected: session.connected,
84
70
  running: session.running, error: session.conversationError });
85
71
  const stageRef = useRef<HTMLDivElement | null>(null);
86
- const { error: materialSettingsError } = useMaterialSettings();
87
- const worldRef = useRef<HTMLDivElement>(null);
88
- const storageKey = canvasLayoutStorageKey(session.session.id, "camera");
89
- const [savedCamera] = useState(() => parseCanvasCamera(window.localStorage.getItem(storageKey)));
90
- const camera = useRef<Camera>(savedCamera ?? { x: INITIAL_INSET, y: INITIAL_INSET, zoom: 1 });
91
- const pendingCamera = useRef<Camera | undefined>(undefined);
92
- const saveTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
93
- const flushCamera = useCallback(() => {
94
- clearTimeout(saveTimer.current);
95
- if (!pendingCamera.current) return;
96
- window.localStorage.setItem(storageKey, JSON.stringify(pendingCamera.current));
97
- pendingCamera.current = undefined;
98
- }, [storageKey]);
99
- useEffect(() => {
100
- window.addEventListener("pagehide", flushCamera);
101
- return () => { window.removeEventListener("pagehide", flushCamera); flushCamera(); };
102
- }, [flushCamera]);
103
- const [overviewCamera, setOverviewCamera] = useState<Camera>();
104
- const [hasScene, setHasScene] = useState<boolean>();
105
- const showStartup = !hasVisibleContent && (tiled || hasScene !== true) && startupState !== undefined;
106
- const overview = overviewCamera !== undefined;
107
- const overviewButtonRef = useRef<HTMLButtonElement>(null);
108
- const cameraSettled = useRef(savedCamera !== undefined);
109
- const boundsRef = useRef<NetBounds | null>(null);
110
- const zoomOutputRef = useRef<HTMLOutputElement>(null);
111
- const bindZoomOutput = useCallback((element: HTMLOutputElement | null) => {
112
- zoomOutputRef.current = element;
113
- if (element) element.textContent = `${Math.round(camera.current.zoom * 100)}%`;
114
- }, []);
115
- useEffect(() => {
116
- onCenterModeChange(canvasReady ? "workflow" : "chat");
117
- if (!canvasReady) {
118
- boundsRef.current = null;
119
- setOverviewCamera(undefined);
120
- }
121
- }, [canvasReady, onCenterModeChange]);
72
+ const showStartup = !hasVisibleContent && startupState !== undefined;
73
+ useEffect(() => { onCenterModeChange(canvasReady ? "workflow" : "chat"); }, [canvasReady, onCenterModeChange]);
122
74
 
123
75
  const visibleStageSize = useCallback((): { width: number; height: number } | undefined => {
124
76
  const element = stageRef.current;
@@ -129,358 +81,28 @@ function RunOrchestrationCenter({
129
81
  return { width: stage.width - Math.max(0, overlap), height: stage.height };
130
82
  }, []);
131
83
 
132
- const renderCamera = useCallback((next: Camera, persist = true) => {
133
- const zoom = Math.min(ZOOM_MAX, Math.max(Number.MIN_VALUE, next.zoom));
134
- camera.current = { x: next.x, y: next.y, zoom };
135
- if (persist) {
136
- pendingCamera.current = camera.current;
137
- clearTimeout(saveTimer.current);
138
- saveTimer.current = setTimeout(flushCamera, 150);
139
- }
140
- const world = worldRef.current;
141
- if (world) {
142
- world.style.setProperty("--cam-x", `${next.x}px`);
143
- world.style.setProperty("--cam-y", `${next.y}px`);
144
- world.style.setProperty("--cam-zoom", String(zoom));
145
- }
146
- const output = zoomOutputRef.current;
147
- if (output) output.textContent = `${Math.round(zoom * 100)}%`;
148
- }, [flushCamera]);
149
-
150
- const keyboardNavigation = useMemo(() => createKeyboardNavigation(() => camera.current, renderCamera), [renderCamera]);
151
- const keyboardTarget = useRef<string | undefined>(undefined);
152
- const cancelKeyboardNavigation = useCallback(() => {
153
- keyboardNavigation.cancel();
154
- keyboardTarget.current = undefined;
155
- }, [keyboardNavigation]);
156
- useEffect(() => () => cancelKeyboardNavigation(), [cancelKeyboardNavigation]);
157
- const applyCamera = useCallback((next: Camera, persist = true) => {
158
- cancelKeyboardNavigation();
159
- renderCamera(next, persist);
160
- }, [cancelKeyboardNavigation, renderCamera]);
161
-
162
- useEffect(() => {
163
- if (tiled || !canvasReady || overview || hasScene === false) return;
164
- const onKeyDown = (event: KeyboardEvent) => {
165
- const element = stageRef.current;
166
- if (!element || event.defaultPrevented || event.isComposing || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey
167
- || element.closest("[inert]") || element.getClientRects().length === 0) return;
168
- const target = event.target;
169
- if (target !== document.body && target !== document.documentElement
170
- && (!(target instanceof Node) || !element.contains(target) || controlUnder(target, element))) return;
171
- const size = visibleStageSize();
172
- if (!size || size.width <= 0 || size.height <= 0) return;
173
- if (!["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key)) return;
174
- event.preventDefault();
175
- const stage = element.getBoundingClientRect();
176
- const centerX = stage.left + size.width / 2;
177
- const centerY = stage.top + size.height / 2;
178
- const elements = Array.from(element.querySelectorAll<HTMLElement>("[data-canvas-navigation-id]"))
179
- .filter((candidate) => candidate.getClientRects().length > 0 && !candidate.closest("[hidden], [inert]"))
180
- .map((candidate): CanvasNavigationBox => {
181
- const box = candidate.getBoundingClientRect();
182
- return { id: candidate.dataset.canvasNavigationId!, left: box.left, top: box.top, right: box.right, bottom: box.bottom };
183
- });
184
- const current = elements.find((candidate) => candidate.id === keyboardTarget.current)
185
- ?? elements.filter((candidate) => candidate.left <= centerX && candidate.right >= centerX && candidate.top <= centerY && candidate.bottom >= centerY)
186
- .sort((a, b) => Math.hypot((a.left + a.right) / 2 - centerX, (a.top + a.bottom) / 2 - centerY)
187
- - Math.hypot((b.left + b.right) / 2 - centerX, (b.top + b.bottom) / 2 - centerY))[0];
188
- const next = nextCanvasElement(elements, current ?? { left: centerX, right: centerX, top: centerY, bottom: centerY }, event.key, current?.id);
189
- if (!next) return;
190
- cameraSettled.current = true;
191
- if (keyboardNavigation.moveTo({
192
- x: camera.current.x + centerX - (next.left + next.right) / 2,
193
- y: camera.current.y + centerY - (next.top + next.bottom) / 2,
194
- zoom: camera.current.zoom,
195
- }, event.repeat, window.matchMedia("(prefers-reduced-motion: reduce)").matches)) keyboardTarget.current = next.id;
196
- };
197
- const cancel = cancelKeyboardNavigation;
198
- window.addEventListener("keydown", onKeyDown);
199
- window.addEventListener("blur", cancel);
200
- document.addEventListener("focusin", cancel);
201
- document.addEventListener("pointerdown", cancel, true);
202
- document.addEventListener("visibilitychange", cancel);
203
- return () => {
204
- window.removeEventListener("keydown", onKeyDown);
205
- window.removeEventListener("blur", cancel);
206
- document.removeEventListener("focusin", cancel);
207
- document.removeEventListener("pointerdown", cancel, true);
208
- document.removeEventListener("visibilitychange", cancel);
209
- cancelKeyboardNavigation();
210
- };
211
- }, [tiled, canvasReady, hasScene, keyboardNavigation, cancelKeyboardNavigation, overview, visibleStageSize]);
212
-
213
- const bindWorld = useCallback((element: HTMLDivElement | null) => {
214
- worldRef.current = element;
215
- if (element) applyCamera(camera.current, false);
216
- }, [applyCamera]);
217
-
218
- const fitBounds = useCallback((bounds: NetBounds, { complete = false, initial = false } = {}): boolean => {
219
- const stage = visibleStageSize();
220
- if (!stage || stage.width <= 0 || stage.height <= 0) return false;
221
- const padding = initial ? INITIAL_INSET : 48;
222
- const width = Math.max(1, bounds.right - bounds.left);
223
- const height = Math.max(1, bounds.bottom - bounds.top);
224
- const zoom = Math.max(complete ? Number.MIN_VALUE : ZOOM_MIN, Math.min(
225
- Math.max(1, stage.width - padding * 2) / width,
226
- Math.max(1, stage.height - padding * 2) / height,
227
- 1));
228
- applyCamera({
229
- x: initial ? padding - bounds.left * zoom : stage.width / 2 - (bounds.left + width / 2) * zoom,
230
- y: initial ? padding - bounds.top * zoom : stage.height / 2 - (bounds.top + height / 2) * zoom,
231
- zoom,
232
- }, !complete);
233
- return true;
234
- }, [applyCamera, visibleStageSize]);
235
-
236
- const fitScene = useCallback(() => {
237
- if (boundsRef.current && fitBounds(boundsRef.current, { initial: true })) cameraSettled.current = true;
238
- }, [fitBounds]);
239
-
240
- const fitInitialScene = useCallback(() => {
241
- if (!cameraSettled.current) fitScene();
242
- }, [fitScene]);
243
-
244
- const centerBounds = useCallback((bounds: NetBounds) => {
245
- const stage = visibleStageSize();
246
- if (!stage || stage.width <= 0 || stage.height <= 0) return;
247
- cameraSettled.current = true;
248
- setOverviewCamera(undefined);
249
- applyCamera({
250
- x: stage.width / 2 - (bounds.left + bounds.right) / 2,
251
- y: stage.height / 2 - (bounds.top + bounds.bottom) / 2,
252
- zoom: 1,
253
- });
254
- }, [applyCamera, visibleStageSize]);
255
-
256
- const resetScene = useCallback(() => {
257
- if (boundsRef.current) centerBounds(boundsRef.current);
258
- }, [centerBounds]);
259
-
260
- const closeOverview = useCallback(() => {
261
- if (!overviewCamera) return;
262
- applyCamera(overviewCamera);
263
- setOverviewCamera(undefined);
264
- overviewButtonRef.current?.focus({ preventScroll: true });
265
- }, [applyCamera, overviewCamera]);
266
-
267
- const toggleOverview = useCallback(() => {
268
- if (overviewCamera) { closeOverview(); return; }
269
- const previous = camera.current;
270
- if (!boundsRef.current || !fitBounds(boundsRef.current, { complete: true })) return;
271
- cameraSettled.current = true;
272
- setOverviewCamera(previous);
273
- }, [closeOverview, fitBounds, overviewCamera]);
274
-
275
- const focusOverviewTarget = useCallback((bounds: NetBounds) => {
276
- centerBounds(bounds);
277
- overviewButtonRef.current?.focus({ preventScroll: true });
278
- }, [centerBounds]);
279
-
280
- useEffect(() => {
281
- if (!overview) return;
282
- const onKeyDown = (event: KeyboardEvent) => {
283
- if (event.key !== "Escape" || event.defaultPrevented || stageRef.current?.closest("[inert]")) return;
284
- event.preventDefault();
285
- closeOverview();
286
- };
287
- window.addEventListener("keydown", onKeyDown);
288
- return () => window.removeEventListener("keydown", onKeyDown);
289
- }, [closeOverview, overview]);
290
-
291
- const requestedEntity = navigation.selectionFor(ORCHESTRATION_TAB_ID);
292
- useEffect(() => {
293
- if (tiled || !requestedEntity || typeof requestedEntity !== "object"
294
- || !("type" in requestedEntity) || !("id" in requestedEntity)) return;
295
- const frame = requestAnimationFrame(() => {
296
- const stage = stageRef.current;
297
- const element = Array.from(stage?.querySelectorAll<HTMLElement>("[data-canvas-entity-id]") ?? [])
298
- .find((candidate) => candidate.dataset.canvasEntityId === requestedEntity.id
299
- && candidate.dataset.canvasEntityType === requestedEntity.type);
300
- if (!stage || !element) return;
301
- const bounds = element.getBoundingClientRect();
302
- const viewport = stage.getBoundingClientRect();
303
- const { x, y, zoom } = camera.current;
304
- const left = (bounds.left - viewport.left - x) / zoom;
305
- const top = (bounds.top - viewport.top - y) / zoom;
306
- if (fitBounds({ left, top, right: left + bounds.width / zoom, bottom: top + bounds.height / zoom })) {
307
- cameraSettled.current = true;
308
- setOverviewCamera(undefined);
309
- }
310
- });
311
- return () => cancelAnimationFrame(frame);
312
- }, [tiled, requestedEntity, fitBounds]);
313
-
314
- const zoomAtStagePoint = useCallback((deltaY: number, deltaMode: number, stageX: number, stageY: number, sensitivity = 1) => {
315
- const current = camera.current;
316
- const worldX = (stageX - current.x) / current.zoom;
317
- const worldY = (stageY - current.y) / current.zoom;
318
- const zoom = Math.min(ZOOM_MAX, Math.max(Math.min(ZOOM_MIN, current.zoom), current.zoom * Math.exp(-wheelPixels(deltaY, deltaMode) * 0.0007 * sensitivity)));
319
- applyCamera({ x: stageX - worldX * zoom, y: stageY - worldY * zoom, zoom });
320
- }, [applyCamera]);
321
-
322
- const panBy = useCallback((dx: number, dy: number) => {
323
- const current = camera.current;
324
- applyCamera({ x: current.x + dx, y: current.y + dy, zoom: current.zoom });
325
- }, [applyCamera]);
326
-
327
- const zoomStep = useCallback((direction: number) => {
328
- const stage = visibleStageSize();
329
- if (!stage || stage.width <= 0 || stage.height <= 0) return;
330
- cameraSettled.current = true;
331
- setOverviewCamera(undefined);
332
- zoomAtStagePoint(direction > 0 ? -400 : 400, 0, stage.width / 2, stage.height / 2);
333
- }, [visibleStageSize, zoomAtStagePoint]);
334
-
335
84
  useEffect(() => {
336
85
  const element = stageRef.current;
337
86
  if (!element) return;
338
87
  const observer = new ResizeObserver(() => {
339
88
  const size = visibleStageSize();
340
89
  if (size) element.style.setProperty("--canvas-visible-width", `${size.width}px`);
341
- if (tiled) return;
342
- if (overview && boundsRef.current) fitBounds(boundsRef.current, { complete: true });
343
- else fitInitialScene();
344
90
  });
345
91
  observer.observe(element);
346
92
  const panel = document.getElementById("workspace-panel");
347
93
  if (panel) observer.observe(panel);
348
94
  return () => observer.disconnect();
349
- }, [tiled, fitBounds, fitInitialScene, overview, visibleStageSize]);
95
+ }, [visibleStageSize]);
350
96
 
351
- const handleLayout = useCallback((bounds: NetBounds) => {
352
- const visible = bounds.right > bounds.left || bounds.bottom > bounds.top;
353
- setHasScene(visible);
354
- if (!visible) {
355
- boundsRef.current = null;
356
- setOverviewCamera(undefined);
357
- return;
358
- }
359
- boundsRef.current = bounds;
360
- if (overview) fitBounds(bounds, { complete: true });
361
- else fitInitialScene();
362
- }, [fitBounds, fitInitialScene, overview]);
97
+ const requestedEntity = navigation.selectionFor(ORCHESTRATION_TAB_ID);
363
98
 
364
99
  const handleSelect = useCallback((selection: EntityReference) => {
365
100
  canvas?.acceptSelection(selection);
366
101
  }, [canvas]);
367
102
 
368
- const stageCleanup = useRef<(() => void) | undefined>(undefined);
369
- const bindStage = useCallback((element: HTMLDivElement | null) => {
370
- stageCleanup.current?.();
371
- stageCleanup.current = undefined;
372
- stageRef.current = element;
373
- if (!element || tiled) return;
374
- let panCleanup: (() => void) | undefined;
375
-
376
- const onWheel = (event: WheelEvent) => {
377
- if (event.defaultPrevented || (!overview && controlUnder(event.target, element))) return;
378
- event.preventDefault();
379
- setOverviewCamera(undefined);
380
- const rect = element.getBoundingClientRect();
381
- cameraSettled.current = true;
382
- zoomAtStagePoint(event.deltaY, event.deltaMode, event.clientX - rect.left, event.clientY - rect.top,
383
- event.ctrlKey ? pinchZoomSensitivity : 1);
384
- };
385
-
386
- const onPointerDown = (event: PointerEvent) => {
387
- if (overview) return;
388
- if (event.defaultPrevented || controlUnder(event.target, element)) return;
389
- const middleButton = event.button === 1;
390
- const leftButton = event.button === 0;
391
- if (!middleButton && !leftButton) return;
392
- event.preventDefault();
393
- cancelKeyboardNavigation();
394
- element.focus({ preventScroll: true });
395
- panCleanup?.();
396
- const startX = event.clientX;
397
- const startY = event.clientY;
398
- let lastX = event.clientX;
399
- let lastY = event.clientY;
400
- let dragged = false;
401
- let active = true;
402
- element.setPointerCapture(event.pointerId);
403
- const move = (moved: PointerEvent) => {
404
- if (!active || moved.pointerId !== event.pointerId) return;
405
- dragged ||= Math.hypot(moved.clientX - startX, moved.clientY - startY) > 4;
406
- cameraSettled.current = true;
407
- panBy(moved.clientX - lastX, moved.clientY - lastY);
408
- lastX = moved.clientX;
409
- lastY = moved.clientY;
410
- };
411
- const cleanup = () => {
412
- if (!active) return;
413
- active = false;
414
- element.removeEventListener("pointermove", move);
415
- element.removeEventListener("pointerup", end);
416
- element.removeEventListener("pointercancel", end);
417
- element.removeEventListener("lostpointercapture", end);
418
- if (element.hasPointerCapture(event.pointerId)) element.releasePointerCapture(event.pointerId);
419
- panCleanup = undefined;
420
- };
421
- const end = (ended: PointerEvent) => {
422
- if (!active || ended.pointerId !== event.pointerId) return;
423
- cleanup();
424
- if (ended.type === "pointerup" && leftButton && !dragged) selectionHandler.current(undefined);
425
- };
426
- panCleanup = cleanup;
427
- element.addEventListener("pointermove", move);
428
- element.addEventListener("pointerup", end);
429
- element.addEventListener("pointercancel", end);
430
- element.addEventListener("lostpointercapture", end);
431
- };
432
-
433
- const onDoubleClick = (event: MouseEvent) => {
434
- if (overview) return;
435
- if (event.defaultPrevented || controlUnder(event.target, element)) return;
436
- resetScene();
437
- };
438
-
439
- const onMiddleDown = (event: MouseEvent) => {
440
- if (event.defaultPrevented || controlUnder(event.target, element)) return;
441
- if (event.button === 1) event.preventDefault();
442
- };
443
-
444
- const onScroll = () => {
445
- element.scrollLeft = 0;
446
- element.scrollTop = 0;
447
- };
448
-
449
- element.addEventListener("wheel", onWheel, { passive: false });
450
- element.addEventListener("pointerdown", onPointerDown);
451
- element.addEventListener("dblclick", onDoubleClick);
452
- element.addEventListener("mousedown", onMiddleDown);
453
- element.addEventListener("auxclick", onMiddleDown);
454
- element.addEventListener("scroll", onScroll);
455
- onScroll();
456
- stageCleanup.current = () => {
457
- panCleanup?.();
458
- element.removeEventListener("wheel", onWheel);
459
- element.removeEventListener("pointerdown", onPointerDown);
460
- element.removeEventListener("dblclick", onDoubleClick);
461
- element.removeEventListener("mousedown", onMiddleDown);
462
- element.removeEventListener("auxclick", onMiddleDown);
463
- element.removeEventListener("scroll", onScroll);
464
- };
465
- }, [tiled, resetScene, overview, panBy, pinchZoomSensitivity, zoomAtStagePoint, cancelKeyboardNavigation]);
466
-
467
- useEffect(() => {
468
- if (!tiled) return;
469
- cancelKeyboardNavigation();
470
- if (overviewCamera) {
471
- applyCamera(overviewCamera);
472
- setOverviewCamera(undefined);
473
- }
474
- }, [tiled, overviewCamera, applyCamera, cancelKeyboardNavigation]);
475
-
476
- const changeMode = (mode: "free" | "tiled") => {
477
- if (mode === presentation.mode) return;
478
- const entities = [
479
- ...canvasElements.flatMap((contribution) => contribution.select(session).filter((element) => element.visible !== false).map((element) => `app:${element.id}`)),
480
- ...(runView?.actors.filter((actor) => actor.kind !== "human" && actor.lifecycle?.kind !== "stopped" && !hiddenActorIds.includes(actor.id)).map((actor) => `@${actor.handle}`) ?? []),
481
- ];
103
+ const changeTiles = (next: CanvasTileNode | null) => {
482
104
  try {
483
- saveCanvasPresentation(session.session.id, { mode, programBasis, root: presentation.root ?? (personalLayout === null && programLayout.layout?.root === undefined ? initialTileLayout(entities) : null) });
105
+ saveCanvasPresentation(session.session.id, { root: next, programBasis });
484
106
  setPresentationError(undefined);
485
107
  } catch (error) {
486
108
  setPresentationError(error instanceof Error ? error.message : String(error));
@@ -488,69 +110,23 @@ function RunOrchestrationCenter({
488
110
  };
489
111
 
490
112
  return (
491
- <div className="relative flex h-full min-h-0 min-w-0 flex-1 overflow-clip" data-view={tiled ? "tiled" : "net"}>
492
- {materialSettingsError && <Alert className="absolute inset-x-4.5 bottom-4.5 z-30 w-auto" role="status" variant="destructive"><AlertDescription className="text-destructive">{materialSettingsError}</AlertDescription></Alert>}
493
- {toolbarContainer && createPortal(<button
494
- aria-label="Canvas-Übersicht"
495
- aria-pressed={overview}
496
- className="inline-flex size-header min-h-header items-center justify-center border-r border-border bg-transparent text-primary transition-colors hover:not-disabled:bg-accent focus-visible:outline-2 focus-visible:outline-primary focus-visible:-outline-offset-3 aria-expanded:bg-primary aria-expanded:text-primary-foreground aria-pressed:bg-primary aria-pressed:text-primary-foreground disabled:opacity-45"
497
- disabled={tiled || !canvasReady || hasScene === false}
498
- onClick={toggleOverview}
499
- ref={overviewButtonRef}
500
- title={tiled ? "Die Canvas-Übersicht ist im Kachelmodus nicht verfügbar" : overview ? "Canvas-Übersicht beenden (Escape)" : "Alle Elemente anzeigen und anspringen"}
501
- type="button"
502
- ><Scan aria-hidden size={22} /></button>, toolbarContainer)}
503
- <StatusGroup container={statusContainer} label="Diagramm steuern" order={10}>
504
- <button aria-label="Verkleinern" className={statusControlClass} disabled={tiled} onClick={() => zoomStep(-1)} type="button">-</button>
505
- <output aria-label={tiled ? "Feste Kachelfläche" : "Zoomstufe"} className={statusOutputClass} ref={tiled ? undefined : bindZoomOutput}>{tiled ? "Fest" : "100%"}</output>
506
- <button aria-label="Vergrößern" className={statusControlClass} disabled={tiled} onClick={() => zoomStep(1)} type="button">+</button>
507
- <button className={statusControlClass} disabled={tiled} onClick={resetScene} title="Inhalt bei 100 Prozent zentrieren" type="button">Einpassen</button>
508
- </StatusGroup>
509
- <StatusGroup container={statusContainer} label="Flächenmodus" order={20}>
510
- <button className={statusControlClass} aria-pressed={!tiled} onClick={() => changeMode("free")} type="button">Frei</button>
511
- <button className={statusControlClass} aria-pressed={tiled} onClick={() => changeMode("tiled")} type="button">Kacheln</button>
512
- {personalLayout && <button className={statusControlClass} onClick={() => saveCanvasPresentation(session.session.id, null)}
513
- title="Eigene Anordnung zurücksetzen und der Programmanordnung folgen" type="button">Programmvorgabe übernehmen</button>}
514
- </StatusGroup>
515
- {inspect && !tiled && <StatusGroup container={statusContainer} label="Canvas-Ansicht" order={30}>
516
- <ActorCanvasControls control="view" session={session} appActorIds={appActorIds} preferences={preferences}
517
- onPreferencesChange={(next) => saveCanvasViewPreferences(session.session.id, next)} />
113
+ <div className="relative flex h-full min-h-0 min-w-0 flex-1 overflow-clip" data-view="tiled">
114
+ {personalLayout && <StatusGroup container={statusContainer} label="Kachelfläche" order={20}>
115
+ <button className={statusControlClass} onClick={() => saveCanvasPresentation(session.session.id, null)}
116
+ title="Eigene Anordnung zurücksetzen und der Programmanordnung folgen" type="button">Programmvorgabe übernehmen</button>
518
117
  </StatusGroup>}
519
- {runToolbarContainer && createPortal(<><ActorHeaderModeControl runId={session.session.id} />{inspect && <ActorCanvasControls control="actors" session={session}
118
+ {runToolbarContainer && createPortal(<><ActorHeaderModeControl runId={session.session.id} />{inspect && <ActorCanvasControls session={session}
520
119
  appActorIds={appActorIds} preferences={preferences}
521
120
  onPreferencesChange={(next) => saveCanvasViewPreferences(session.session.id, next)} />}</>, runToolbarContainer)}
522
- <div aria-label={tiled ? "Kachelfläche" : "Canvas, mit Pfeiltasten zum nächsten Element"} className="relative min-h-0 min-w-0 flex-[1_1_360px] overflow-clip bg-[image:var(--canvas-backdrop)]" ref={bindStage} tabIndex={0}>
121
+ <div aria-label="Kachelfläche" className="relative min-h-0 min-w-0 flex-[1_1_360px] overflow-clip bg-[image:var(--canvas-backdrop)]" ref={stageRef}>
523
122
  <CanvasGrain />
524
123
  {(programLayout.error || presentationError) && <p className="absolute inset-x-2 top-2 z-10 bg-secondary p-3 text-destructive" role="alert">{programLayout.error || presentationError}</p>}
525
- {tiled && runView && !showStartup && <div className="absolute inset-y-0 left-0 w-[var(--canvas-visible-width,100%)] overflow-hidden">
526
- <CanvasTiles root={presentation.root} onChange={(root) => saveCanvasPresentation(session.session.id, { mode: "tiled", root, programBasis })}
124
+ {runView && !showStartup && <div className="absolute inset-y-0 left-0 w-[var(--canvas-visible-width,100%)] overflow-hidden">
125
+ <CanvasTiles root={root} onChange={changeTiles}
527
126
  canvasElements={canvasElements} cardSections={cardSections} session={session} view={runView} navigation={navigation} onSelect={handleSelect}
528
127
  selected={requestedEntity && typeof requestedEntity === "object" && "type" in requestedEntity && "id" in requestedEntity ? requestedEntity as EntityReference : undefined} />
529
128
  </div>}
530
- {!tiled && <div className="absolute top-0 left-0 h-0 w-0 origin-top-left overflow-visible [transform:translate(var(--cam-x,0px),var(--cam-y,0px))_scale(var(--cam-zoom,1))]" ref={bindWorld}>
531
- {canvasReady && runView && (
532
- <NetLayer
533
- cardSections={cardSections}
534
- canvasElements={canvasElements}
535
- hiddenActorIds={hiddenActorIds}
536
- showConnections={inspect && preferences.showConnections}
537
- navigation={navigation}
538
- onLayout={handleLayout}
539
- onSelect={handleSelect}
540
- onOverviewTarget={overview ? focusOverviewTarget : undefined}
541
- selectedActorId={canvas?.selection?.type === "actor" ? canvas.selection.id : undefined}
542
- session={session}
543
- view={runView}
544
- />
545
- )}
546
- </div>}
547
- {!tiled && overview && <p className="pointer-events-none absolute top-3 left-3 max-w-[calc(var(--canvas-visible-width,100%)-24px)] rounded-lg border border-border bg-card px-3 py-2 text-[.75rem] text-foreground" role="status">Element anklicken zum Anspringen. Escape bricht ab.</p>}
548
129
  {showStartup && <CanvasStartup state={startupState} />}
549
- {!showStartup && !tiled && (!canvasReady || hasScene === false) && (
550
- <p className="pointer-events-none absolute inset-0 grid place-items-center text-[0.85rem] text-muted-foreground">{canvasReady
551
- ? "Keine Elemente eingeblendet. Über Actors oder Ansicht kannst du die Darstellung ändern."
552
- : "Die Fläche füllt sich, sobald der Run Actors hat."}</p>
553
- )}
554
130
  </div>
555
131
  </div>
556
132
  );
@@ -563,14 +139,3 @@ function CanvasGrain() {
563
139
  <path d="M0 0h180v180H0z" fill="#887890" filter="url(#canvas-grain)" opacity=".32" />
564
140
  </svg>;
565
141
  }
566
-
567
- function controlUnder(target: EventTarget | null, limit: HTMLElement): boolean {
568
- for (let node = target instanceof Element ? target : null; node; node = node.parentElement) {
569
- if (node.matches("[data-canvas-card], [data-canvas-scroll], iframe, a, button, input, select, textarea, [contenteditable], [role=button], [role=slider], [role=spinbutton]")) return true;
570
- const style = getComputedStyle(node);
571
- if ((style.overflowY === "auto" || style.overflowY === "scroll")
572
- || (style.overflowX === "auto" || style.overflowX === "scroll")) return true;
573
- if (node === limit) break;
574
- }
575
- return false;
576
- }