@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,7 +2,7 @@ import { realpath, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import type { WorkspaceProcessContext } from "../context.js";
4
4
  import { runManagedProcess } from "../managed-process.js";
5
- import type { LanguageServerDiagnostic, LanguageServerSnapshot } from "./contract.js";
5
+ import type { LanguageServerDiagnostic, LanguageServerInstanceSnapshot, LanguageServerSnapshot } from "./contract.js";
6
6
  import { diagnosticEntries, formatDiagnostics } from "./diagnostics.js";
7
7
  import { LanguageServerSession, withTimeout, type LanguageServerLaunch } from "./session.js";
8
8
  import { allowedWorkspacePath, containsWorkspacePath, expandWorkspaceAlias, resolvedWorkspacePath } from "../paths.js";
@@ -13,6 +13,7 @@ export interface LanguageServerAdapter {
13
13
  languages: Readonly<Record<string, string>>;
14
14
  rootDescription: string;
15
15
  resolveRoot: (workspaceRoot: string, root: string) => Promise<string>;
16
+ rootDirectory: (root: string) => string;
16
17
  launch: (context: WorkspaceProcessContext, root: string) => Promise<LanguageServerLaunch>;
17
18
  open: (session: LanguageServerSession, root: string, timeoutMs: number) => Promise<string>;
18
19
  }
@@ -30,6 +31,7 @@ interface OpenServer {
30
31
  }
31
32
 
32
33
  interface ServerEntry {
34
+ runId: string;
33
35
  root: string;
34
36
  state: "opening" | "ready" | "failed";
35
37
  summary: string;
@@ -47,12 +49,23 @@ const GIT_TIMEOUT_MS = 30 * 1000;
47
49
 
48
50
  const messageOf = (error: unknown): string => error instanceof Error ? error.message : String(error);
49
51
 
50
- const git = async (context: WorkspaceProcessContext, tool: string, args: readonly string[]): Promise<string> => {
52
+ const keyOf = (runId: string, root: string): string => `${runId}\n${root}`;
53
+
54
+ const runOf = (key: string): string => key.slice(0, key.indexOf("\n"));
55
+
56
+ const instanceCount = (count: number): string => `${count} offene Instanz${count === 1 ? "" : "en"}`;
57
+
58
+ const git = async (
59
+ context: WorkspaceProcessContext,
60
+ directory: string,
61
+ tool: string,
62
+ args: readonly string[],
63
+ ): Promise<string> => {
51
64
  let output = "";
52
65
  const result = await runManagedProcess({
53
66
  command: "git",
54
67
  args: [...args],
55
- cwd: context.root,
68
+ cwd: directory,
56
69
  env: context.env,
57
70
  uid: context.uid,
58
71
  gid: context.gid,
@@ -66,14 +79,15 @@ const git = async (context: WorkspaceProcessContext, tool: string, args: readonl
66
79
  return output;
67
80
  };
68
81
 
69
- /** Git nennt seine Pfade relativ zur Repo-Wurzel; `-- .` grenzt sie auf den Arbeitsbereich ein. */
82
+ /** Git nennt seine Pfade relativ zur Repo-Wurzel; `-- .` grenzt sie auf die Wurzel der Instanz ein. */
70
83
  export const changedWorkspaceFiles = async (
71
84
  context: WorkspaceProcessContext,
85
+ directory: string,
72
86
  tool: string,
73
87
  handles: (filePath: string) => boolean,
74
88
  ): Promise<string[]> => {
75
- const topLevel = (await git(context, tool, ["rev-parse", "--show-toplevel"])).trim();
76
- const candidates = (await git(context, tool, ["status", "--porcelain", "--untracked-files=all", "--no-renames", "--", "."]))
89
+ const topLevel = (await git(context, directory, tool, ["rev-parse", "--show-toplevel"])).trim();
90
+ const candidates = (await git(context, directory, tool, ["status", "--porcelain", "--untracked-files=all", "--no-renames", "--", "."]))
77
91
  .split("\n")
78
92
  .filter((line) => line.length > 3 && line[1] !== "D" && line[0] !== "D")
79
93
  .map((line) => path.resolve(topLevel, line.slice(3)))
@@ -92,7 +106,7 @@ export class LanguageServerHost {
92
106
  readonly #openTimeoutMs: number;
93
107
  readonly #diagnosticsTimeoutMs: number;
94
108
  readonly #servers = new Map<string, ServerEntry>();
95
- readonly #roots = new Map<string, string>();
109
+ readonly #roots = new Map<string, Set<string>>();
96
110
  readonly #lifetimes = new Map<string, object>();
97
111
  readonly #closing = new Map<string, Set<Promise<void>>>();
98
112
  readonly #idleTimers = new Map<string, NodeJS.Timeout>();
@@ -117,71 +131,163 @@ export class LanguageServerHost {
117
131
 
118
132
  async open(runId: string, root: string): Promise<string> {
119
133
  const lifetime = this.#lifetime(runId);
120
- const previous = this.#servers.get(runId);
134
+ const { context, absoluteRoot } = await this.#prepare(runId, lifetime, root);
135
+ const current = this.#current(runId, absoluteRoot);
136
+ if (current && current.state !== "failed") {
137
+ const server = await current.pending!.catch(async (error: unknown) => {
138
+ await this.#recordFailure(runId, lifetime, absoluteRoot, error);
139
+ throw error;
140
+ });
141
+ this.#assertCurrent(current);
142
+ this.#touch(runId, absoluteRoot);
143
+ return this.#openMessage(runId, `${this.adapter.label} ist für ${absoluteRoot} bereits geöffnet: ${server.summary}`);
144
+ }
145
+ const server = await this.#start(runId, context, absoluteRoot);
146
+ this.#assertLifetime(runId, lifetime);
147
+ if (this.#servers.get(keyOf(runId, absoluteRoot))?.server !== server) throw this.#stopped();
148
+ return this.#openMessage(runId, `${server.summary} (Wurzel ${absoluteRoot})`);
149
+ }
150
+
151
+ async close(runId: string, root?: string): Promise<string> {
152
+ if (root === undefined) {
153
+ const roots = this.#openRoots(runId);
154
+ if (roots.length === 0) return `${this.adapter.label} ist in diesem Run nicht geöffnet`;
155
+ await Promise.all(roots.map((entry) => this.#closeInstance(runId, entry)));
156
+ const closed = roots.length === 1 ? "eine Instanz" : `${roots.length} Instanzen`;
157
+ return `${this.adapter.label}: ${closed} beendet; keine offene Instanz mehr in diesem Run`;
158
+ }
159
+ const context = await this.#contextFor(runId);
160
+ const chosen = await this.#known(runId, context, root);
161
+ await this.#closeInstance(runId, chosen);
162
+ return `${this.adapter.label} für ${chosen} beendet; ${instanceCount(this.#openRoots(runId).length)} in diesem Run`;
163
+ }
164
+
165
+ async snapshot(runId: string): Promise<LanguageServerSnapshot> {
166
+ const instances: LanguageServerInstanceSnapshot[] = [];
167
+ for (const root of this.#openRoots(runId)) {
168
+ const instance = await this.#instanceSnapshot(runId, root);
169
+ if (instance) instances.push(instance);
170
+ }
171
+ return { instances };
172
+ }
173
+
174
+ async diagnostics(
175
+ runId: string,
176
+ paths: readonly string[] | undefined,
177
+ includeWarnings: boolean,
178
+ root?: string,
179
+ ): Promise<string> {
180
+ const lifetime = this.#lifetime(runId);
181
+ const context = await this.#contextFor(runId);
182
+ this.#assertLifetime(runId, lifetime);
183
+ const roots = root === undefined ? this.#openRoots(runId) : [await this.#known(runId, context, root)];
184
+ if (roots.length === 0) throw new Error(`Kein ${this.adapter.label}-Server geöffnet; zuerst ${this.adapter.id}_open aufrufen`);
185
+ const results = paths === undefined || paths.length === 0
186
+ ? await this.#changed(runId, context, roots, includeWarnings)
187
+ : await this.#requested(runId, context, roots, paths, includeWarnings);
188
+ if (results.length === 0) return `Keine geänderten ${this.adapter.label}-Dateien in den geöffneten Wurzeln`;
189
+ return results.join("\n\n");
190
+ }
191
+
192
+ async annotate(runId: string, absolutePath: string): Promise<string | undefined> {
193
+ if (!this.handles(absolutePath) || this.#openRoots(runId).length === 0) return undefined;
194
+ try {
195
+ const lifetime = this.#lifetime(runId);
196
+ const context = await this.#contextFor(runId);
197
+ this.#assertLifetime(runId, lifetime);
198
+ const file = await allowedWorkspacePath(absolutePath, [context.root, ...context.additionalRoots ?? []]);
199
+ const chosen = this.#match(this.#openRoots(runId), file);
200
+ if (!chosen) return undefined;
201
+ const server = await this.#ensure(runId, context, chosen);
202
+ return await this.#format(context, server, file, false);
203
+ } catch (error) {
204
+ return `Diagnostik (${this.adapter.label}) fehlgeschlagen: ${messageOf(error)}`;
205
+ }
206
+ }
207
+
208
+ async stopSession(runId: string): Promise<void> {
209
+ this.#lifetimes.delete(runId);
210
+ const keys = new Set([
211
+ ...this.#openRoots(runId).map((root) => keyOf(runId, root)),
212
+ ...[...this.#servers.keys()].filter((key) => runOf(key) === runId),
213
+ ]);
214
+ this.#roots.delete(runId);
215
+ for (const key of keys) this.#seen.delete(key);
216
+ await Promise.all([...keys].map((key) => this.#close(key)));
217
+ await Promise.all(this.#closing.get(runId) ?? []);
218
+ }
219
+
220
+ async shutdown(): Promise<void> {
221
+ this.#closed = true;
222
+ const runs = new Set([
223
+ ...this.#lifetimes.keys(),
224
+ ...this.#roots.keys(),
225
+ ...[...this.#servers.keys()].map(runOf),
226
+ ...this.#closing.keys(),
227
+ ]);
228
+ const results = await Promise.allSettled([...runs].map((runId) => this.stopSession(runId)));
229
+ const failures = results
230
+ .filter((result): result is PromiseRejectedResult => result.status === "rejected")
231
+ .map((result) => result.reason);
232
+ if (failures.length > 0) throw new AggregateError(failures, `${this.adapter.label}-Shutdown fehlgeschlagen`);
233
+ }
234
+
235
+ async #prepare(
236
+ runId: string,
237
+ lifetime: object,
238
+ root: string,
239
+ ): Promise<{ context: WorkspaceProcessContext; absoluteRoot: string }> {
121
240
  let absoluteRoot = root;
122
241
  try {
123
242
  const context = await this.#contextFor(runId);
124
243
  this.#assertLifetime(runId, lifetime);
125
244
  const allowed = await Promise.all([context.root, ...context.additionalRoots ?? []].map(resolvedWorkspacePath));
126
245
  const requested = await allowedWorkspacePath(path.resolve(context.root, expandWorkspaceAlias(root, context.workspaceAliases ?? {})), allowed);
246
+ absoluteRoot = requested;
127
247
  const workspaceRoot = allowed.find((directory) => containsWorkspacePath(directory, requested))!;
128
248
  absoluteRoot = await this.adapter.resolveRoot(workspaceRoot, requested);
129
249
  this.#assertLifetime(runId, lifetime);
130
- const current = this.#current(runId);
131
- if (current && current.root === absoluteRoot && current.state !== "failed") {
132
- const server = await current.pending!;
133
- this.#assertCurrent(runId, current);
134
- this.#touch(runId);
135
- return `${this.adapter.label} ist bereits geöffnet: ${server.summary}`;
136
- }
137
- const server = await this.#start(runId, context, absoluteRoot);
138
- this.#assertLifetime(runId, lifetime);
139
- if (this.#servers.get(runId)?.server !== server) throw this.#stopped();
140
- return server.summary;
250
+ return { context, absoluteRoot };
141
251
  } catch (error) {
142
- if (this.#lifetimes.get(runId) === lifetime && this.#servers.get(runId) === previous && !this.#closed) {
143
- const failed: ServerEntry = { root: absoluteRoot, state: "failed", summary: messageOf(error), controller: new AbortController() };
144
- this.#servers.set(runId, failed);
145
- this.#roots.set(runId, absoluteRoot);
146
- this.#seen.delete(runId);
147
- if (previous) await this.#dispose(runId, previous);
148
- }
252
+ await this.#recordFailure(runId, lifetime, absoluteRoot, error);
149
253
  throw error;
150
254
  }
151
255
  }
152
256
 
153
- async status(runId: string): Promise<string> {
154
- const current = this.#current(runId);
155
- if (current?.state === "opening") return current.summary;
156
- if (current?.state === "failed") return `${this.adapter.label} konnte nicht geöffnet werden: ${current.summary}`;
157
- if (current) return `${this.adapter.label} läuft: ${current.summary}`;
158
- const root = this.#roots.get(runId);
159
- return root
160
- ? `${this.adapter.label} ist nach Leerlauf beendet und wird beim nächsten Zugriff für ${root} neu geladen`
161
- : `${this.adapter.label} ist nicht geöffnet`;
257
+ async #recordFailure(runId: string, lifetime: object, root: string, error: unknown): Promise<void> {
258
+ if (this.#closed || this.#lifetimes.get(runId) !== lifetime) return;
259
+ const key = keyOf(runId, root);
260
+ const previous = this.#servers.get(key);
261
+ if (previous && previous.state !== "failed") return;
262
+ this.#servers.set(key, {
263
+ runId,
264
+ root,
265
+ state: "failed",
266
+ summary: messageOf(error),
267
+ controller: new AbortController(),
268
+ });
269
+ this.#remember(runId, root);
270
+ this.#seen.delete(key);
271
+ if (previous) await this.#dispose(previous);
162
272
  }
163
273
 
164
- async snapshot(runId: string): Promise<LanguageServerSnapshot> {
165
- const current = this.#current(runId);
166
- if (current?.state === "opening" || current?.state === "failed") {
167
- return { state: current.state, root: current.root, summary: current.summary, files: [] };
274
+ #openMessage(runId: string, text: string): string {
275
+ return `${text}; ${instanceCount(this.#openRoots(runId).length)} von ${this.adapter.label} in diesem Run`;
276
+ }
277
+
278
+ async #instanceSnapshot(runId: string, root: string): Promise<LanguageServerInstanceSnapshot | undefined> {
279
+ const entry = this.#current(runId, root);
280
+ if (entry?.state === "opening" || entry?.state === "failed") {
281
+ return { state: entry.state, root, summary: entry.summary, files: [] };
168
282
  }
169
- const root = current?.root ?? this.#roots.get(runId);
170
- const rootExists = !root || await this.#exists(root);
171
- if (this.#servers.get(runId) !== current || this.#roots.get(runId) !== root) return this.snapshot(runId);
172
- if (!rootExists) {
173
- await this.stopSession(runId);
174
- return { state: "closed", root: null, summary: null, files: [] };
283
+ if (!await this.#exists(root)) {
284
+ await this.#closeInstance(runId, root);
285
+ return undefined;
175
286
  }
176
- const seen = this.#seen.get(runId);
287
+ const seen = this.#seen.get(keyOf(runId, root));
177
288
  const files = seen === undefined ? [] : await this.#existingFiles(runId, seen);
178
- if (this.#servers.get(runId) !== current || this.#roots.get(runId) !== root) return this.snapshot(runId);
179
- return {
180
- state: current ? "ready" : root ? "suspended" : "closed",
181
- root: root ?? null,
182
- summary: current?.summary ?? null,
183
- files,
184
- };
289
+ if (!this.#roots.get(runId)?.has(root)) return undefined;
290
+ return { state: entry ? "ready" : "suspended", root, summary: entry?.summary ?? null, files };
185
291
  }
186
292
 
187
293
  async #exists(absolutePath: string): Promise<boolean> {
@@ -191,7 +297,7 @@ export class LanguageServerHost {
191
297
  async #existingFiles(
192
298
  runId: string,
193
299
  seen: ReadonlyMap<string, readonly LanguageServerDiagnostic[]>,
194
- ): Promise<LanguageServerSnapshot["files"]> {
300
+ ): Promise<LanguageServerInstanceSnapshot["files"]> {
195
301
  const context = await this.#contextFor(runId);
196
302
  const entries = await Promise.all([...seen.entries()].map(async ([file, diagnostics]) =>
197
303
  await this.#exists(path.resolve(context.root, expandWorkspaceAlias(file, context.workspaceAliases ?? {}))) ? { path: file, diagnostics: [...diagnostics] } : null));
@@ -200,54 +306,61 @@ export class LanguageServerHost {
200
306
  .sort((left, right) => left.path.localeCompare(right.path, "de"));
201
307
  }
202
308
 
203
- async diagnostics(runId: string, paths: readonly string[] | undefined, includeWarnings: boolean): Promise<string> {
204
- const lifetime = this.#lifetime(runId);
205
- const context = await this.#contextFor(runId);
206
- this.#assertLifetime(runId, lifetime);
207
- const server = await this.#ensure(runId, context);
208
- const files = paths === undefined || paths.length === 0
209
- ? await changedWorkspaceFiles(context, `${this.adapter.id}_diagnostics`, (file) => this.handles(file))
210
- : await Promise.all(paths.map((requested) => allowedWorkspacePath(path.resolve(context.root, expandWorkspaceAlias(requested, context.workspaceAliases ?? {})), [context.root, ...context.additionalRoots ?? []])));
211
- if (files.length === 0) return `Keine geänderten ${this.adapter.label}-Dateien im Arbeitsverzeichnis`;
309
+ async #changed(
310
+ runId: string,
311
+ context: WorkspaceProcessContext,
312
+ roots: readonly string[],
313
+ includeWarnings: boolean,
314
+ ): Promise<string[]> {
212
315
  const results: string[] = [];
213
- for (const file of files) {
214
- if (!this.handles(file)) {
215
- throw new Error(`${path.relative(context.root, file)} hat keine ${this.adapter.label}-Endung (${Object.keys(this.adapter.languages).join(", ")})`);
216
- }
217
- results.push(await this.#format(context, server, file, includeWarnings));
316
+ for (const root of roots) {
317
+ const server = await this.#ensure(runId, context, root);
318
+ const files = await changedWorkspaceFiles(
319
+ context,
320
+ this.adapter.rootDirectory(root),
321
+ `${this.adapter.id}_diagnostics`,
322
+ (file) => this.handles(file),
323
+ );
324
+ for (const file of files) results.push(await this.#format(context, server, file, includeWarnings));
218
325
  }
219
- return results.join("\n\n");
326
+ return results;
220
327
  }
221
328
 
222
- async annotate(runId: string, absolutePath: string): Promise<string | undefined> {
223
- if (!this.handles(absolutePath) || !this.#roots.has(runId)) return undefined;
224
- try {
225
- const lifetime = this.#lifetime(runId);
226
- const context = await this.#contextFor(runId);
227
- this.#assertLifetime(runId, lifetime);
228
- const server = await this.#ensure(runId, context);
229
- return await this.#format(context, server, absolutePath, false);
230
- } catch (error) {
231
- return `Diagnostik (${this.adapter.label}) fehlgeschlagen: ${messageOf(error)}`;
329
+ async #requested(
330
+ runId: string,
331
+ context: WorkspaceProcessContext,
332
+ roots: readonly string[],
333
+ paths: readonly string[],
334
+ includeWarnings: boolean,
335
+ ): Promise<string[]> {
336
+ const results: string[] = [];
337
+ for (const requested of paths) {
338
+ const file = await allowedWorkspacePath(path.resolve(context.root, expandWorkspaceAlias(requested, context.workspaceAliases ?? {})), [context.root, ...context.additionalRoots ?? []]);
339
+ if (!this.handles(file)) {
340
+ throw new Error(`${path.relative(context.root, file)} hat keine ${this.adapter.label}-Endung (${Object.keys(this.adapter.languages).join(", ")})`);
341
+ }
342
+ const chosen = this.#match(roots, file);
343
+ if (!chosen) {
344
+ throw new Error(`${path.relative(context.root, file)} liegt in keiner geöffneten ${this.adapter.label}-Wurzel; offen: ${roots.join(", ")}`);
345
+ }
346
+ results.push(await this.#format(context, await this.#ensure(runId, context, chosen), file, includeWarnings));
232
347
  }
348
+ return results;
233
349
  }
234
350
 
235
- async stopSession(runId: string): Promise<void> {
236
- this.#lifetimes.delete(runId);
237
- this.#roots.delete(runId);
238
- this.#seen.delete(runId);
239
- await this.#close(runId);
240
- await Promise.all(this.#closing.get(runId) ?? []);
351
+ #match(roots: readonly string[], file: string): string | undefined {
352
+ return [...roots]
353
+ .filter((root) => containsWorkspacePath(this.adapter.rootDirectory(root), file))
354
+ .sort((left, right) => this.adapter.rootDirectory(right).length - this.adapter.rootDirectory(left).length)[0];
241
355
  }
242
356
 
243
- async shutdown(): Promise<void> {
244
- this.#closed = true;
245
- const runs = new Set([...this.#lifetimes.keys(), ...this.#roots.keys(), ...this.#servers.keys(), ...this.#closing.keys()]);
246
- const results = await Promise.allSettled([...runs].map((runId) => this.stopSession(runId)));
247
- const failures = results
248
- .filter((result): result is PromiseRejectedResult => result.status === "rejected")
249
- .map((result) => result.reason);
250
- if (failures.length > 0) throw new AggregateError(failures, `${this.adapter.label}-Shutdown fehlgeschlagen`);
357
+ async #known(runId: string, context: WorkspaceProcessContext, root: string): Promise<string> {
358
+ const roots = this.#openRoots(runId);
359
+ if (roots.length === 0) throw new Error(`Kein ${this.adapter.label}-Server geöffnet; zuerst ${this.adapter.id}_open aufrufen`);
360
+ const requested = await resolvedWorkspacePath(path.resolve(context.root, expandWorkspaceAlias(root, context.workspaceAliases ?? {})));
361
+ const found = roots.find((open) => open === requested);
362
+ if (!found) throw new Error(`Für ${root} ist kein ${this.adapter.label}-Server geöffnet; offen: ${roots.join(", ")}`);
363
+ return found;
251
364
  }
252
365
 
253
366
  async #format(
@@ -256,61 +369,81 @@ export class LanguageServerHost {
256
369
  absolutePath: string,
257
370
  includeWarnings: boolean,
258
371
  ): Promise<string> {
259
- this.#touch(context.runId);
372
+ this.#touch(context.runId, server.root);
373
+ const key = keyOf(context.runId, server.root);
260
374
  const checkedPath = await allowedWorkspacePath(absolutePath, [context.root, ...context.additionalRoots ?? []]);
261
375
  const diagnostics = await server.session.diagnosticsFor(await realpath(checkedPath), this.#diagnosticsTimeoutMs);
262
- if (this.#servers.get(context.runId)?.server !== server) throw this.#stopped();
376
+ if (this.#servers.get(key)?.server !== server) throw this.#stopped();
263
377
  const alias = Object.entries(context.workspaceAliases ?? {}).find(([, root]) => containsWorkspacePath(root, checkedPath));
264
378
  const relativePath = alias ? `${alias[0]}/${path.relative(alias[1], checkedPath)}`
265
379
  : containsWorkspacePath(context.root, checkedPath) ? path.relative(context.root, checkedPath) : checkedPath;
266
- const seen = this.#seen.get(context.runId) ?? new Map<string, readonly LanguageServerDiagnostic[]>();
380
+ const seen = this.#seen.get(key) ?? new Map<string, readonly LanguageServerDiagnostic[]>();
267
381
  seen.set(relativePath, diagnosticEntries(diagnostics));
268
- this.#seen.set(context.runId, seen);
382
+ this.#seen.set(key, seen);
269
383
  return formatDiagnostics(this.adapter.label, relativePath, diagnostics, includeWarnings);
270
384
  }
271
385
 
272
- #current(runId: string): ServerEntry | undefined {
273
- const current = this.#servers.get(runId);
386
+ #openRoots(runId: string): string[] {
387
+ return [...this.#roots.get(runId) ?? []];
388
+ }
389
+
390
+ #remember(runId: string, root: string): void {
391
+ const roots = this.#roots.get(runId) ?? new Set<string>();
392
+ roots.add(root);
393
+ this.#roots.set(runId, roots);
394
+ }
395
+
396
+ async #closeInstance(runId: string, root: string): Promise<void> {
397
+ const roots = this.#roots.get(runId);
398
+ roots?.delete(root);
399
+ if (roots?.size === 0) this.#roots.delete(runId);
400
+ const key = keyOf(runId, root);
401
+ this.#seen.delete(key);
402
+ await this.#close(key);
403
+ }
404
+
405
+ #current(runId: string, root: string): ServerEntry | undefined {
406
+ const key = keyOf(runId, root);
407
+ const current = this.#servers.get(key);
274
408
  if (current?.server?.session.exited) {
275
- this.#servers.delete(runId);
276
- void this.#dispose(runId, current).catch((error) => console.error(`${this.adapter.label}: Bereinigung fehlgeschlagen: ${messageOf(error)}`));
409
+ this.#servers.delete(key);
410
+ void this.#dispose(current).catch((error) => console.error(`${this.adapter.label}: Bereinigung fehlgeschlagen: ${messageOf(error)}`));
277
411
  return undefined;
278
412
  }
279
413
  return current;
280
414
  }
281
415
 
282
- async #ensure(runId: string, context: WorkspaceProcessContext): Promise<OpenServer> {
416
+ async #ensure(runId: string, context: WorkspaceProcessContext, root: string): Promise<OpenServer> {
283
417
  if (this.#closed) throw this.#stopped();
284
- const current = this.#current(runId);
418
+ const current = this.#current(runId, root);
285
419
  if (current?.state === "failed") throw new Error(current.summary);
286
420
  if (current) {
287
421
  const server = await current.pending!;
288
- this.#assertCurrent(runId, current);
422
+ this.#assertCurrent(current);
289
423
  return server;
290
424
  }
291
- const root = this.#roots.get(runId);
292
- if (!root) throw new Error(`Kein ${this.adapter.label}-Server geöffnet; zuerst ${this.adapter.id}_open aufrufen`);
293
425
  return this.#start(runId, context, root);
294
426
  }
295
427
 
296
428
  #start(runId: string, context: WorkspaceProcessContext, root: string): Promise<OpenServer> {
297
- const previous = this.#servers.get(runId);
298
- const closing = previous ? this.#dispose(runId, previous) : undefined;
429
+ const key = keyOf(runId, root);
430
+ const previous = this.#servers.get(key);
431
+ const closing = previous ? this.#dispose(previous) : undefined;
299
432
  const entry: ServerEntry = {
300
- root, state: "opening", summary: `${this.adapter.label} lädt ${root}`, controller: new AbortController(),
433
+ runId, root, state: "opening", summary: `${this.adapter.label} lädt ${root}`, controller: new AbortController(),
301
434
  };
302
- this.#servers.set(runId, entry);
303
- this.#roots.set(runId, root);
304
- this.#seen.delete(runId);
435
+ this.#servers.set(key, entry);
436
+ this.#remember(runId, root);
437
+ this.#seen.delete(key);
305
438
  entry.pending = (async (): Promise<OpenServer> => {
306
439
  try {
307
440
  await closing;
308
- this.#assertCurrent(runId, entry);
441
+ this.#assertCurrent(entry);
309
442
  const launch = await this.adapter.launch(context, root);
310
- this.#assertCurrent(runId, entry);
443
+ this.#assertCurrent(entry);
311
444
  entry.initializing = LanguageServerSession.start(launch, this.#openTimeoutMs);
312
445
  const session = await entry.initializing;
313
- this.#assertCurrent(runId, entry);
446
+ this.#assertCurrent(entry);
314
447
  let aborted!: () => void;
315
448
  const cancelled = new Promise<never>((_resolve, reject) => {
316
449
  aborted = () => reject(entry.controller.signal.reason);
@@ -326,49 +459,49 @@ export class LanguageServerHost {
326
459
  } finally {
327
460
  entry.controller.signal.removeEventListener("abort", aborted);
328
461
  }
329
- this.#assertCurrent(runId, entry);
462
+ this.#assertCurrent(entry);
330
463
  if (session.exited) throw new Error(`${this.adapter.label} wurde während des Ladens beendet`);
331
464
  const server = { root, session, summary };
332
465
  entry.server = server;
333
466
  entry.state = "ready";
334
467
  entry.summary = summary;
335
- this.#touch(runId);
468
+ this.#touch(runId, root);
336
469
  return server;
337
470
  } catch (error) {
338
- if (this.#servers.get(runId) === entry && !entry.controller.signal.aborted) {
471
+ if (this.#servers.get(key) === entry && !entry.controller.signal.aborted) {
339
472
  entry.state = "failed";
340
473
  entry.summary = messageOf(error);
341
474
  }
342
- await this.#dispose(runId, entry);
475
+ await this.#dispose(entry);
343
476
  throw error;
344
477
  }
345
478
  })();
346
479
  return entry.pending;
347
480
  }
348
481
 
349
- async #close(runId: string): Promise<void> {
350
- const timer = this.#idleTimers.get(runId);
482
+ async #close(key: string): Promise<void> {
483
+ const timer = this.#idleTimers.get(key);
351
484
  if (timer) clearTimeout(timer);
352
- this.#idleTimers.delete(runId);
353
- const entry = this.#servers.get(runId);
485
+ this.#idleTimers.delete(key);
486
+ const entry = this.#servers.get(key);
354
487
  if (!entry) return;
355
- this.#servers.delete(runId);
356
- await this.#dispose(runId, entry);
488
+ this.#servers.delete(key);
489
+ await this.#dispose(entry);
357
490
  }
358
491
 
359
- #dispose(runId: string, entry: ServerEntry): Promise<void> {
492
+ #dispose(entry: ServerEntry): Promise<void> {
360
493
  entry.controller.abort(this.#stopped());
361
494
  if (entry.closing) return entry.closing;
362
495
  entry.closing = (async () => {
363
496
  const session = await entry.initializing?.catch(() => undefined);
364
497
  await session?.shutdown();
365
498
  })();
366
- const pending = this.#closing.get(runId) ?? new Set<Promise<void>>();
367
- this.#closing.set(runId, pending);
499
+ const pending = this.#closing.get(entry.runId) ?? new Set<Promise<void>>();
500
+ this.#closing.set(entry.runId, pending);
368
501
  pending.add(entry.closing);
369
502
  const finished = () => {
370
503
  pending.delete(entry.closing!);
371
- if (pending.size === 0 && this.#closing.get(runId) === pending) this.#closing.delete(runId);
504
+ if (pending.size === 0 && this.#closing.get(entry.runId) === pending) this.#closing.delete(entry.runId);
372
505
  };
373
506
  entry.closing.then(finished, finished);
374
507
  return entry.closing;
@@ -385,25 +518,29 @@ export class LanguageServerHost {
385
518
  if (this.#closed || this.#lifetimes.get(runId) !== lifetime) throw this.#stopped();
386
519
  }
387
520
 
388
- #assertCurrent(runId: string, entry: ServerEntry): void {
389
- if (this.#closed || entry.controller.signal.aborted || entry.server?.session.exited || this.#servers.get(runId) !== entry) throw this.#stopped();
521
+ #assertCurrent(entry: ServerEntry): void {
522
+ if (this.#closed
523
+ || entry.controller.signal.aborted
524
+ || entry.server?.session.exited
525
+ || this.#servers.get(keyOf(entry.runId, entry.root)) !== entry) throw this.#stopped();
390
526
  }
391
527
 
392
528
  #stopped(): Error {
393
529
  return new Error(`${this.adapter.label}-Start wurde beendet`);
394
530
  }
395
531
 
396
- #touch(runId: string): void {
397
- const entry = this.#servers.get(runId);
532
+ #touch(runId: string, root: string): void {
533
+ const key = keyOf(runId, root);
534
+ const entry = this.#servers.get(key);
398
535
  if (!entry || entry.state !== "ready") return;
399
- const existing = this.#idleTimers.get(runId);
536
+ const existing = this.#idleTimers.get(key);
400
537
  if (existing) clearTimeout(existing);
401
538
  const timer = setTimeout(() => {
402
- this.#idleTimers.delete(runId);
403
- if (this.#servers.get(runId) !== entry) return;
404
- void this.#close(runId).catch((error) => console.error(`${this.adapter.label}: Beenden nach Leerlauf fehlgeschlagen: ${messageOf(error)}`));
539
+ this.#idleTimers.delete(key);
540
+ if (this.#servers.get(key) !== entry) return;
541
+ void this.#close(key).catch((error) => console.error(`${this.adapter.label}: Beenden nach Leerlauf fehlgeschlagen: ${messageOf(error)}`));
405
542
  }, this.#idleMs);
406
543
  timer.unref();
407
- this.#idleTimers.set(runId, timer);
544
+ this.#idleTimers.set(key, timer);
408
545
  }
409
546
  }
@@ -7,7 +7,7 @@ export const ACTOR_INVOCATIONS_STATE_ID = "ragents.actor-programs.invocations";
7
7
  export const ACTOR_SCRIPT_STATE_ID = "ragents.actor-programs.script";
8
8
  export const ACTOR_STATE_ID = "ragents.actor-state";
9
9
 
10
- export interface ActorViewPlacement { kind: "canvas"; anchorActorId: string; width: number; height: number; }
10
+ export interface ActorViewPlacement { kind: "canvas"; anchorActorId: string; }
11
11
  export interface ActorViewDefinition {
12
12
  id: string;
13
13
  key: string;
@@ -29,7 +29,7 @@ one active program package; add further functions and views to that package.
29
29
 
30
30
  `package.json` contains `name`, `private: true`, `type: "module"` and `ragents` metadata:
31
31
  `title`, optional `description`, optional `backend`, and optional `views`. Each view has an
32
- `id`, `client`, optional `title`, `styles`, `width` and `height`. At least a backend or a view
32
+ `id`, `client` and optional `title` and `styles`. At least a backend or a view
33
33
  is required. The host supplies the HTML root and fixed local dependencies.
34
34
 
35
35
  Import `defineActor` from `@ragents/server` and `Type` from `typebox`. Its contract contains
@@ -68,9 +68,9 @@ Use `FlowDiagram` for other graphs (star layout, fit, actions). Keep local IDs s
68
68
  See `controls`. For custom SVG use `SvgEdge`, Tailwind utilities, responsive `viewBox` and
69
69
  accessible descriptions.
70
70
  Address view visibility with `package-name/view-key`, `@handle/view-key` or a unique title;
71
- canvas nodes and lines use the same names with the `app:` prefix.
72
- The host controls resizing, collapse and full view. Views appear on the canvas; owner cards
73
- start hidden but remain in the Actors list. Personal actor visibility is separate from
71
+ a tile uses the same names with the `app:` prefix.
72
+ The host sizes the tile and offers the full view. Views appear as tiles; owner cards are not
73
+ placed by themselves but stay in the Actors list. Personal actor visibility is separate from
74
74
  `actor_view_set_visibility` and cannot be changed by layouts.
75
75
 
76
76
  Tests call `program.functions.<name>(input, context)` or `program.onInput(input, context)`.
@@ -78,8 +78,6 @@ export const appPackageSchema = Type.Object({
78
78
  views: Type.Optional(Type.Array(Type.Object({
79
79
  id: Type.String({ pattern: "^[a-z][a-z0-9-]{0,63}$" }), title: Type.Optional(Type.String({ minLength: 1 })),
80
80
  client: Type.String({ minLength: 1 }), styles: Type.Optional(Type.String()),
81
- width: Type.Optional(Type.Integer({ minimum: 280, maximum: 960 })),
82
- height: Type.Optional(Type.Integer({ minimum: 180, maximum: 720 })),
83
81
  }, { additionalProperties: false }))),
84
82
  }, { additionalProperties: false });
85
83
  export type AppPackage = Static<typeof appPackageSchema>;