@schlenkr/ragents 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/apps/server/src/plugin-support/language-server/contract.ts +6 -1
  2. package/apps/server/src/plugin-support/language-server/tools.ts +29 -4
  3. package/apps/server/src/plugin-support/run-scripts.ts +4 -1
  4. package/apps/server/src/plugin-support/start-entries-contract.ts +4 -3
  5. package/apps/server/src/plugin-support/workspace-sandbox-host.ts +2 -0
  6. package/apps/web/src/App.tsx +0 -10
  7. package/apps/web/src/PluginChat.tsx +8 -8
  8. package/apps/web/src/PluginRegistry.tsx +0 -12
  9. package/apps/web/src/StartSurface.tsx +2 -2
  10. package/apps/web/src/ThemeSettings.tsx +1 -1
  11. package/apps/web/src/column/ColumnApp.tsx +83 -7
  12. package/apps/web/src/column/clipboard.ts +64 -0
  13. package/apps/web/src/column/host-contract.ts +10 -0
  14. package/apps/web/src/column.tsx +2 -0
  15. package/apps/web/src/panel/LoginForm.tsx +9 -3
  16. package/apps/web/src/panel/OverviewPage.tsx +187 -62
  17. package/apps/web/src/panel/PanelDialogs.tsx +115 -0
  18. package/apps/web/src/panel/PanelPage.tsx +1 -1
  19. package/apps/web/src/panel/SettingsPage.tsx +75 -79
  20. package/apps/web/src/panel/contract.ts +17 -9
  21. package/apps/web/src/panel/target-state.ts +27 -2
  22. package/apps/web/src/ui/theme.css +4 -0
  23. package/package.json +2 -2
  24. package/packages/ragents/src/plugin-host.ts +5 -2
  25. package/packages/ragents/src/plugin-types.ts +3 -2
  26. package/packages/workspace-executor/src/context.ts +4 -0
  27. package/packages/workspace-executor/src/executor.ts +9 -2
  28. package/packages/workspace-executor/src/index.ts +2 -0
  29. package/packages/workspace-executor/src/language-server/adapters/fsharp.ts +1 -0
  30. package/packages/workspace-executor/src/language-server/adapters/roslyn.ts +1 -0
  31. package/packages/workspace-executor/src/language-server/adapters/typescript.ts +34 -6
  32. package/packages/workspace-executor/src/language-server/contract.ts +7 -3
  33. package/packages/workspace-executor/src/language-server/host.ts +275 -138
  34. package/plugins/ragents.actor-programs/contract.ts +1 -1
  35. package/plugins/ragents.actor-programs/prompt.hbs +4 -4
  36. package/plugins/ragents.actor-programs/server/app-project.ts +0 -2
  37. package/plugins/ragents.actor-programs/server/runtime.ts +1 -1
  38. package/plugins/ragents.actor-programs/web/AppsPanel.tsx +3 -5
  39. package/plugins/ragents.actor-programs/web/EmbeddedApps.tsx +7 -39
  40. package/plugins/ragents.actor-programs/web/api.ts +1 -10
  41. package/plugins/ragents.lsp-roslyn/web/language-server-plugin.tsx +73 -44
  42. package/plugins/ragents.orchestration/canvas.hbs +28 -44
  43. package/plugins/ragents.orchestration/contract.ts +17 -287
  44. package/plugins/ragents.orchestration/server/canvas-tool.ts +18 -123
  45. package/plugins/ragents.orchestration/tiled-layout.ts +12 -4
  46. package/plugins/ragents.orchestration/web/ActorCanvasControls.tsx +23 -47
  47. package/plugins/ragents.orchestration/web/ActorChatPreview.tsx +3 -9
  48. package/plugins/ragents.orchestration/web/ActorShortcut.tsx +3 -5
  49. package/plugins/ragents.orchestration/web/ActorTile.tsx +3 -2
  50. package/plugins/ragents.orchestration/web/CanvasTiles.tsx +3 -3
  51. package/plugins/ragents.orchestration/web/Center.tsx +28 -463
  52. package/plugins/ragents.orchestration/web/MaterialBody.tsx +6 -87
  53. package/plugins/ragents.orchestration/web/TiledCanvas.tsx +2 -3
  54. package/plugins/ragents.orchestration/web/canvas-view-settings.ts +7 -13
  55. package/plugins/ragents.orchestration/web/column/RunColumn.tsx +44 -19
  56. package/plugins/ragents.orchestration/web/column/column-state.ts +13 -4
  57. package/plugins/ragents.orchestration/web/constants.ts +0 -2
  58. package/plugins/ragents.orchestration/web/index.tsx +0 -6
  59. package/plugins/ragents.orchestration/web/tile-docking.ts +7 -1
  60. package/plugins/ragents.orchestration/web/tiled-view-settings.ts +7 -9
  61. package/plugins/ragents.overseer/server/coordinator-prompt.ts +1 -1
  62. package/plugins/ragents.reference/examples.ts +1 -2
  63. package/plugins/ragents.reference/run-scripts/balcony-wizard/RUN.md +1 -1
  64. package/plugins/ragents.reference/run-scripts/balcony-wizard/actors/balcony-app/package.json +1 -1
  65. package/plugins/ragents.reference/run-scripts/balcony-wizard/src/server.ts +1 -1
  66. package/plugins/ragents.reference/run-scripts/balcony-wizard/tests/program.test.ts +2 -2
  67. package/plugins/ragents.reference/run-scripts/conversation-circle/RUN.md +3 -4
  68. package/plugins/ragents.reference/run-scripts/conversation-circle/src/server.ts +9 -15
  69. package/plugins/ragents.reference/run-scripts/conversation-circle/tests/program.test.ts +5 -3
  70. package/plugins/ragents.reference/run-scripts/learning-afternoon/package.json +1 -1
  71. package/plugins/ragents.reference/run-scripts/learning-afternoon/src/server.ts +14 -4
  72. package/plugins/ragents.reference/run-scripts/word-game/src/server.ts +1 -1
  73. package/plugins/ragents.reference/skills/210-moderator-handover/SKILL.md +1 -1
  74. package/plugins/ragents.reference/walkthroughs.ts +14 -14
  75. package/plugins/ragents.workspace/client/workspace-client.ts +4 -1
  76. package/scripts/package/publish-package.ts +4 -5
  77. package/scripts/workspace-client/run-workspace-client.ts +3 -1
  78. package/apps/web/src/MaterialSettings.tsx +0 -40
  79. package/apps/web/src/material-settings.ts +0 -104
  80. package/plugins/ragents.orchestration/web/ActorNet.tsx +0 -607
  81. package/plugins/ragents.orchestration/web/CardSizeSettings.tsx +0 -42
  82. package/plugins/ragents.orchestration/web/ScriptActorCard.tsx +0 -56
  83. package/plugins/ragents.orchestration/web/ZoomSettings.tsx +0 -38
  84. package/plugins/ragents.orchestration/web/canvas-layout-storage.ts +0 -39
  85. package/plugins/ragents.orchestration/web/canvas-layout.ts +0 -326
  86. package/plugins/ragents.orchestration/web/canvas-scene.ts +0 -311
  87. package/plugins/ragents.orchestration/web/card-size-settings.ts +0 -36
  88. package/plugins/ragents.orchestration/web/keyboard-navigation.ts +0 -55
  89. package/plugins/ragents.orchestration/web/material-layout.ts +0 -14
  90. package/plugins/ragents.orchestration/web/useCanvasMeasurements.ts +0 -78
  91. package/plugins/ragents.orchestration/web/zoom-settings.ts +0 -28
  92. package/plugins/ragents.reference/skills/120-canvas-conversation-circle/SKILL.md +0 -12
  93. package/plugins/ragents.reference/skills/130-canvas-pipeline-with-approval/SKILL.md +0 -12
  94. package/plugins/ragents.reference/skills/140-canvas-team-tree-and-topic-grid/SKILL.md +0 -12
@@ -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();
@@ -80,6 +80,7 @@ export const publishPlan = (manifest: Record<string, unknown>, published: readon
80
80
 
81
81
  export interface PublishOptions {
82
82
  readonly directory: string;
83
+ readonly version: string;
83
84
  readonly token: string;
84
85
  readonly dryRun: boolean;
85
86
  readonly npm: NpmRunner;
@@ -92,7 +93,7 @@ const scopeAdvice = (output: string): string => /Scope not found/i.test(output)
92
93
  + "auf npmjs.com anlegen bzw. den Token für den Scope freigeben."
93
94
  : "";
94
95
 
95
- /** Veröffentlicht den gebauten Ordner und nennt danach die Fassung, die npm ausliefert. */
96
+ /** Veröffentlicht den gebauten Ordner; der Exit 0 von npm publish ist die Bestätigung. */
96
97
  export const publishDirectory = (options: PublishOptions): void => {
97
98
  const hide = redacting(options.token);
98
99
  const published = options.npm(["publish", "--access", "public", ...options.dryRun ? ["--dry-run"] : []], { cwd: options.directory, token: options.token });
@@ -103,9 +104,7 @@ export const publishDirectory = (options: PublishOptions): void => {
103
104
  options.log("== Probelauf: nichts veröffentlicht");
104
105
  return;
105
106
  }
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()}`);
107
+ options.log(`== Veröffentlicht: ${PACKAGE_NAME}@${options.version}`);
109
108
  };
110
109
 
111
110
  const main = async (): Promise<void> => {
@@ -117,7 +116,7 @@ const main = async (): Promise<void> => {
117
116
  const built = await buildPackage(path.join(repositoryRoot, "dist", PACKAGE_FOLDER));
118
117
  const plan = publishPlan(built.manifest, publishedVersions(PACKAGE_NAME, runNpm));
119
118
  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) });
119
+ publishDirectory({ directory: built.directory, version: plan.version, token, dryRun, npm: runNpm, log: (line) => console.log(line) });
121
120
  };
122
121
 
123
122
  const moduleUrl: string | undefined = import.meta.url;
@@ -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
- }
@@ -1,104 +0,0 @@
1
- import { useSyncExternalStore } from "react";
2
- import { subscribeStorageChanges } from "./lib/local-storage-setting";
3
-
4
- export interface MaterialSettings {
5
- steps: number;
6
- }
7
-
8
- export const DEFAULT_MATERIAL_SETTINGS: MaterialSettings = Object.freeze({ steps: 1 });
9
- export const MATERIAL_SETTINGS_LIMITS = { minSteps: 0, maxSteps: 5 };
10
- export const MATERIAL_DEPTH_PER_STEP = 12;
11
- export const MATERIAL_SETTINGS_STORAGE_KEY = "ragents.material-settings";
12
-
13
- interface MaterialSettingsSnapshot {
14
- settings: MaterialSettings;
15
- error?: string;
16
- }
17
-
18
- export function parseMaterialSettings(raw: string | null): MaterialSettings {
19
- if (raw === null) return DEFAULT_MATERIAL_SETTINGS;
20
- const value: unknown = JSON.parse(raw);
21
- if (!value || typeof value !== "object" || !("steps" in value)
22
- || typeof value.steps !== "number" || !Number.isInteger(value.steps) || value.steps < MATERIAL_SETTINGS_LIMITS.minSteps || value.steps > MATERIAL_SETTINGS_LIMITS.maxSteps) {
23
- throw new Error("Ungültiges Material: Tiefenstufen müssen eine ganze Zahl von 0 bis 5 sein. Bitte die Materialeinstellungen zurücksetzen.");
24
- }
25
- return Object.freeze({ steps: value.steps });
26
- }
27
-
28
- const errorMessage = (cause: unknown) => cause instanceof Error ? cause.message : String(cause);
29
- const serverSnapshot: MaterialSettingsSnapshot = { settings: DEFAULT_MATERIAL_SETTINGS };
30
- const getServerSnapshot = () => serverSnapshot;
31
- const subscribeServer = () => () => {};
32
-
33
- export function createMaterialSettingsStore(browser: Window) {
34
- let snapshot: MaterialSettingsSnapshot;
35
- try {
36
- snapshot = { settings: parseMaterialSettings(browser.localStorage.getItem(MATERIAL_SETTINGS_STORAGE_KEY)) };
37
- } catch (cause) {
38
- snapshot = { settings: DEFAULT_MATERIAL_SETTINGS, error: `Das gespeicherte Material konnte nicht geladen werden. Die Standardwerte werden angezeigt. ${errorMessage(cause)}` };
39
- }
40
- const listeners = new Set<() => void>();
41
- let disposed = false;
42
- const publish = (next: MaterialSettingsSnapshot) => {
43
- if (snapshot.settings.steps === next.settings.steps && snapshot.error === next.error) return;
44
- snapshot = next;
45
- for (const listener of listeners) listener();
46
- };
47
- const report = (message: string) => publish({ ...snapshot, error: message });
48
- const storageChanged = (event: StorageEvent) => {
49
- try {
50
- const storage = browser.localStorage;
51
- if (event.storageArea !== storage) return;
52
- publish({ settings: parseMaterialSettings(storage.getItem(MATERIAL_SETTINGS_STORAGE_KEY)) });
53
- } catch (cause) {
54
- report(`Das Material aus einem anderen Browser-Tab konnte nicht übernommen werden. ${errorMessage(cause)}`);
55
- }
56
- };
57
- const unsubscribeStorage = subscribeStorageChanges(browser, (key) => key === MATERIAL_SETTINGS_STORAGE_KEY, storageChanged);
58
- return {
59
- getSnapshot: () => snapshot,
60
- subscribe(listener: () => void) {
61
- if (disposed) throw new Error("Die Materialeinstellungen wurden bereits beendet.");
62
- listeners.add(listener);
63
- return () => { listeners.delete(listener); };
64
- },
65
- save(settings: MaterialSettings) {
66
- if (disposed) throw new Error("Die Materialeinstellungen wurden bereits beendet.");
67
- try {
68
- const next = parseMaterialSettings(JSON.stringify(settings));
69
- browser.localStorage.setItem(MATERIAL_SETTINGS_STORAGE_KEY, JSON.stringify(next));
70
- publish({ settings: next });
71
- } catch (cause) {
72
- report(`Das Material konnte nicht gespeichert werden. ${errorMessage(cause)}`);
73
- }
74
- },
75
- reset() {
76
- if (disposed) throw new Error("Die Materialeinstellungen wurden bereits beendet.");
77
- try {
78
- browser.localStorage.removeItem(MATERIAL_SETTINGS_STORAGE_KEY);
79
- publish({ settings: DEFAULT_MATERIAL_SETTINGS });
80
- return true;
81
- } catch (cause) {
82
- report(`Das Material konnte nicht zurückgesetzt werden. ${errorMessage(cause)}`);
83
- return false;
84
- }
85
- },
86
- dispose() {
87
- if (disposed) return;
88
- disposed = true;
89
- unsubscribeStorage();
90
- listeners.clear();
91
- },
92
- };
93
- }
94
-
95
- let activeStore: ReturnType<typeof createMaterialSettingsStore> | undefined;
96
- const materialSettingsStore = () => activeStore ??= createMaterialSettingsStore(window);
97
-
98
- export function useMaterialSettings(): MaterialSettingsSnapshot {
99
- const store = typeof window === "undefined" ? undefined : materialSettingsStore();
100
- return useSyncExternalStore(store?.subscribe ?? subscribeServer, store?.getSnapshot ?? getServerSnapshot, getServerSnapshot);
101
- }
102
-
103
- export const saveMaterialSettings = (settings: MaterialSettings): void => materialSettingsStore().save(settings);
104
- export const resetMaterialSettings = (): boolean => materialSettingsStore().reset();