@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
@@ -58,25 +58,19 @@ export default defineActor(contract, {
58
58
  }
59
59
 
60
60
  await context.functions.canvas_layout_replace({
61
- nodes: [
62
- { entity: "shape:thema" },
63
- { id: "runde", group: "circle", label: "Runde", frame: true },
64
- { entity: participants[0]!, parent: "runde" },
65
- { entity: participants[1]!, parent: "runde" },
66
- { entity: participants[2]!, parent: "runde" },
67
- ],
68
- shapes: [{ id: "thema", kind: "diamond", text: topic }],
69
- lines: [
70
- { from: participants[0]!, to: participants[1]!, arrow: "end", label: "gibt weiter" },
71
- { from: participants[1]!, to: participants[2]!, arrow: "end", label: "gibt weiter" },
72
- { from: participants[2]!, to: participants[0]!, arrow: "end", label: "gibt weiter" },
73
- { from: "shape:thema", to: participants[0]!, style: "dashed", label: "Auftakt" },
74
- ],
61
+ root: {
62
+ direction: "vertical",
63
+ weights: [1, 1],
64
+ children: [
65
+ { entity: participants[0]! },
66
+ { direction: "horizontal", weights: [1, 1], children: [{ entity: participants[1]! }, { entity: participants[2]! }] },
67
+ ],
68
+ },
75
69
  });
76
70
 
77
71
  await context.functions.actor_input({
78
72
  actor: "@coordinator",
79
- content: `Die Runde steht: ${participants.join(", ")} sind aufgestellt, die Fläche ist gestaltet. Thema: "${topic}". `
73
+ content: `Die Runde steht: ${participants.join(", ")} sind aufgestellt, die Fläche ist aufgeteilt. Thema: "${topic}". `
80
74
  + `Führe genau ${rounds} Gesprächsrunden durch: in jeder Runde ${participants.join(", ")} in dieser Reihenfolge. `
81
75
  + "Jeder erhält die bisherigen Beiträge. Ändere die Fläche nicht. "
82
76
  + "Fasse das Gespräch am Ende in drei Sätzen zusammen.",
@@ -8,9 +8,11 @@ test("Thema und Rundenzahl steuern Teilnehmer, Fläche und Auftrag", async () =>
8
8
  await program.onInput!(startInput({ topic: "Teamfrühstück", rounds: 3 }), context);
9
9
  assert.deepEqual(calls.filter((call) => call.name === "agent_spawn").map((call) => (call.input as { handle: string }).handle), ["mira", "jon", "ada"]);
10
10
  assert.deepEqual(calls.find((call) => call.name === "run_configure")?.input, { title: "Gesprächsrunde: Teamfrühstück" });
11
- const layout = calls.find((call) => call.name === "canvas_layout_replace")?.input as { shapes: { text: string }[]; lines: unknown[] };
12
- assert.equal(layout.shapes[0].text, "Teamfrühstück");
13
- assert.equal(layout.lines.length, 4);
11
+ const layout = calls.find((call) => call.name === "canvas_layout_replace")?.input as { root: unknown };
12
+ assert.deepEqual(layout.root, { direction: "vertical", weights: [1, 1], children: [
13
+ { entity: "@mira" },
14
+ { direction: "horizontal", weights: [1, 1], children: [{ entity: "@jon" }, { entity: "@ada" }] },
15
+ ] });
14
16
  assert.match(JSON.stringify(calls.at(-1)), /genau 3 Gesprächsrunden/);
15
17
  assert.deepEqual(context.state.read(), { built: true });
16
18
  const completedCalls = calls.length;
@@ -5,6 +5,6 @@
5
5
  "ragents": {
6
6
  "title": "Lernnachmittag",
7
7
  "backend": "src/server.ts",
8
- "views": [{ "id": "main", "client": "src/client.tsx", "width": 620, "height": 550 }]
8
+ "views": [{ "id": "main", "client": "src/client.tsx" }]
9
9
  }
10
10
  }
@@ -38,6 +38,13 @@ const contract = {
38
38
  const startMarker = "START_LEARNING_AFTERNOON";
39
39
 
40
40
 
41
+ type Tile = { entity: string } | { direction: "vertical"; weights: [number, number]; children: [Tile, Tile] };
42
+
43
+ function stackedTiles(entities: string[]): Tile {
44
+ const [first, ...rest] = entities;
45
+ return rest.length === 0 ? { entity: first! } : { direction: "vertical", weights: [1, rest.length], children: [{ entity: first! }, stackedTiles(rest)] };
46
+ }
47
+
41
48
  function boardWith(helpers: HelperState[]): LearningState {
42
49
  const working = helpers.some((helper) => helper.status === "working");
43
50
  return { phase: working ? "working" : helpers.some((helper) => helper.status === "error") ? "error" : "complete", helpers };
@@ -65,10 +72,13 @@ export default defineActor(contract, {
65
72
  const actor = await context.functions.agent_spawn({ handle: `learning-${helper.id}`, displayName: helper.label, profile: profile.name, prompt, tools: [] });
66
73
  return { ...helper, actorId: actor.id };
67
74
  }));
68
- await context.functions.canvas_layout_replace({ nodes: [
69
- ...helpers.map((helper) => ({ entity: `@learning-${helper.id}` })),
70
- { entity: `app:@${context.actor.handle}/main` },
71
- ] });
75
+ await context.functions.canvas_layout_replace({
76
+ root: {
77
+ direction: "horizontal",
78
+ weights: [1, 1],
79
+ children: [{ entity: `app:@${context.actor.handle}/main` }, stackedTiles(helpers.map((helper) => `@learning-${helper.id}`))],
80
+ },
81
+ });
72
82
  await context.functions.run_configure({ title: learningWorkflow.title, primaryActor: `@${context.actor.handle}` });
73
83
  context.state.replace({ board: { phase: "ready", helpers } });
74
84
  return;
@@ -37,7 +37,7 @@ export default defineActor(contract, {
37
37
  try {
38
38
  if (!state.status) {
39
39
  await context.functions.run_configure({ title: "Wortspiel", primaryActor: context.actor.id });
40
- await context.functions.canvas_layout_replace({ nodes: [{ entity: `app:@${context.actor.handle}/main`, width: 640, height: 700 }] });
40
+ await context.functions.canvas_layout_replace({ root: { entity: `app:@${context.actor.handle}/main` } });
41
41
  const start = payloadOf(JSON.parse(input.content));
42
42
  if (start.input !== null || !start.options || typeof start.options !== "object" || Array.isArray(start.options)
43
43
  || Object.keys(start).some((key) => key !== "input" && key !== "options")) {
@@ -6,7 +6,7 @@ title: "Gespräch an einen Moderator übergeben"
6
6
  description: "Zeigt die Übergabe eines laufenden Chats an einen anderen Primary-Actor. Der Moderator übernimmt als direkter Ansprechpartner und hält den Aufgabenfortschritt fest."
7
7
  category: "Zusammenarbeit"
8
8
  order: 210
9
- tags: "Rückfragen, To-dos, Anwendungsfall, Konzeptdemo, Agententeams, Primary-Actor, Canvas-Layout"
9
+ tags: "Rückfragen, To-dos, Anwendungsfall, Konzeptdemo, Agententeams, Primary-Actor"
10
10
  ---
11
11
 
12
12
  Ich hätte gern einen Workshop für einen ruhigeren Arbeitstag: Ein Planer bringt zwei Ideen mit, ein skeptischer Gast prüft die Nachteile. Ein Moderator übernimmt die Runde und spricht danach direkt mit mir im Chat, damit ich einen Vorschlag auswählen kann. Zeig die drei auf der Fläche und übergib das Gespräch wirklich an den Moderator. Seine Aufgabenliste zeigt den Fortschritt.
@@ -2,25 +2,25 @@ export const referenceWalkthroughs = [
2
2
  {
3
3
  id: "mini-app-owner-inspector",
4
4
  title: "Den Helfer hinter einer Mini-App öffnen",
5
- description: "Die Mini-App bleibt auf der Fläche; ihr Besitzer ist auch ohne zusätzliche Karte erreichbar.",
6
- tags: ["Anwendungsfall", "Konzeptdemo", "Persönliche Canvas-Ansicht"],
5
+ description: "Die Mini-App bleibt als Kachel sichtbar; ihr Besitzer ist auch ohne eigene Kachel erreichbar.",
6
+ tags: ["Anwendungsfall", "Konzeptdemo", "Kachelfläche"],
7
7
  steps: [
8
- "Im Profil core Sammelboard einrichten öffnen und den Leitfaden abschließen. Erwartung: Die Liste steht auf dem Canvas; die Karte ihres Besitzers ist in der Standardansicht ausgeblendet.",
9
- "Unten Actors öffnen und den Namen des Listenbesitzers wählen. Erwartung: Rechts öffnen sich dessen Chat und Details, ohne eine zusätzliche Karte einzublenden.",
10
- "In der Actorliste die Canvas-Sichtbarkeit dieses Actors einschalten. Erwartung: Seine Karte erscheint zusätzlich; die Mini-App bleibt bedienbar und behält ihre Einträge.",
11
- "Die Karte wieder ausblenden und die App erneut bedienen. Erwartung: Actor und Funktionen arbeiten weiter. Unter Ansicht hebt Ansicht zurücksetzen die persönliche Einzelentscheidung wieder auf.",
8
+ "Im Profil core Sammelboard einrichten öffnen und den Leitfaden abschließen. Erwartung: Die Liste steht als Kachel auf der Fläche; ihr Besitzer hat keine eigene Kachel.",
9
+ "In der Kopfzeile Actors öffnen und den Namen des Listenbesitzers wählen. Erwartung: Rechts öffnen sich dessen Chat und Details, ohne die Kachelaufteilung zu ändern.",
10
+ "Den Besitzer aus der Kopfzeile auf die Kachel der Liste ziehen und rechts andocken. Erwartung: Seine Kachel erscheint neben der Liste; die Mini-App bleibt bedienbar und behält ihre Einträge.",
11
+ "Die neue Kachel über das Kreuz in ihrer Kopfzeile wieder entfernen und die App erneut bedienen. Erwartung: Actor und Funktionen arbeiten weiter; sein Name bleibt in der Kopfzeile erreichbar.",
12
12
  ],
13
13
  },
14
14
  {
15
- id: "personal-conversation-canvas",
16
- title: "Eine Gesprächsrunde übersichtlicher anzeigen",
17
- description: "Actor-Karten und Verbindungen lassen sich persönlich ausblenden, ohne den Gesprächsaufbau zu verändern.",
18
- tags: ["Anwendungsfall", "Konzeptdemo", "Persönliche Canvas-Ansicht"],
15
+ id: "personal-tile-arrangement",
16
+ title: "Eine Gesprächsrunde anders aufteilen",
17
+ description: "Kacheln lassen sich persönlich umstellen und wieder auf die Programmvorgabe zurücksetzen.",
18
+ tags: ["Anwendungsfall", "Konzeptdemo", "Kachelfläche"],
19
19
  steps: [
20
- "Im Profil core den Skill-Einstieg Eine Gesprächsrunde auf der Fläche anordnen ausführen und den Aufbau abwarten. Die sichtbaren Karten und Verbindungen ansehen.",
21
- "Unter Actors einen Gesprächspartner vom Canvas ausblenden. Erwartung: Seine Karte und ihre Verbindungen verschwinden; sein Name in der Liste öffnet weiterhin den Inspector. Die übrigen Beteiligten arbeiten weiter.",
22
- "Unter Ansicht Verbindungen anzeigen ausschalten und denselben Run im selben Browser neu laden. Erwartung: Die persönliche Auswahl bleibt erhalten; die Actors und das gespeicherte Layout sind unverändert.",
23
- "Unter Ansicht Ansicht zurücksetzen wählen. Erwartung: Verbindungen und gewöhnliche Actor-Karten erscheinen wieder; Actors mit eigener Mini-App bleiben nach der Standardvorgabe ausgeblendet. Bei einem gestoppten Actor bleiben sein Name und sein Verlauf in der Liste erreichbar.",
20
+ "Im Profil core den Einstieg Gesprächsrunde einrichten ausführen und den Aufbau abwarten. Die vom Run gesetzte Aufteilung ansehen.",
21
+ "Eine Kachel an ihrer Kopfzeile fassen und am oberen Rand einer anderen Kachel andocken. Erwartung: Die Aufteilung ändert sich sofort; alle Gesprächspartner arbeiten weiter.",
22
+ "Die Trennlinie zwischen zwei Kacheln verschieben und denselben Run im selben Browser neu laden. Erwartung: Die persönliche Aufteilung bleibt erhalten; die Actors und die Programmanordnung sind unverändert.",
23
+ "In der Statusleiste Programmvorgabe übernehmen wählen. Erwartung: Die Fläche zeigt wieder die Aufteilung des Runs; der Knopf verschwindet, bis wieder eine eigene Änderung vorliegt.",
24
24
  ],
25
25
  },
26
26
  {
@@ -37,6 +37,8 @@ export interface WorkspaceClientExecution {
37
37
  }
38
38
 
39
39
  export interface WorkspaceClientOptions {
40
+ /** Die Host-Wurzel dieses Rechners, aus der die Sprachserver aufgelöst werden; sie kann erst später entstehen. */
41
+ hostRoot: () => string | undefined;
40
42
  onExecuted?: (execution: WorkspaceClientExecution) => void;
41
43
  }
42
44
 
@@ -75,7 +77,7 @@ export class WorkspaceClient {
75
77
  constructor(
76
78
  private readonly transport: WorkspaceClientTransport,
77
79
  identity: WorkspaceClientIdentity,
78
- private readonly options: WorkspaceClientOptions = {},
80
+ private readonly options: WorkspaceClientOptions,
79
81
  ) {
80
82
  this.#identity = { ...identity, folders: [...identity.folders] };
81
83
  this.#executor = new SandboxWorkspaceExecutor({
@@ -198,6 +200,7 @@ export class WorkspaceClient {
198
200
  root: await realPathOf(cwd),
199
201
  home: { home: homedir() },
200
202
  logDirectory: join(tmpdir(), "ragents-workspace-logs", input.runId),
203
+ hostRoot: this.options.hostRoot(),
201
204
  additions: input.env,
202
205
  }));
203
206
  const startedAt = Date.now();
@@ -1,13 +1,14 @@
1
+ // Der Hook löst @aicontainer/* auch für eine Profildatei außerhalb des Hosts auf; im Paket gibt es dort kein node_modules.
2
+ import "../../apps/server/src/host-resolution.ts";
1
3
  import { spawn } from "node:child_process";
2
4
  import { randomUUID } from "node:crypto";
3
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
5
+ import { closeSync, mkdirSync, openSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
4
6
  import path from "node:path";
5
- import { fileURLToPath, pathToFileURL } from "node:url";
7
+ import { fileURLToPath } from "node:url";
6
8
  import { coreContracts } from "../../apps/server/src/api/contracts.ts";
7
9
  import { profileFileName } from "../../apps/server/src/config-file.ts";
8
- import { isEnvironmentReference } from "../../apps/server/src/config-definition.ts";
9
- import { defaultDataDirectory } from "../../apps/server/src/data-directory.ts";
10
10
  import { hostRoot } from "../../apps/server/src/host-version.ts";
11
+ import { readProfileTarget, type ProfileTarget } from "../../apps/server/src/profile-target.ts";
11
12
  import { RpcClient } from "../../apps/web/src/rpc/client.ts";
12
13
  import { WORKSPACE_BINDING_OPTION_ID } from "../../plugins/ragents.workspace/contract.ts";
13
14
  import { JournalReader, journalLines, readJournal, RUN_ID_PATTERN, type JournalEvent } from "./journal.ts";
@@ -33,7 +34,8 @@ export const usage = (): string => `Verwendung: ragents <befehl> [argumente]
33
34
  Vorgabeprofil ist ${DEFAULT_PROFILE}. Exit-Code: 0 fertig, 2 abgebrochen, 1 fehlgeschlagen oder
34
35
  Verbindungsproblem. Die letzte Zeile auf stdout ist "run: <id>". Die Adresse kommt aus
35
36
  <Datenordner>/host.json, sonst aus RAGENTS_URL, sonst aus host.PORT des Profils; RAGENTS_TOKEN
36
- geht als Bearer-Token mit, falls das Profil eine Anmeldung verlangt.`;
37
+ geht als Bearer-Token mit, falls das Profil eine Anmeldung verlangt. Der so gestartete Host baut
38
+ die Oberfläche nicht - ein Agent braucht sie nicht; mit Oberfläche startet ragents start <profil>.`;
37
39
 
38
40
  export type AgentCommand =
39
41
  | { readonly kind: "run"; readonly profile: string; readonly folder: string; readonly text: string; readonly entry: string | undefined; readonly json: boolean }
@@ -123,14 +125,6 @@ export const parseArguments = (argv: readonly string[]): AgentCommand => {
123
125
  throw new Error(command ? `Unbekannter Befehl: ${command}\n\n${usage()}` : usage());
124
126
  };
125
127
 
126
- export interface ProfileTarget {
127
- readonly profile: string;
128
- readonly profileFile: string;
129
- readonly port: number;
130
- readonly dataDirectory: string;
131
- readonly baseUrl: string;
132
- }
133
-
134
128
  export interface HostRecord {
135
129
  readonly profile: string;
136
130
  readonly url: string;
@@ -155,17 +149,8 @@ export const writeHostRecord = (dataDirectory: string, record: HostRecord): void
155
149
  writeFileSync(hostRecordFile(dataDirectory), `${JSON.stringify(record, null, 2)}\n`);
156
150
  };
157
151
 
158
- export const loadProfile = async (profile: string, root = hostRoot()): Promise<ProfileTarget> => {
159
- const profileFile = path.join(root, profileFileName(profile));
160
- if (!existsSync(profileFile)) throw new Error(`Die Profildatei fehlt: ${profileFile}`);
161
- const module = await import(pathToFileURL(profileFile).href) as { config: { host: { PORT?: number; DATA_DIR?: unknown } } };
162
- const port = Number(process.env.PORT || module.config.host.PORT);
163
- if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`host.PORT des Profils ${profile} ist keine gültige Portnummer.`);
164
- const configured = module.config.host.DATA_DIR;
165
- const fromFile = isEnvironmentReference(configured) ? process.env[configured.name] : typeof configured === "string" ? configured : undefined;
166
- const dataDirectory = path.resolve(process.env.DATA_DIR ?? fromFile ?? defaultDataDirectory(profile));
167
- return { profile, profileFile, port, dataDirectory, baseUrl: `http://localhost:${port}` };
168
- };
152
+ export const loadProfile = (profile: string, root = hostRoot()): Promise<ProfileTarget> =>
153
+ readProfileTarget(profile, path.join(root, profileFileName(profile)));
169
154
 
170
155
  /** pnpm und der bin-Befehl führen dieses Skript mit dem Arbeitsverzeichnis apps/server aus; ein relativer Ordner gilt trotzdem ab dem Aufrufer. */
171
156
  export const callerDirectory = (): string => process.env.RAGENTS_CWD ?? process.env.INIT_CWD ?? process.cwd();
@@ -39,12 +39,17 @@ const ROOT_FILE_SOURCES = new Set(PACKAGE_ROOT_FILES.map(([source]) => source));
39
39
 
40
40
  const skippedInPackage = (relative: string): boolean => SKIPPED_IN_PACKAGE.test(relative) || ROOT_FILE_SOURCES.has(relative);
41
41
 
42
- /** Was der Host zur Laufzeit lädt: Server, Engine, Plugins, Profile und die Skripte hinter den Unterbefehlen. */
42
+ /** Was der Host zur Laufzeit lädt: Server, Engine, Plugins, Profile, den Web-Build und die Skripte hinter den Unterbefehlen. */
43
43
  export const packageContents = (root = repositoryRoot): readonly string[] => [
44
44
  "apps/server/package.json",
45
+ "apps/server/tsconfig.json",
45
46
  "apps/server/src",
46
47
  "apps/server/agent-home/settings.json",
47
48
  "apps/web/package.json",
49
+ "apps/web/tsconfig.json",
50
+ "apps/web/vite.config.ts",
51
+ "apps/web/index.html",
52
+ "apps/web/column.html",
48
53
  "apps/web/src",
49
54
  ...readdirSync(path.join(root, "packages"), { withFileTypes: true })
50
55
  .filter((entry) => entry.isDirectory())
@@ -58,10 +63,10 @@ export const packageContents = (root = repositoryRoot): readonly string[] => [
58
63
  "scripts/workspace-client",
59
64
  ];
60
65
 
61
- /** Was der Host ausführt oder in eine Mini-App bündelt; die web-Ordner der Plugins baut der zentrale Server. */
62
- const SCANNED = ["apps/server/src", "apps/web/src", "packages", "plugins", "scripts"];
66
+ /** Was der Host ausführt, in eine Mini-App bündelt oder beim Start in sein Web baut. */
67
+ const SCANNED = ["apps/server/src", "apps/web", "packages", "plugins", "scripts"];
63
68
  const PACKAGE_NAME_PATTERN = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
64
- const SCAN_SKIPPED = new Set(["web", "node_modules", "dist"]);
69
+ const SCAN_SKIPPED = new Set(["node_modules", "dist"]);
65
70
  const SCANNED_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".mjs", ".js", ".css"]);
66
71
  const IMPORT_PATTERN = /\b(?:from|import|require(?:\.resolve)?)\s*\(?\s*["']([^"'\n]+)["']/g;
67
72
 
@@ -114,10 +119,18 @@ const packageNameOf = (specifier: string): string | undefined => {
114
119
  return PACKAGE_NAME_PATTERN.test(name) ? name : undefined;
115
120
  };
116
121
 
117
- /** Die Pakete, die der ausgeführte Teil des Pakets wirklich importiert, plus die zur Laufzeit verlinkten Bibliotheken. */
122
+ const declaredWebDependencies = (packageDirectory: string): readonly string[] => {
123
+ const file = path.join(packageDirectory, "apps/web/package.json");
124
+ const declared = (JSON.parse(readFileSync(file, "utf8")) as { dependencies?: Record<string, string> }).dependencies ?? {};
125
+ return Object.keys(declared);
126
+ };
127
+
128
+ /** Die Pakete, die der ausgeführte Teil des Pakets wirklich importiert, plus die zur Laufzeit verlinkten Bibliotheken
129
+ * und die, die das Web des Hosts den web-Hälften fremder Plugins anbietet. */
118
130
  export const requiredPackages = (packageDirectory: string): ReadonlyMap<string, string> => {
119
131
  const sources = new Map<string, string>();
120
132
  for (const name of runtimeLibraries) sources.set(name, "plugins/ragents.actor-programs (verlinkt zur Laufzeit)");
133
+ for (const name of declaredWebDependencies(packageDirectory)) sources.set(name, "apps/web/package.json (für die web-Hälften der Plugins)");
121
134
  for (const folder of scanRoots(packageDirectory)) {
122
135
  for (const file of scannedFiles(folder)) {
123
136
  const content = readFileSync(file, "utf8");
@@ -1,6 +1,7 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { compareVersions, nextVersion, readVersion, versionLine, writeHostPackageVersion, writeVersion } from "../publish-version.ts";
4
5
  import { buildPackage, PACKAGE_FOLDER, PACKAGE_NAME } from "./build-package.ts";
5
6
 
6
7
  const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
@@ -8,7 +9,8 @@ const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url))
8
9
  const usage = `Verwendung: pnpm publish:package [--dry-run]
9
10
  Baut das Paket ${PACKAGE_NAME} und veröffentlicht es auf npm. Der Token kommt aus der
10
11
  Umgebungsvariable npm_key. --dry-run reicht npm den Probelauf durch und veröffentlicht nichts.
11
- Die Fassung steht in der package.json der Wurzel und wird von Hand erhöht.`;
12
+ Die Fassung steht in der package.json der Wurzel; das Skript zählt vor dem Bauen die letzte Stelle
13
+ über die zuletzt veröffentlichte hoch, im Probelauf nur in der Ausgabe.`;
12
14
 
13
15
  const REGISTRY_TOKEN_KEY = "npm_config_//registry.npmjs.org/:_authToken";
14
16
 
@@ -52,6 +54,10 @@ export const publishedVersions = (name: string, npm: NpmRunner): readonly string
52
54
  return typeof parsed === "string" ? [parsed] : parsed;
53
55
  };
54
56
 
57
+ /** Die höchste Fassung, die auf npm liegt; ein dort unbekanntes Paket hat keine. */
58
+ export const latestPublishedVersion = (npm: NpmRunner = runNpm, name = PACKAGE_NAME): string | undefined =>
59
+ [...publishedVersions(name, npm)].sort(compareVersions).at(-1);
60
+
55
61
  export interface PublishPlan {
56
62
  readonly name: string;
57
63
  readonly version: string;
@@ -59,13 +65,13 @@ export interface PublishPlan {
59
65
  }
60
66
 
61
67
  /** Was vor einem Publish stimmen muss: das gebaute Manifest und eine Fassung, die es auf npm noch nicht gibt. */
62
- export const publishPlan = (manifest: Record<string, unknown>, published: readonly string[]): PublishPlan => {
63
- const { name, version } = manifest as { name?: unknown; version?: unknown };
68
+ export const publishPlan = (manifest: Record<string, unknown>, published: readonly string[], version: string): PublishPlan => {
69
+ const { name } = manifest as { name?: unknown };
64
70
  const access = (manifest.publishConfig as { access?: unknown } | undefined)?.access;
65
71
  const hostVersion = (manifest.ragents as { hostVersion?: unknown } | undefined)?.hostVersion;
66
72
  if (name !== PACKAGE_NAME) throw new Error(`Das gebaute Paket heißt ${String(name)}, veröffentlicht wird ${PACKAGE_NAME}`);
67
73
  if (access !== "public") throw new Error(`${PACKAGE_NAME} hat einen Scope und braucht publishConfig.access "public", nicht ${JSON.stringify(access)}`);
68
- if (typeof version !== "string" || !/^\d+\.\d+\.\d+(?:-[0-9a-z.-]+)?$/.test(version)) {
74
+ if (!/^\d+\.\d+\.\d+(?:-[0-9a-z.-]+)?$/.test(version)) {
69
75
  throw new Error(`Die Fassung ${JSON.stringify(version)} ist keine Fassung; sie steht als version in der package.json der Wurzel`);
70
76
  }
71
77
  if (typeof hostVersion !== "string" || !/^[0-9a-f]{40}$/.test(hostVersion)) {
@@ -80,6 +86,7 @@ export const publishPlan = (manifest: Record<string, unknown>, published: readon
80
86
 
81
87
  export interface PublishOptions {
82
88
  readonly directory: string;
89
+ readonly version: string;
83
90
  readonly token: string;
84
91
  readonly dryRun: boolean;
85
92
  readonly npm: NpmRunner;
@@ -92,7 +99,7 @@ const scopeAdvice = (output: string): string => /Scope not found/i.test(output)
92
99
  + "auf npmjs.com anlegen bzw. den Token für den Scope freigeben."
93
100
  : "";
94
101
 
95
- /** Veröffentlicht den gebauten Ordner und nennt danach die Fassung, die npm ausliefert. */
102
+ /** Veröffentlicht den gebauten Ordner; der Exit 0 von npm publish ist die Bestätigung. */
96
103
  export const publishDirectory = (options: PublishOptions): void => {
97
104
  const hide = redacting(options.token);
98
105
  const published = options.npm(["publish", "--access", "public", ...options.dryRun ? ["--dry-run"] : []], { cwd: options.directory, token: options.token });
@@ -103,9 +110,7 @@ export const publishDirectory = (options: PublishOptions): void => {
103
110
  options.log("== Probelauf: nichts veröffentlicht");
104
111
  return;
105
112
  }
106
- const view = options.npm(["view", PACKAGE_NAME, "version"], { cwd: repositoryRoot });
107
- if (view.status !== 0) throw new Error(`npm view ${PACKAGE_NAME} version endete mit Code ${view.status}: ${hide(oneLine(view.stderr))}`);
108
- options.log(`== Auf npm steht jetzt ${PACKAGE_NAME}@${view.stdout.trim()}`);
113
+ options.log(`== Veröffentlicht: ${PACKAGE_NAME}@${options.version}`);
109
114
  };
110
115
 
111
116
  const main = async (): Promise<void> => {
@@ -114,10 +119,19 @@ const main = async (): Promise<void> => {
114
119
  if (unknown.length) throw new Error(`Unbekanntes Argument: ${unknown.join(" ")}\n${usage}`);
115
120
  const token = process.env.npm_key;
116
121
  if (!token) throw new Error(`npm_key fehlt: der npm-Token mit Schreibrecht auf ${PACKAGE_NAME}.\n${usage}`);
122
+ const manifestFile = path.join(repositoryRoot, "package.json");
123
+ const published = publishedVersions(PACKAGE_NAME, runNpm);
124
+ const next = nextVersion(readVersion(manifestFile), published);
125
+ console.log(versionLine(next));
126
+ const extensionManifestFile = path.join(repositoryRoot, "apps", "vscode", "package.json");
127
+ if (!dryRun) {
128
+ writeVersion(manifestFile, next.version);
129
+ writeHostPackageVersion(extensionManifestFile, next.version);
130
+ }
117
131
  const built = await buildPackage(path.join(repositoryRoot, "dist", PACKAGE_FOLDER));
118
- const plan = publishPlan(built.manifest, publishedVersions(PACKAGE_NAME, runNpm));
132
+ const plan = publishPlan(built.manifest, published, next.version);
119
133
  console.log(`== ${plan.name}@${plan.version} aus Host ${plan.hostVersion.slice(0, 12)}, ${built.fileCount} Dateien, ${(built.byteSize / 1024 / 1024).toFixed(1)} MB`);
120
- publishDirectory({ directory: built.directory, token, dryRun, npm: runNpm, log: (line) => console.log(line) });
134
+ publishDirectory({ directory: built.directory, version: plan.version, token, dryRun, npm: runNpm, log: (line) => console.log(line) });
121
135
  };
122
136
 
123
137
  const moduleUrl: string | undefined = import.meta.url;
@@ -20,12 +20,13 @@ export const commands = {
20
20
 
21
21
  const usage = `Verwendung: ragents <befehl> [argumente]
22
22
 
23
- run <ordner> "<auftrag>" Host starten, Run auf dem Ordner anlegen und auf das Ende warten
23
+ run <ordner> "<auftrag>" Host ohne Oberfläche starten, Run anlegen und auf das Ende warten
24
24
  send <run> "<text>" Folgeauftrag im selben Run
25
25
  journal <run> [--tools] Verlauf eines Runs lesen
26
26
  stop <run> | stop --host laufenden Turn abbrechen oder den gemerkten Host beenden
27
27
  connect <server-url> Client-Profil holen und den lokalen Server damit starten
28
- start <profil> ein Profil des Pakets oder einen geholten Stand starten
28
+ start <profil|pfad> ein Profil des Pakets, eine eigene Profildatei oder einen
29
+ geholten Stand starten; baut die Oberfläche bei Bedarf
29
30
  provision [<profil>|--workspace] Werkzeuge der Plugins holen
30
31
  workspace-client <server-url> diesen Rechner als Arbeitsplatz anmelden
31
32
  `;
@@ -8,6 +8,8 @@ import { fileURLToPath } from "node:url";
8
8
  import { extract as extractTar } from "tar";
9
9
  import { ragentsDataRoot } from "../../apps/server/src/data-directory.ts";
10
10
  import { hostRoot, readHostPackage, readHostVersion } from "../../apps/server/src/host-version.ts";
11
+ import { readProfileTarget } from "../../apps/server/src/profile-target.ts";
12
+ import { ensureWebBuild } from "../../apps/server/src/web-build.ts";
11
13
  import { RpcClient } from "../../apps/web/src/rpc/client.ts";
12
14
  import { profileDistributionContracts, type ClientProfileDescription } from "../../plugins/ragents.profile-distribution/contract.ts";
13
15
 
@@ -226,24 +228,33 @@ export interface StartedProfile {
226
228
  readonly profileFile: string;
227
229
  /** Nur bei einem geholten Stand gesetzt; ein Profil des Hosts nimmt die Vorgaben seiner eigenen Konfiguration. */
228
230
  readonly dataDirectory?: string;
231
+ /** Nur bei einem geholten Stand gesetzt; sonst baut der Start das Web des Profils selbst. */
229
232
  readonly webDirectory?: string;
230
233
  readonly port: number | undefined;
231
234
  }
232
235
 
233
- /** Provisioniert die Werkzeuge des Profils und startet den Server damit. */
236
+ /** Provisioniert die Werkzeuge des Profils, baut sein Web falls nötig und startet den Server damit. */
234
237
  export const startProfile = async (started: StartedProfile): Promise<number> => {
238
+ const dataDirectory = started.dataDirectory ?? (await readProfileTarget(started.profile, started.profileFile)).dataDirectory;
235
239
  const env: NodeJS.ProcessEnv = {
236
240
  ...process.env,
237
241
  PRODUCT_PROFILE: started.profile,
238
242
  PRODUCT_PROFILE_FILE: started.profileFile,
239
- ...(started.dataDirectory ? { DATA_DIR: started.dataDirectory } : {}),
240
- ...(started.webDirectory ? { WEB_DIST_DIR: started.webDirectory } : {}),
243
+ DATA_DIR: dataDirectory,
241
244
  };
242
245
  console.log("== Werkzeuge provisionieren");
243
246
  const provisioned = await run(hostScript("scripts/provision/run-provision.ts"), env);
244
247
  if (provisioned !== 0) throw new Error(`Die Provisionierung endete mit Code ${provisioned}`);
245
- console.log(started.webDirectory ? `== Server starten (Web ${started.webDirectory})` : "== Server starten");
246
- return run(hostScript("apps/server/src/main.ts", started.port !== undefined ? ["--port", String(started.port)] : []), env);
248
+ const webDirectory = started.webDirectory ?? await ensureWebBuild({
249
+ root: hostRoot(),
250
+ profile: started.profile,
251
+ profileFile: started.profileFile,
252
+ dataDirectory,
253
+ environment: env,
254
+ log: (line) => console.log(line),
255
+ });
256
+ console.log(`== Server starten (Web ${webDirectory}, Daten ${dataDirectory})`);
257
+ return run(hostScript("apps/server/src/main.ts", started.port !== undefined ? ["--port", String(started.port)] : []), { ...env, WEB_DIST_DIR: webDirectory });
247
258
  };
248
259
 
249
260
  /** Startet einen Stand, den connect geholt hat, mit dem Web aus dem Cache. */
@@ -1,3 +1,5 @@
1
+ // Der Hook löst @aicontainer/* auch für eine Profildatei außerhalb des Hosts auf; im Paket gibt es dort kein node_modules.
2
+ import "../../apps/server/src/host-resolution.ts";
1
3
  import { statSync } from "node:fs";
2
4
  import { readFile, readdir } from "node:fs/promises";
3
5
  import path from "node:path";
@@ -5,14 +7,16 @@ import { fileURLToPath } from "node:url";
5
7
  import { profileFileName } from "../../apps/server/src/config-file.ts";
6
8
  import { ragentsDataRoot } from "../../apps/server/src/data-directory.ts";
7
9
  import { hostRoot } from "../../apps/server/src/host-version.ts";
10
+ import { callerDirectory } from "../agent/agent-cli.ts";
8
11
  import { startCached, startProfile, type CachedProfile } from "./connect.ts";
9
12
 
10
- const usage = (): string => `Verwendung: ragents start <profil> [--port <n>]
11
- Startet ein Profil dieses Hosts (core, developer) oder einen Stand, den ragents connect schon
12
- geholt hat, ohne den Server zu fragen. Ein geholter Stand bringt sein Web aus demselben Cache
13
- mit. Ohne Argument nennt der Befehl die Profile im Cache.`;
13
+ const usage = (): string => `Verwendung: ragents start <profil|pfad> [--port <n>]
14
+ Startet ein Profil dieses Hosts (core, developer), eine eigene ragents.config.<profil>.ts an
15
+ beliebiger Stelle oder einen Stand, den ragents connect schon geholt hat, ohne den Server zu
16
+ fragen. Ein eigenes Profil bekommt sein Web beim Start gebaut, ein geholter Stand bringt es aus
17
+ demselben Cache mit. Ohne Argument nennt der Befehl die Profile im Cache.`;
14
18
 
15
- export const parseArguments = (argv: readonly string[]): { profile: string; port: number | undefined } => {
19
+ export const parseArguments = (argv: readonly string[]): { selection: string; port: number | undefined } => {
16
20
  let profile: string | undefined;
17
21
  let port: number | undefined;
18
22
  for (let index = 0; index < argv.length; index += 1) {
@@ -28,7 +32,7 @@ export const parseArguments = (argv: readonly string[]): { profile: string; port
28
32
  profile = argument;
29
33
  }
30
34
  if (!profile) throw new Error(`Der Profilname fehlt.\n${usage()}`);
31
- return { profile, port };
35
+ return { selection: profile, port };
32
36
  };
33
37
 
34
38
  const directories = async (folder: string): Promise<readonly string[]> =>
@@ -60,22 +64,36 @@ export const selectCachedProfile = (cached: readonly CachedProfile[], profile: s
60
64
  + "nimm ragents connect <server-url>.");
61
65
  };
62
66
 
63
- /** Ein Profil, das neben dem Host liegt: im Checkout die Datei im Repo, im Paket die mitgelieferte. */
64
- export const hostProfileFile = (profile: string, root = hostRoot()): string | undefined => {
65
- const file = path.join(root, profileFileName(profile));
66
- return statSync(file, { throwIfNoEntry: false })?.isFile() ? file : undefined;
67
+ const PROFILE_FILE = /^ragents\.config\.([a-z0-9]+(?:-[a-z0-9]+)*)\.ts$/;
68
+
69
+ export interface LocalProfile {
70
+ readonly profile: string;
71
+ readonly profileFile: string;
72
+ }
73
+
74
+ /** Ein Profil ist ein Name neben dem Host (Checkout wie Paket) oder ein Pfad zu einer ragents.config.<profil>.ts an beliebiger Stelle. */
75
+ export const localProfile = (selection: string, root = hostRoot(), caller = callerDirectory()): LocalProfile | undefined => {
76
+ if (!selection.includes("/") && !selection.endsWith(".ts")) {
77
+ const file = path.join(root, profileFileName(selection));
78
+ return statSync(file, { throwIfNoEntry: false })?.isFile() ? { profile: selection, profileFile: file } : undefined;
79
+ }
80
+ const profileFile = path.resolve(caller, selection);
81
+ const match = PROFILE_FILE.exec(path.basename(profileFile));
82
+ if (!match) throw new Error(`Eine Profildatei heißt ragents.config.<profil>.ts, nicht ${path.basename(profileFile)}`);
83
+ if (!statSync(profileFile, { throwIfNoEntry: false })?.isFile()) throw new Error(`Die Profildatei fehlt: ${profileFile}`);
84
+ return { profile: match[1]!, profileFile };
67
85
  };
68
86
 
69
87
  const main = async (): Promise<void> => {
70
88
  const arguments_ = parseArguments(process.argv.slice(2));
71
- const own = hostProfileFile(arguments_.profile);
89
+ const own = localProfile(arguments_.selection);
72
90
  if (own) {
73
- console.log(`== Profil ${arguments_.profile} dieses Hosts`);
74
- console.log(`== Profildatei ${own}`);
75
- process.exitCode = await startProfile({ profile: arguments_.profile, profileFile: own, port: arguments_.port });
91
+ console.log(`== Profil ${own.profile}`);
92
+ console.log(`== Profildatei ${own.profileFile}`);
93
+ process.exitCode = await startProfile({ profile: own.profile, profileFile: own.profileFile, port: arguments_.port });
76
94
  return;
77
95
  }
78
- const cached = selectCachedProfile(await cachedProfiles(ragentsDataRoot()), arguments_.profile);
96
+ const cached = selectCachedProfile(await cachedProfiles(ragentsDataRoot()), arguments_.selection);
79
97
  console.log(`== Profil ${cached.profile} vom Server ${cached.serverUrl}, Stand ${cached.version.slice(0, 12)} (aus dem Cache)`);
80
98
  console.log(`== Profildatei ${cached.profileFile}`);
81
99
  console.log(`== Daten ${cached.dataDirectory}`);
@@ -3,6 +3,7 @@ import { statSync } from "node:fs";
3
3
  import { hostname } from "node:os";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { hostRoot } from "../../apps/server/src/host-version.ts";
6
7
  import { provisionWorkspace } from "../../apps/server/src/profile/provisioning.ts";
7
8
  import { workspaceClientTransport } from "../../plugins/ragents.workspace/client/transport.ts";
8
9
  import { WorkspaceClient } from "../../plugins/ragents.workspace/client/workspace-client.ts";
@@ -13,7 +14,7 @@ dieses Rechners aus - dasselbe, was die VS-Code-Erweiterung tut, nur ohne VS Cod
13
14
  gilt das aktuelle Verzeichnis. Jeder Werkzeugaufruf erscheint als eine Zeile auf stdout.
14
15
  Die Sprachserver holt der Start in den Werkzeugordner dieses Rechners (dasselbe wie
15
16
  pnpm provision --workspace); ROSLYN_LANGUAGE_SERVER und FSHARP_LANGUAGE_SERVER übersteuern ihn,
16
- TypeScript bringt der Executor mit. Das Skript läuft, bis es mit Strg-C beendet wird.`;
17
+ TypeScript kommt aus dem Host-Ordner. Das Skript läuft, bis es mit Strg-C beendet wird.`;
17
18
 
18
19
  export interface WorkspaceClientArguments {
19
20
  readonly serverUrl: string;
@@ -71,6 +72,7 @@ const main = async (): Promise<void> => {
71
72
  };
72
73
  const transport = workspaceClientTransport(parsed.serverUrl, process.env.RAGENTS_TOKEN);
73
74
  const client = new WorkspaceClient(transport, identity, {
75
+ hostRoot,
74
76
  onExecuted: ({ runId, tool, durationMs, error }) =>
75
77
  console.log(`== ${runId.slice(0, 8)} ${tool} ${durationMs} ms ${error ?? "ok"}`),
76
78
  });
@@ -1,40 +0,0 @@
1
- import { useEffect, useId, useState } from "react";
2
- import { useAccess } from "./AccessContext";
3
- import { DEFAULT_MATERIAL_SETTINGS, MATERIAL_SETTINGS_LIMITS, resetMaterialSettings, saveMaterialSettings, useMaterialSettings } from "./material-settings";
4
- import { Button, Input } from "./ui";
5
-
6
- const noteClasses = "text-sm leading-[1.6] text-muted-foreground";
7
-
8
- export function MaterialSettings() {
9
- const { settings, error } = useMaterialSettings();
10
- const writable = useAccess().can("settings.write");
11
- const prefix = useId();
12
- const [steps, setSteps] = useState(String(settings.steps));
13
- useEffect(() => { setSteps(String(settings.steps)); }, [settings]);
14
- const valid = (text: string, min: number, max: number) => text.trim() !== "" && Number.isInteger(Number(text)) && Number(text) >= min && Number(text) <= max;
15
- const stepsValid = valid(steps, MATERIAL_SETTINGS_LIMITS.minSteps, MATERIAL_SETTINGS_LIMITS.maxSteps);
16
- const changeSteps = (value: string) => {
17
- if (!writable) return;
18
- setSteps(value);
19
- if (valid(value, MATERIAL_SETTINGS_LIMITS.minSteps, MATERIAL_SETTINGS_LIMITS.maxSteps)) saveMaterialSettings({ ...settings, steps: Number(value) });
20
- };
21
- return <div className="flex min-w-0 max-w-[680px] flex-col gap-5">
22
- <p className={noteClasses}>Schichtwerk mit matten Flächen und gestapelter Tiefe. Änderungen gelten sofort für alle Runs und werden in diesem Browser gespeichert.</p>
23
- <div className="flex flex-col gap-2">
24
- <label className="text-sm font-semibold text-foreground" id={`${prefix}-steps-label`} htmlFor={`${prefix}-steps`}>Tiefenstufen</label>
25
- <div className="flex min-w-0 items-center gap-5">
26
- <input className="w-0 min-w-0 flex-1 accent-primary" aria-labelledby={`${prefix}-steps-label`} disabled={!writable} type="range" min={MATERIAL_SETTINGS_LIMITS.minSteps} max={MATERIAL_SETTINGS_LIMITS.maxSteps} step="1" value={settings.steps} onChange={(event) => changeSteps(event.target.value)} />
27
- <Input className="w-[84px] flex-none" id={`${prefix}-steps`} aria-invalid={!stepsValid} aria-describedby={`${prefix}-steps-note`} disabled={!writable} type="number" min={MATERIAL_SETTINGS_LIMITS.minSteps} max={MATERIAL_SETTINGS_LIMITS.maxSteps} step="1" value={steps} onChange={(event) => changeSteps(event.target.value)} />
28
- </div>
29
- <p id={`${prefix}-steps-note`} className={stepsValid ? noteClasses : "text-sm leading-[1.6] text-destructive"}>{stepsValid ? `0 ist flach, bis zu 5 gleich tiefe Stufen. Standard: ${DEFAULT_MATERIAL_SETTINGS.steps}.` : "Bitte eine ganze Zahl von 0 bis 5 eingeben."}</p>
30
- </div>
31
- <div><Button variant="outline" disabled={!writable} onClick={() => {
32
- if (!writable) return;
33
- if (resetMaterialSettings()) {
34
- setSteps(String(DEFAULT_MATERIAL_SETTINGS.steps));
35
- }
36
- }}>Auf Standard zurücksetzen</Button></div>
37
- {!writable && <p className={noteClasses}>Die Materialeinstellungen sind mit deinen Rechten nur lesbar.</p>}
38
- {error && <p className="text-sm leading-[1.6] text-destructive" role="alert">{error}</p>}
39
- </div>;
40
- }