@schlenkr/ragents 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +10 -4
  2. package/apps/server/src/plugin-support/language-server/contract.ts +6 -1
  3. package/apps/server/src/plugin-support/language-server/tools.ts +29 -4
  4. package/apps/server/src/plugin-support/run-scripts.ts +4 -1
  5. package/apps/server/src/plugin-support/start-entries-contract.ts +4 -3
  6. package/apps/server/src/plugin-support/workspace-sandbox-host.ts +2 -0
  7. package/apps/server/src/profile-target.ts +25 -0
  8. package/apps/server/src/web-build.ts +82 -0
  9. package/apps/server/tsconfig.json +38 -0
  10. package/apps/web/column.html +12 -0
  11. package/apps/web/index.html +12 -0
  12. package/apps/web/src/App.tsx +0 -10
  13. package/apps/web/src/PluginChat.tsx +8 -8
  14. package/apps/web/src/PluginRegistry.tsx +0 -12
  15. package/apps/web/src/StartSurface.tsx +2 -2
  16. package/apps/web/src/ThemeSettings.tsx +1 -1
  17. package/apps/web/src/column/ColumnApp.tsx +83 -7
  18. package/apps/web/src/column/clipboard.ts +64 -0
  19. package/apps/web/src/column/host-contract.ts +10 -0
  20. package/apps/web/src/column.tsx +2 -0
  21. package/apps/web/src/panel/LoginForm.tsx +9 -3
  22. package/apps/web/src/panel/OverviewPage.tsx +187 -62
  23. package/apps/web/src/panel/PanelDialogs.tsx +115 -0
  24. package/apps/web/src/panel/PanelPage.tsx +1 -1
  25. package/apps/web/src/panel/SettingsPage.tsx +75 -79
  26. package/apps/web/src/panel/contract.ts +17 -9
  27. package/apps/web/src/panel/target-state.ts +27 -2
  28. package/apps/web/src/ui/theme.css +4 -0
  29. package/apps/web/tsconfig.json +41 -0
  30. package/apps/web/vite.config.ts +140 -0
  31. package/package.json +6 -2
  32. package/packages/ragents/src/plugin-host.ts +5 -2
  33. package/packages/ragents/src/plugin-types.ts +3 -2
  34. package/packages/workspace-executor/src/context.ts +4 -0
  35. package/packages/workspace-executor/src/executor.ts +9 -2
  36. package/packages/workspace-executor/src/index.ts +2 -0
  37. package/packages/workspace-executor/src/language-server/adapters/fsharp.ts +1 -0
  38. package/packages/workspace-executor/src/language-server/adapters/roslyn.ts +1 -0
  39. package/packages/workspace-executor/src/language-server/adapters/typescript.ts +34 -6
  40. package/packages/workspace-executor/src/language-server/contract.ts +7 -3
  41. package/packages/workspace-executor/src/language-server/host.ts +275 -138
  42. package/plugins/ragents.actor-programs/contract.ts +1 -1
  43. package/plugins/ragents.actor-programs/prompt.hbs +4 -4
  44. package/plugins/ragents.actor-programs/server/app-project.ts +0 -2
  45. package/plugins/ragents.actor-programs/server/runtime.ts +1 -1
  46. package/plugins/ragents.actor-programs/web/AppsPanel.tsx +3 -5
  47. package/plugins/ragents.actor-programs/web/EmbeddedApps.tsx +7 -39
  48. package/plugins/ragents.actor-programs/web/api.ts +1 -10
  49. package/plugins/ragents.lsp-roslyn/web/language-server-plugin.tsx +73 -44
  50. package/plugins/ragents.orchestration/canvas.hbs +28 -44
  51. package/plugins/ragents.orchestration/contract.ts +17 -287
  52. package/plugins/ragents.orchestration/server/canvas-tool.ts +18 -123
  53. package/plugins/ragents.orchestration/tiled-layout.ts +12 -4
  54. package/plugins/ragents.orchestration/web/ActorCanvasControls.tsx +23 -47
  55. package/plugins/ragents.orchestration/web/ActorChatPreview.tsx +3 -9
  56. package/plugins/ragents.orchestration/web/ActorShortcut.tsx +3 -5
  57. package/plugins/ragents.orchestration/web/ActorTile.tsx +3 -2
  58. package/plugins/ragents.orchestration/web/CanvasTiles.tsx +3 -3
  59. package/plugins/ragents.orchestration/web/Center.tsx +28 -463
  60. package/plugins/ragents.orchestration/web/MaterialBody.tsx +6 -87
  61. package/plugins/ragents.orchestration/web/TiledCanvas.tsx +2 -3
  62. package/plugins/ragents.orchestration/web/canvas-view-settings.ts +7 -13
  63. package/plugins/ragents.orchestration/web/column/RunColumn.tsx +44 -19
  64. package/plugins/ragents.orchestration/web/column/column-state.ts +13 -4
  65. package/plugins/ragents.orchestration/web/constants.ts +0 -2
  66. package/plugins/ragents.orchestration/web/index.tsx +0 -6
  67. package/plugins/ragents.orchestration/web/tile-docking.ts +7 -1
  68. package/plugins/ragents.orchestration/web/tiled-view-settings.ts +7 -9
  69. package/plugins/ragents.overseer/server/coordinator-prompt.ts +1 -1
  70. package/plugins/ragents.profile-distribution/server/web.ts +3 -27
  71. package/plugins/ragents.reference/examples.ts +1 -2
  72. package/plugins/ragents.reference/run-scripts/balcony-wizard/RUN.md +1 -1
  73. package/plugins/ragents.reference/run-scripts/balcony-wizard/actors/balcony-app/package.json +1 -1
  74. package/plugins/ragents.reference/run-scripts/balcony-wizard/src/server.ts +1 -1
  75. package/plugins/ragents.reference/run-scripts/balcony-wizard/tests/program.test.ts +2 -2
  76. package/plugins/ragents.reference/run-scripts/conversation-circle/RUN.md +3 -4
  77. package/plugins/ragents.reference/run-scripts/conversation-circle/src/server.ts +9 -15
  78. package/plugins/ragents.reference/run-scripts/conversation-circle/tests/program.test.ts +5 -3
  79. package/plugins/ragents.reference/run-scripts/learning-afternoon/package.json +1 -1
  80. package/plugins/ragents.reference/run-scripts/learning-afternoon/src/server.ts +14 -4
  81. package/plugins/ragents.reference/run-scripts/word-game/src/server.ts +1 -1
  82. package/plugins/ragents.reference/skills/210-moderator-handover/SKILL.md +1 -1
  83. package/plugins/ragents.reference/walkthroughs.ts +14 -14
  84. package/plugins/ragents.workspace/client/workspace-client.ts +4 -1
  85. package/scripts/agent/agent-cli.ts +9 -24
  86. package/scripts/package/build-package.ts +18 -5
  87. package/scripts/package/publish-package.ts +24 -10
  88. package/scripts/package/ragents.mjs +3 -2
  89. package/scripts/remote/connect.ts +16 -5
  90. package/scripts/remote/start.ts +33 -15
  91. package/scripts/workspace-client/run-workspace-client.ts +3 -1
  92. package/apps/web/src/MaterialSettings.tsx +0 -40
  93. package/apps/web/src/material-settings.ts +0 -104
  94. package/plugins/ragents.orchestration/web/ActorNet.tsx +0 -607
  95. package/plugins/ragents.orchestration/web/CardSizeSettings.tsx +0 -42
  96. package/plugins/ragents.orchestration/web/ScriptActorCard.tsx +0 -56
  97. package/plugins/ragents.orchestration/web/ZoomSettings.tsx +0 -38
  98. package/plugins/ragents.orchestration/web/canvas-layout-storage.ts +0 -39
  99. package/plugins/ragents.orchestration/web/canvas-layout.ts +0 -326
  100. package/plugins/ragents.orchestration/web/canvas-scene.ts +0 -311
  101. package/plugins/ragents.orchestration/web/card-size-settings.ts +0 -36
  102. package/plugins/ragents.orchestration/web/keyboard-navigation.ts +0 -55
  103. package/plugins/ragents.orchestration/web/material-layout.ts +0 -14
  104. package/plugins/ragents.orchestration/web/useCanvasMeasurements.ts +0 -78
  105. package/plugins/ragents.orchestration/web/zoom-settings.ts +0 -28
  106. package/plugins/ragents.reference/skills/120-canvas-conversation-circle/SKILL.md +0 -12
  107. package/plugins/ragents.reference/skills/130-canvas-pipeline-with-approval/SKILL.md +0 -12
  108. package/plugins/ragents.reference/skills/140-canvas-team-tree-and-topic-grid/SKILL.md +0 -12
@@ -2,87 +2,20 @@ import { canvasTileNodeOf } from "./tiled-layout.js";
2
2
 
3
3
  export const ORCHESTRATION_PLUGIN_ID = "ragents.orchestration";
4
4
 
5
- export const CANVAS_GROUP_KINDS = ["h", "v", "wrap-h", "wrap-v", "grid", "circle", "tree"] as const;
6
- export type CanvasGroupKind = typeof CANVAS_GROUP_KINDS[number];
7
-
8
- export const CANVAS_TREE_DIRECTIONS = ["right", "down"] as const;
9
- export type CanvasTreeDirection = typeof CANVAS_TREE_DIRECTIONS[number];
10
-
11
- export const CANVAS_SHAPE_KINDS = ["circle", "diamond"] as const;
12
- export type CanvasShapeKind = typeof CANVAS_SHAPE_KINDS[number];
13
-
14
- export const CANVAS_ARROWS = ["none", "end", "both"] as const;
15
- export type CanvasArrow = typeof CANVAS_ARROWS[number];
16
-
17
- export const CANVAS_LINE_STYLES = ["solid", "dashed"] as const;
18
- export type CanvasLineStyle = typeof CANVAS_LINE_STYLES[number];
19
-
20
- /** Default sizes of the shapes in px; a circle is measured by its diameter, a diamond by its width. */
21
- export const CANVAS_SHAPE_DEFAULT_SIZE: Record<CanvasShapeKind, number> = { circle: 120, diamond: 150 };
22
- const DIAMOND_ASPECT = 0.62;
23
-
24
5
  export type CanvasEntity =
25
6
  | { kind: "actor"; handle: string }
26
- | { kind: "shape"; id: string }
27
7
  | { kind: "app"; id: string };
28
8
 
29
- type CanvasGroupBase = {
30
- id: string;
31
- parent?: string;
32
- label?: string;
33
- gap?: number;
34
- frame: boolean;
35
- };
36
-
37
- /** The model sends one flat object per node; internally each group kind carries exactly its own parameters. */
38
- export type CanvasGroupNode =
39
- | (CanvasGroupBase & { group: "h" | "v" | "circle" })
40
- | (CanvasGroupBase & { group: "wrap-h"; width: number })
41
- | (CanvasGroupBase & { group: "wrap-v"; height: number })
42
- | (CanvasGroupBase & { group: "grid"; columns: number })
43
- | (CanvasGroupBase & { group: "tree"; root: string; direction: CanvasTreeDirection });
44
-
45
- export type CanvasItemNode = {
46
- entity: string;
47
- parent?: string;
48
- chatInput?: boolean;
49
- };
50
-
51
- export type CanvasLayoutNode = CanvasGroupNode | CanvasItemNode;
52
-
53
- export type CanvasShape = {
54
- id: string;
55
- kind: CanvasShapeKind;
56
- text: string;
57
- size: number;
58
- };
59
-
60
- export type CanvasLine = {
61
- from: string;
62
- to: string;
63
- label?: string;
64
- arrow: CanvasArrow;
65
- style: CanvasLineStyle;
66
- };
67
-
68
9
  export type CanvasTileNode =
69
- | { entity: string }
10
+ | { entity: string; chatInput?: boolean }
70
11
  | { direction: "horizontal" | "vertical"; weights: [number, number]; children: [CanvasTileNode, CanvasTileNode] };
71
12
 
72
13
  export type CanvasLayout = {
73
- mode?: "free" | "tiled";
74
- root?: CanvasTileNode | null;
75
- nodes: CanvasLayoutNode[];
76
- shapes: CanvasShape[];
77
- lines: CanvasLine[];
14
+ root: CanvasTileNode | null;
78
15
  };
79
16
 
80
- export const isCanvasGroupNode = (node: CanvasLayoutNode): node is CanvasGroupNode => "group" in node;
81
-
82
17
  const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
83
18
 
84
- const isAbsent = (value: unknown) => value === undefined || value === null;
85
-
86
19
  const normalizedHandle = (value: string) => value.trim().replace(/^@/, "").toLowerCase();
87
20
 
88
21
  export const canvasEntityOf = (text: string): CanvasEntity => {
@@ -92,231 +25,28 @@ export const canvasEntityOf = (text: string): CanvasEntity => {
92
25
  if (!handle || /\s/.test(handle)) throw new Error(`Entität ${JSON.stringify(text)}: nach dem @ fehlt ein Handle`);
93
26
  return { kind: "actor", handle };
94
27
  }
95
- const match = /^(shape|app):(.+)$/s.exec(trimmed);
28
+ const match = /^app:(.+)$/s.exec(trimmed);
96
29
  if (!match) {
97
- throw new Error(`Entität ${JSON.stringify(text)}: erwartet @handle, shape:<id> oder app:<id>`);
30
+ throw new Error(`Entität ${JSON.stringify(text)}: erwartet @handle oder app:<id>`);
98
31
  }
99
- const id = match[2].trim();
32
+ const id = match[1].trim();
100
33
  if (!id) throw new Error(`Entität ${JSON.stringify(text)}: die ID nach dem Doppelpunkt ist leer`);
101
- return match[1] === "shape" ? { kind: "shape", id } : { kind: "app", id };
34
+ return { kind: "app", id };
102
35
  };
103
36
 
104
37
  export const canvasEntityKey = (entity: CanvasEntity): string =>
105
- entity.kind === "actor" ? `@${entity.handle}` : `${entity.kind}:${entity.id}`;
106
-
107
- export const canvasShapeSize = (shape: CanvasShape): { width: number; height: number } =>
108
- shape.kind === "circle"
109
- ? { width: shape.size, height: shape.size }
110
- : { width: shape.size, height: Math.round(shape.size * DIAMOND_ASPECT) };
111
-
112
- const oneOf = <T extends string>(allowed: readonly T[], value: unknown, where: string): T => {
113
- if (typeof value !== "string" || !allowed.includes(value as T)) {
114
- throw new Error(`${where}: ${JSON.stringify(value)} ist unbekannt, erlaubt sind ${allowed.join(", ")}`);
115
- }
116
- return value as T;
117
- };
38
+ entity.kind === "actor" ? `@${entity.handle}` : `app:${entity.id}`;
118
39
 
119
- const optionalText = (value: unknown, where: string): string | undefined => {
120
- if (isAbsent(value)) return undefined;
121
- if (typeof value !== "string") throw new Error(`${where} muss ein Text sein`);
122
- return value.trim() || undefined;
123
- };
124
-
125
- const requiredText = (value: unknown, where: string): string => {
126
- if (typeof value !== "string" || !value.trim()) throw new Error(`${where} fehlt oder ist leer`);
127
- return value.trim();
128
- };
129
-
130
- const positiveNumber = (value: unknown, where: string): number => {
131
- if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) throw new Error(`${where} muss eine positive Zahl sein`);
132
- return value;
133
- };
134
-
135
- const optionalGap = (value: unknown, where: string): number | undefined => {
136
- if (isAbsent(value)) return undefined;
137
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0) throw new Error(`${where} darf nicht negativ sein`);
138
- return value;
139
- };
40
+ const LEGACY_KEYS = ["nodes", "shapes", "lines", "mode"] as const;
140
41
 
141
- const withoutUndefined = <T extends object>(value: T): T =>
142
- Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)) as T;
143
-
144
- const GROUP_PARAMETERS: Record<CanvasGroupKind, readonly string[]> = {
145
- "h": [],
146
- "v": [],
147
- "circle": [],
148
- "wrap-h": ["width"],
149
- "wrap-v": ["height"],
150
- "grid": ["columns"],
151
- "tree": ["direction", "root"],
152
- };
153
-
154
- const groupNodeOf = (value: Record<string, unknown>, index: number): CanvasGroupNode => {
155
- const where = `nodes[${index}]`;
156
- const group = oneOf(CANVAS_GROUP_KINDS, value.group, `${where}.group`);
157
- const id = requiredText(value.id, `${where}.id (jede Gruppe braucht eine ID)`);
158
- if (!isAbsent(value.entity)) {
159
- throw new Error(`${where}: Gruppe ${id} darf kein entity tragen; Elemente sind eigene Knoten mit parent ${JSON.stringify(id)}`);
160
- }
161
- for (const parameter of ["width", "height", "columns", "direction", "root"]) {
162
- if (!isAbsent(value[parameter]) && !GROUP_PARAMETERS[group].includes(parameter)) {
163
- throw new Error(`${where}: Gruppe ${id} (${group}) kennt keinen Parameter ${parameter}`
164
- + (parameter === "width" ? "; für Umbruch wrap-h nehmen" : parameter === "height" ? "; für Umbruch wrap-v nehmen" : ""));
165
- }
166
- }
167
- if (!isAbsent(value.frame) && typeof value.frame !== "boolean") throw new Error(`${where}.frame muss true oder false sein`);
168
- const base: CanvasGroupBase = withoutUndefined({
169
- id,
170
- parent: optionalText(value.parent, `${where}.parent`),
171
- label: optionalText(value.label, `${where}.label`),
172
- gap: optionalGap(value.gap, `${where}.gap`),
173
- frame: value.frame === true,
174
- });
175
- switch (group) {
176
- case "h":
177
- case "v":
178
- case "circle":
179
- return { ...base, group };
180
- case "wrap-h":
181
- return { ...base, group, width: positiveNumber(value.width, `${where}.width (wrap-h braucht die Zeilenbreite)`) };
182
- case "wrap-v":
183
- return { ...base, group, height: positiveNumber(value.height, `${where}.height (wrap-v braucht die Spaltenhöhe)`) };
184
- case "grid": {
185
- const columns = value.columns;
186
- if (typeof columns !== "number" || !Number.isInteger(columns) || columns < 1) {
187
- throw new Error(`${where}.columns (grid braucht eine ganze Spaltenzahl ab 1)`);
188
- }
189
- return { ...base, group, columns };
190
- }
191
- case "tree": {
192
- const entity = canvasEntityOf(requiredText(value.root, `${where}.root (tree braucht den @handle des Wurzel-Actors)`));
193
- if (entity.kind !== "actor") throw new Error(`${where}.root: die Wurzel eines Baums ist ein Actor (@handle)`);
194
- return {
195
- ...base,
196
- group,
197
- root: canvasEntityKey(entity),
198
- direction: isAbsent(value.direction) ? "right" : oneOf(CANVAS_TREE_DIRECTIONS, value.direction, `${where}.direction`),
199
- };
200
- }
201
- }
202
- };
203
-
204
- const itemNodeOf = (value: Record<string, unknown>, index: number): CanvasItemNode => {
205
- const where = `nodes[${index}]`;
206
- if (typeof value.entity !== "string") {
207
- throw new Error(`${where}: ein Knoten ist entweder eine Gruppe (group + id) oder ein Element (entity)`);
208
- }
209
- const entity = canvasEntityOf(value.entity);
210
- if (value.chatInput !== undefined && (entity.kind !== "actor" || typeof value.chatInput !== "boolean")) {
211
- throw new Error(`${where}.chatInput ist nur für Actors als Wahrheitswert erlaubt`);
212
- }
213
- return withoutUndefined({
214
- entity: canvasEntityKey(entity),
215
- parent: optionalText(value.parent, `${where}.parent`),
216
- chatInput: value.chatInput as boolean | undefined,
217
- });
218
- };
219
-
220
- const shapeOf = (value: unknown, index: number): CanvasShape => {
221
- const where = `shapes[${index}]`;
222
- if (!isRecord(value)) throw new Error(`${where} muss ein Objekt sein`);
223
- const kind = oneOf(CANVAS_SHAPE_KINDS, value.kind, `${where}.kind`);
224
- return {
225
- id: requiredText(value.id, `${where}.id`),
226
- kind,
227
- text: requiredText(value.text, `${where}.text (jede Form trägt einen Text)`),
228
- size: isAbsent(value.size) ? CANVAS_SHAPE_DEFAULT_SIZE[kind] : positiveNumber(value.size, `${where}.size`),
229
- };
230
- };
231
-
232
- const lineOf = (value: unknown, index: number): CanvasLine => {
233
- const where = `lines[${index}]`;
234
- if (!isRecord(value)) throw new Error(`${where} muss ein Objekt sein`);
235
- const from = canvasEntityKey(canvasEntityOf(requiredText(value.from, `${where}.from`)));
236
- const to = canvasEntityKey(canvasEntityOf(requiredText(value.to, `${where}.to`)));
237
- if (from === to) throw new Error(`${where}: eine Linie führt von einer Entität zu einer ANDEREN, nicht von ${from} zu sich selbst`);
238
- return withoutUndefined({
239
- from,
240
- to,
241
- label: optionalText(value.label, `${where}.label`),
242
- arrow: isAbsent(value.arrow) ? "none" : oneOf(CANVAS_ARROWS, value.arrow, `${where}.arrow`),
243
- style: isAbsent(value.style) ? "solid" : oneOf(CANVAS_LINE_STYLES, value.style, `${where}.style`),
244
- });
245
- };
246
-
247
- const assertNoCycle = (groups: Map<string, CanvasGroupNode>) => {
248
- for (const start of groups.values()) {
249
- const seen = new Set<string>([start.id]);
250
- let current = start;
251
- while (current.parent) {
252
- const next = groups.get(current.parent);
253
- if (!next) throw new Error(`Gruppe ${current.id}: parent ${JSON.stringify(current.parent)} ist keine bekannte Gruppe`);
254
- if (seen.has(next.id)) throw new Error(`Gruppe ${start.id} liegt über parent in sich selbst`);
255
- seen.add(next.id);
256
- current = next;
257
- }
258
- }
259
- };
260
-
261
- /** Parses and validates a canvas layout; throws a German error naming the first violation. */
42
+ /** Parses and validates a tile layout; throws a German error naming the first violation. */
262
43
  export const canvasLayoutOf = (value: unknown): CanvasLayout => {
263
- if (!isRecord(value)) throw new Error("Das Layout muss ein Objekt mit nodes, shapes und lines sein");
264
- if (!Array.isArray(value.nodes)) throw new Error("nodes fehlt oder ist keine Liste");
265
- const rawShapes = value.shapes ?? [];
266
- const rawLines = value.lines ?? [];
267
- if (!Array.isArray(rawShapes)) throw new Error("shapes muss eine Liste sein");
268
- if (!Array.isArray(rawLines)) throw new Error("lines muss eine Liste sein");
269
-
270
- const nodes = value.nodes.map((entry, index): CanvasLayoutNode => {
271
- if (!isRecord(entry)) throw new Error(`nodes[${index}] muss ein Objekt sein`);
272
- if (!isAbsent(entry.group) && entry.chatInput !== undefined) throw new Error(`nodes[${index}].chatInput ist nur für Actors erlaubt`);
273
- return isAbsent(entry.group) ? itemNodeOf(entry, index) : groupNodeOf(entry, index);
274
- });
275
- const shapes = rawShapes.map(shapeOf);
276
- const lines = rawLines.map(lineOf);
277
-
278
- const groups = new Map<string, CanvasGroupNode>();
279
- for (const node of nodes.filter(isCanvasGroupNode)) {
280
- if (groups.has(node.id)) throw new Error(`Gruppen-ID ${JSON.stringify(node.id)} kommt mehrfach vor`);
281
- groups.set(node.id, node);
282
- }
283
- assertNoCycle(groups);
284
- for (const node of nodes) {
285
- if (!node.parent) continue;
286
- const parent = groups.get(node.parent);
287
- if (!parent) throw new Error(`parent ${JSON.stringify(node.parent)} ist keine bekannte Gruppe`);
288
- if (parent.group === "tree") {
289
- throw new Error(`Gruppe ${parent.id} ist ein Baum und füllt sich aus der Abstammung von ${parent.root}; sie nimmt keine eigenen Kinder auf`);
290
- }
291
- }
292
-
293
- const shapeIds = new Set<string>();
294
- for (const shape of shapes) {
295
- if (shapeIds.has(shape.id)) throw new Error(`Form-ID ${JSON.stringify(shape.id)} kommt mehrfach vor`);
296
- shapeIds.add(shape.id);
297
- }
298
- const placed = new Set<string>();
299
- for (const node of nodes) {
300
- if (isCanvasGroupNode(node)) continue;
301
- if (placed.has(node.entity)) throw new Error(`${node.entity} ist mehr als einmal platziert`);
302
- placed.add(node.entity);
303
- }
304
- const assertKnownShape = (reference: string, where: string) => {
305
- const entity = canvasEntityOf(reference);
306
- if (entity.kind === "shape" && !shapeIds.has(entity.id)) {
307
- throw new Error(`${where}: Form ${JSON.stringify(entity.id)} ist nicht in shapes definiert`);
308
- }
309
- };
310
- for (const reference of placed) assertKnownShape(reference, "nodes");
311
- for (const shape of shapes) {
312
- if (!placed.has(`shape:${shape.id}`)) throw new Error(`Form ${JSON.stringify(shape.id)} ist nirgends platziert; jede Form braucht einen Knoten mit entity shape:${shape.id}`);
313
- }
314
- lines.forEach((line, index) => {
315
- assertKnownShape(line.from, `lines[${index}].from`);
316
- assertKnownShape(line.to, `lines[${index}].to`);
317
- });
318
-
319
- const mode = value.mode === undefined ? undefined : oneOf(["free", "tiled"] as const, value.mode, "mode");
320
- const root = value.root === undefined ? undefined : value.root === null ? null : canvasTileNodeOf(value.root);
321
- return withoutUndefined({ nodes, shapes, lines, mode, root });
44
+ if (!isRecord(value)) throw new Error("Die Anordnung muss ein Objekt mit root sein");
45
+ const legacy = LEGACY_KEYS.filter((key) => key in value);
46
+ if (legacy.length > 0) {
47
+ throw new Error(`Die Programmanordnung stammt aus dem entfernten freien Canvas (${legacy.join(", ")}); `
48
+ + "canvas_layout_replace mit root setzt sie als Kachelaufteilung neu");
49
+ }
50
+ if (!("root" in value)) throw new Error("root fehlt; erwartet wird eine Kachel, eine Teilung oder null");
51
+ return { root: value.root === null ? null : canvasTileNodeOf(value.root) };
322
52
  };
@@ -12,38 +12,26 @@ import {
12
12
  import { toolDescriptorFrom } from "@aicontainer/server/plugin-support/agent-tool.js";
13
13
  import { ACTOR_PROGRAMS_STATE_ID, resolveActorView, type ActorProgramState } from "@aicontainer/plugins/ragents.actor-programs/contract.js";
14
14
  import {
15
- CANVAS_ARROWS,
16
- CANVAS_GROUP_KINDS,
17
- CANVAS_LINE_STYLES,
18
- CANVAS_SHAPE_KINDS,
19
- CANVAS_TREE_DIRECTIONS,
20
15
  ORCHESTRATION_PLUGIN_ID,
21
16
  canvasEntityOf,
22
17
  canvasLayoutOf,
23
- isCanvasGroupNode,
24
18
  type CanvasLayout,
25
19
  } from "../contract.js";
26
20
  import { canvasTileEntities, mapCanvasTileEntities } from "../tiled-layout.js";
27
21
 
28
- const ENTITY = "@handle names an actor, shape:<id> one of the shapes of this call, "
29
- + "app:@handle/view-key or app:program-name/view-key an activated actor view. "
30
- + "Use the actor handle or program name and view key you chose; the server resolves the view ID";
31
-
32
22
  export const canvasToolMetadata = {
33
23
  name: "canvas_layout_replace",
34
24
  label: "Replace Canvas Layout",
35
- description: "Arrange actors and mini-apps on the free canvas or in a viewport-filling tiled layout with nested, weighted splits.",
36
- longDescription: "Free mode is the default. Set mode: 'tiled' and root to a recursive tree: "
37
- + "{entity:'@helper'} or {direction:'horizontal',weights:[2,1],children:[{entity:'app:@workspace/main'},{entity:'@helper'}]}. "
38
- + "horizontal means left/right; vertical means top/bottom. Weights [1,1] split equally. "
39
- + "Every split has two children, which may themselves be splits. Tiled mode has draggable dividers and no canvas pan or zoom. "
40
- + "Mode/root-only calls preserve the free layout; mode:'free' restores it. Personal user arrangements take precedence until reset. "
41
- + "In free mode the canvas draws NO lines by itself; "
42
- + "a relation is visible only when you add a line here. Actors you do not place appear below your "
43
- + "layout as lineage trees. Approximate sizes in px for planning wrap widths: an actor card is 150 to "
44
- + "320 wide and 44 high without card sections, with sections (documents, to-dos, tools) 360 to 640 wide "
45
- + "and 100 to 300 high; an actor view uses its declared canvas placement (280 to 960 by 180 to 720); "
46
- + "a circle shape is 120 wide, a diamond 150 by 94 by default.",
25
+ description: "Arrange actors and mini-apps as tiles on the viewport-filling work surface, using a binary tree of weighted splits.",
26
+ longDescription: "root is the whole arrangement and replaces the stored one. A tile is {entity:'@helper'} or "
27
+ + "{entity:'app:@workspace/main'}; a split is {direction:'horizontal',weights:[1,1],children:[tile,tile]}. "
28
+ + "horizontal means left/right, vertical means top/bottom, and weights give the ratio of the two children. "
29
+ + "App left, chat right at 50:50: {root:{direction:'horizontal',weights:[1,1],children:["
30
+ + "{entity:'app:@workspace/main'},{entity:'@helper'}]}}. One tile above two tiles at 2:1: "
31
+ + "{root:{direction:'vertical',weights:[2,1],children:[{entity:'@lead'},"
32
+ + "{direction:'horizontal',weights:[1,1],children:[{entity:'@first'},{entity:'@second'}]}]}}. "
33
+ + "root:null clears the surface. Dividers are draggable, so do not resend a layout to fight a personal arrangement. "
34
+ + "Participants you do not place stay reachable through the run header.",
47
35
  } as const;
48
36
 
49
37
  export const canDesignCanvas = defineToolAvailability({
@@ -52,11 +40,12 @@ export const canDesignCanvas = defineToolAvailability({
52
40
  requiredCapabilities: ["plugin.state.write"],
53
41
  }, (actor) => actor.kind !== "human" && holdsUsable(actor, "plugin.state.write"));
54
42
 
55
- const literals = <T extends string>(values: readonly T[]) => values.map((value) => Type.Literal(value));
56
-
57
43
  const tileSchema = Type.Cyclic({
58
44
  CanvasTile: Type.Union([
59
- Type.Object({ entity: Type.String({ minLength: 1, description: "Actor @handle or activated mini-app app:@handle/view-key; no shapes" }) }, { additionalProperties: false }),
45
+ Type.Object({
46
+ entity: Type.String({ minLength: 1, description: "Actor @handle or activated mini-app app:@handle/view-key or app:program-name/view-key; the server resolves the view ID" }),
47
+ chatInput: Type.Optional(Type.Boolean({ description: "Actor tiles only: false hides the chat composer in the tile; default true. Does not change permissions or the inspector" })),
48
+ }, { additionalProperties: false }),
60
49
  Type.Object({
61
50
  direction: Type.Union([Type.Literal("horizontal"), Type.Literal("vertical")]),
62
51
  weights: Type.Tuple([Type.Number({ exclusiveMinimum: 0 }), Type.Number({ exclusiveMinimum: 0 })]),
@@ -66,40 +55,7 @@ const tileSchema = Type.Cyclic({
66
55
  }, "CanvasTile");
67
56
 
68
57
  const canvasLayoutSchema = Type.Object({
69
- mode: Type.Optional(Type.Union([Type.Literal("free"), Type.Literal("tiled")], { description: "free is the default; tiled fills the viewport, disables pan/zoom and uses root" })),
70
- root: Type.Optional(Type.Union([tileSchema, Type.Null()], { description: "Tiled layout: an actor/app leaf or a binary split. Maximum 64 unique leaves and 16 nested splits. null clears the tiles; omitted preserves them" })),
71
- nodes: Type.Optional(Type.Array(Type.Object({
72
- id: Type.Optional(Type.String({ minLength: 1, description: "Groups only: an id you choose; member nodes name it in parent" })),
73
- group: Type.Optional(Type.Union(literals(CANVAS_GROUP_KINDS), {
74
- description: "Makes the node a group. h = row, v = column, wrap-h = row wrapping at width, "
75
- + "wrap-v = column wrapping at height, grid = columns by rows, circle = members on a ring "
76
- + "(actors talking to each other), tree = the root actor and everything it spawned",
77
- })),
78
- entity: Type.Optional(Type.String({ minLength: 1, description: `Items only: ${ENTITY}` })),
79
- chatInput: Type.Optional(Type.Boolean({ description: "Actor items only: false hides the chat composer on free canvas and tiles; default true. Does not change permissions or the inspector" })),
80
- parent: Type.Optional(Type.String({ minLength: 1, description: "Id of the enclosing group; omitted = top level (top-level nodes stack vertically)" })),
81
- label: Type.Optional(Type.String({ description: "Groups: caption above the members, shown with or without frame" })),
82
- frame: Type.Optional(Type.Boolean({ description: "Groups: true draws a dashed outline around the members; default false, no outline" })),
83
- gap: Type.Optional(Type.Number({ minimum: 0, description: "Groups: minimum distance between members in px, default 44; labelled connections may increase it to leave room for their text" })),
84
- width: Type.Optional(Type.Number({ exclusiveMinimum: 0, description: "wrap-h only and required there: row width in px before wrapping" })),
85
- height: Type.Optional(Type.Number({ exclusiveMinimum: 0, description: "wrap-v only and required there: column height in px before wrapping" })),
86
- columns: Type.Optional(Type.Integer({ minimum: 1, description: "grid only and required there" })),
87
- direction: Type.Optional(Type.Union(literals(CANVAS_TREE_DIRECTIONS), { description: "tree only: where the children go, default right" })),
88
- root: Type.Optional(Type.String({ minLength: 1, description: "tree only and required there: @handle of the root actor" })),
89
- }))),
90
- shapes: Type.Optional(Type.Array(Type.Object({
91
- id: Type.String({ minLength: 1, description: "Your id; place the shape as an item with entity shape:<id>" }),
92
- kind: Type.Union(literals(CANVAS_SHAPE_KINDS)),
93
- text: Type.String({ minLength: 1, description: "Text inside the shape; keep it to a few words" }),
94
- size: Type.Optional(Type.Number({ exclusiveMinimum: 0, description: "Diameter of a circle or width of a diamond in px; default 120 and 150" })),
95
- }))),
96
- lines: Type.Optional(Type.Array(Type.Object({
97
- from: Type.String({ minLength: 1, description: ENTITY }),
98
- to: Type.String({ minLength: 1, description: `${ENTITY}; must differ from from` }),
99
- label: Type.Optional(Type.String({ description: "Short caption on the line" })),
100
- arrow: Type.Optional(Type.Union(literals(CANVAS_ARROWS), { description: "none = plain line (default), end = arrow at to, both = arrows at both ends" })),
101
- style: Type.Optional(Type.Union(literals(CANVAS_LINE_STYLES), { description: "solid (default) or dashed" })),
102
- }))),
58
+ root: Type.Union([tileSchema, Type.Null()], { description: "The whole arrangement: a tile or a binary split. Maximum 64 unique tiles and 16 nested splits. null clears the surface" }),
103
59
  });
104
60
 
105
61
  const invalid = (message: string) => new DomainError("invalid-canvas-layout", message, 400);
@@ -130,21 +86,6 @@ const actorByHandle = (view: RunView, reference: string, where: string) => {
130
86
  return found;
131
87
  };
132
88
 
133
- const descendantsOf = (view: RunView, actorId: string): string[] => {
134
- const result: string[] = [];
135
- const queue = [actorId];
136
- while (queue.length > 0) {
137
- const current = queue.shift()!;
138
- for (const actor of view.actors) {
139
- if (actor.kind !== "human" && actor.createdBy === current && !result.includes(actor.id)) {
140
- result.push(actor.id);
141
- queue.push(actor.id);
142
- }
143
- }
144
- }
145
- return result;
146
- };
147
-
148
89
  const isActorReference = (reference: string) => canvasEntityOf(reference).kind === "actor";
149
90
 
150
91
  const resolveAppReferences = (view: RunView, layout: CanvasLayout): CanvasLayout => {
@@ -165,50 +106,15 @@ const resolveAppReferences = (view: RunView, layout: CanvasLayout): CanvasLayout
165
106
  throw invalid(`${where}: ${reference}: ${error instanceof Error ? error.message : String(error)} Canvas-Entitäten erhalten das Präfix app:.`);
166
107
  }
167
108
  };
168
- return parsedLayout({
169
- ...layout,
170
- ...(layout.root ? { root: mapCanvasTileEntities(layout.root, (entity) => resolve(entity, "root")) } : {}),
171
- nodes: layout.nodes.map((node, index) => isCanvasGroupNode(node)
172
- ? node : { ...node, entity: resolve(node.entity, `nodes[${index}].entity`) }),
173
- lines: layout.lines.map((line, index) => ({
174
- ...line,
175
- from: resolve(line.from, `lines[${index}].from`),
176
- to: resolve(line.to, `lines[${index}].to`),
177
- })),
178
- });
109
+ return parsedLayout({ root: layout.root ? mapCanvasTileEntities(layout.root, (entity) => resolve(entity, "root")) : null });
179
110
  };
180
111
 
181
- /** Resolve app names and validate entities and actor ancestry before any layout is stored. */
112
+ /** Resolve app names and validate the placed actors before any layout is stored. */
182
113
  export const checkLayoutAgainstRun = (view: RunView, input: CanvasLayout): CanvasLayout => {
183
114
  const layout = resolveAppReferences(view, input);
184
115
  for (const reference of canvasTileEntities(layout.root)) {
185
116
  if (isActorReference(reference)) actorByHandle(view, reference, "root");
186
117
  }
187
- const placed = new Map<string, string>();
188
- layout.nodes.forEach((node, index) => {
189
- if (isCanvasGroupNode(node) || !isActorReference(node.entity)) return;
190
- const actor = actorByHandle(view, node.entity, `nodes[${index}]`);
191
- placed.set(actor.id, node.entity);
192
- });
193
- const trees = layout.nodes.flatMap((node, index) =>
194
- isCanvasGroupNode(node) && node.group === "tree"
195
- ? [{ node, root: actorByHandle(view, node.root, `nodes[${index}].root`) }]
196
- : []);
197
- for (const { node, root } of trees) {
198
- if (placed.has(root.id)) {
199
- throw invalid(`Baum ${node.id}: die Wurzel ${node.root} ist zusätzlich einzeln platziert`);
200
- }
201
- for (const id of descendantsOf(view, root.id)) {
202
- const reference = placed.get(id);
203
- if (reference) throw invalid(`Baum ${node.id}: ${reference} gehört zur Abstammung von ${node.root} und ist zusätzlich einzeln platziert`);
204
- const other = trees.find((tree) => tree.root.id === id);
205
- if (other) throw invalid(`Baum ${other.node.id}: ${other.node.root} liegt schon im Baum ${node.id} von ${node.root}`);
206
- }
207
- }
208
- layout.lines.forEach((line, index) => {
209
- if (isActorReference(line.from)) actorByHandle(view, line.from, `lines[${index}].from`);
210
- if (isActorReference(line.to)) actorByHandle(view, line.to, `lines[${index}].to`);
211
- });
212
118
  return layout;
213
119
  };
214
120
 
@@ -220,18 +126,7 @@ export const createCanvasTool = (): RunFunction =>
220
126
  available: canDesignCanvas,
221
127
  run: ({ runtime, caller, context, eventsFor }, toolCallId, input) => {
222
128
  const view = runtime.view(caller.runId);
223
- if (input.nodes === undefined && input.mode === undefined && input.root === undefined) throw invalid("nodes oder mode/root muss angegeben werden");
224
- if (input.nodes === undefined && (input.shapes !== undefined || input.lines !== undefined)) throw invalid("shapes und lines ersetzen den freien Canvas zusammen mit nodes");
225
- const currentState = view.pluginStates.find((entry) => entry.pluginId === ORCHESTRATION_PLUGIN_ID && entry.scope.kind === "run");
226
- const current = currentState ? parsedLayout(currentState.state) : { nodes: [], shapes: [], lines: [] };
227
- const checked = checkLayoutAgainstRun(view, parsedLayout({ ...input, nodes: input.nodes ?? [] }));
228
- const layout: CanvasLayout = {
229
- ...current,
230
- ...checked,
231
- ...(input.nodes === undefined ? { nodes: current.nodes, shapes: current.shapes, lines: current.lines } : {}),
232
- ...(input.mode !== undefined || input.root !== undefined || current.mode !== undefined
233
- ? { mode: input.mode ?? (input.root !== undefined ? "tiled" : "free") } : {}),
234
- };
129
+ const layout = checkLayoutAgainstRun(view, parsedLayout(input));
235
130
  runtime.replacePluginState(context(toolCallId), caller.runId, {
236
131
  pluginId: ORCHESTRATION_PLUGIN_ID,
237
132
  scope: { kind: "run" },
@@ -12,15 +12,17 @@ export const canvasTileNodeOf = (input: unknown): CanvasTileNode => {
12
12
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${where} muss eine Kachel oder Teilung sein`);
13
13
  const node = value as Record<string, unknown>;
14
14
  if (Object.hasOwn(node, "entity")) {
15
- if (Object.keys(node).some((key) => key !== "entity")) throw new Error(`${where}: eine Kachel trägt nur entity; eine Teilung direction, weights und children`);
15
+ if (Object.keys(node).some((key) => key !== "entity" && key !== "chatInput")) throw new Error(`${where}: eine Kachel trägt nur entity und chatInput; eine Teilung direction, weights und children`);
16
16
  if (typeof node.entity !== "string") throw new Error(`${where}.entity muss @handle oder app:<name>/<view> sein`);
17
17
  const entity = canvasEntityOf(node.entity);
18
- if (entity.kind === "shape") throw new Error(`${where}: Kacheln zeigen Actors (@handle) oder Mini-Apps (app:<name>/<view>), keine Formen`);
19
18
  const key = canvasEntityKey(entity);
19
+ if (node.chatInput !== undefined && (entity.kind !== "actor" || typeof node.chatInput !== "boolean")) {
20
+ throw new Error(`${where}.chatInput ist nur für Actors als Wahrheitswert erlaubt`);
21
+ }
20
22
  if (entities.has(key)) throw new Error(`${where}: ${key} ist mehr als einmal als Kachel platziert`);
21
23
  entities.add(key);
22
24
  if (entities.size > MAX_CANVAS_TILES) throw new Error(`Höchstens ${MAX_CANVAS_TILES} Kacheln sind erlaubt`);
23
- return { entity: key };
25
+ return node.chatInput === undefined ? { entity: key } : { entity: key, chatInput: node.chatInput as boolean };
24
26
  }
25
27
  if (Object.keys(node).some((key) => !["direction", "weights", "children"].includes(key))) throw new Error(`${where}: eine Teilung trägt nur direction, weights und children`);
26
28
  if (node.direction !== "horizontal" && node.direction !== "vertical") throw new Error(`${where}.direction muss horizontal oder vertical sein`);
@@ -37,11 +39,17 @@ export const canvasTileNodeOf = (input: unknown): CanvasTileNode => {
37
39
  return parse(input, "root", 0);
38
40
  };
39
41
 
42
+ /** A tile hides the chat composer of its actor only when its leaf says chatInput: false. */
43
+ export const canvasTileChatInput = (root: CanvasTileNode | null | undefined, entity: string): boolean =>
44
+ !root ? true : "entity" in root
45
+ ? root.entity !== entity || root.chatInput !== false
46
+ : canvasTileChatInput(root.children[0], entity) && canvasTileChatInput(root.children[1], entity);
47
+
40
48
  export const canvasTileEntities = (root: CanvasTileNode | null | undefined): string[] =>
41
49
  !root ? [] : "entity" in root ? [root.entity] : root.children.flatMap(canvasTileEntities);
42
50
 
43
51
  export const mapCanvasTileEntities = (root: CanvasTileNode, resolve: (entity: string) => string): CanvasTileNode =>
44
- "entity" in root ? { entity: resolve(root.entity) } : {
52
+ "entity" in root ? { ...root, entity: resolve(root.entity) } : {
45
53
  ...root,
46
54
  children: [mapCanvasTileEntities(root.children[0], resolve), mapCanvasTileEntities(root.children[1], resolve)],
47
55
  };