@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
@@ -0,0 +1,115 @@
1
+ import { FolderOpenIcon } from "lucide-react";
2
+ import { useEffect, useState, type FormEvent, type ReactNode } from "react";
3
+ import { Button, Dialog, DialogContent, DialogHeader, DialogTitle, Input, Label, Toggle } from "../ui";
4
+ import type { PanelAction, PanelState, TargetKind, TargetView } from "./contract";
5
+ import { busyState } from "./target-state";
6
+ import { LoginForm } from "./LoginForm";
7
+
8
+ const PROFILE_FILE = /^ragents\.config\.([a-z0-9]+(?:-[a-z0-9]+)*)\.ts$/;
9
+
10
+ /** Eine Profildatei heißt ragents.config.<profil>.ts; ihr Profil ist der Name, den der Dialog vorschlägt. */
11
+ export const profileNameOf = (profileFile: string): string =>
12
+ PROFILE_FILE.exec(profileFile.split(/[/\\]/).pop() ?? "")?.[1] ?? "";
13
+
14
+ const fileNameOf = (profileFile: string): string => profileFile.split(/[/\\]/).pop() ?? profileFile;
15
+
16
+ function PanelDialog({ title, onClose, children }: { title: string; onClose: () => void; children: ReactNode }) {
17
+ return <Dialog onOpenChange={(next) => { if (!next) onClose(); }} open>
18
+ <DialogContent className="gap-3" scope="page" size="small">
19
+ <DialogHeader><DialogTitle className="text-[0.95rem] font-semibold">{title}</DialogTitle></DialogHeader>
20
+ {children}
21
+ </DialogContent>
22
+ </Dialog>;
23
+ }
24
+
25
+ /** Die Anmeldung einer Umgebung, aus der Übersicht wie aus den Einstellungen derselbe Dialog. */
26
+ export function LoginDialog({ target, onClose, send }: { target: TargetView; onClose: () => void; send: (action: PanelAction) => void }) {
27
+ const demanded = target.state.kind === "login-required" ? target.state.mode : "password";
28
+ const [mode, setMode] = useState<"password" | "token">(demanded);
29
+ useEffect(() => { if (target.state.kind === "connected") onClose(); }, [onClose, target.state.kind]);
30
+ return <PanelDialog onClose={onClose} title={`Anmelden an ${target.name}`}>
31
+ <div aria-label="Art der Anmeldung" className="flex gap-1.5" role="group">
32
+ <Toggle onPressedChange={() => setMode("password")} pressed={mode === "password"} size="sm" variant="outline">Benutzer</Toggle>
33
+ <Toggle onPressedChange={() => setMode("token")} pressed={mode === "token"} size="sm" variant="outline">Zugangstoken</Toggle>
34
+ </div>
35
+ <LoginForm busy={busyState(target)} key={mode} mode={mode} onCancel={onClose} send={send} submitLabel="Anmelden" target={target} />
36
+ </PanelDialog>;
37
+ }
38
+
39
+ /** Anlegen und Bearbeiten einer Umgebung; Bearbeiten schickt update und behält damit die gespeicherten Anmeldedaten. */
40
+ export function TargetDialog({ state, target, onClose, send }: {
41
+ state: PanelState;
42
+ target: TargetView | undefined;
43
+ onClose: () => void;
44
+ send: (action: PanelAction) => void;
45
+ }) {
46
+ const [kind, setKind] = useState<TargetKind>(target?.kind ?? "server");
47
+ const [name, setName] = useState(target?.name ?? "");
48
+ const [url, setUrl] = useState(target?.kind === "server" ? target.address : "");
49
+ const [profileFile, setProfileFile] = useState(target?.kind === "profile" ? target.address : "");
50
+ const [sent, setSent] = useState<{ name: string; at: PanelState }>();
51
+ const picked = state.pickedProfileFile;
52
+ const settled = sent !== undefined && sent.at !== state;
53
+ useEffect(() => {
54
+ if (!picked) return;
55
+ setKind("profile");
56
+ setProfileFile(picked);
57
+ setName((current) => current.trim() === "" ? profileNameOf(picked) : current);
58
+ }, [picked]);
59
+ useEffect(() => {
60
+ if (!settled || sent === undefined) return;
61
+ if (state.problem !== undefined) setSent(undefined);
62
+ else if (state.targets.some((entry) => entry.name === sent.name)) onClose();
63
+ }, [onClose, sent, settled, state]);
64
+ const takeProfile = (file: string) => {
65
+ setProfileFile(file);
66
+ setName((current) => current.trim() === "" ? profileNameOf(file) : current);
67
+ };
68
+ const value = kind === "server" ? url : profileFile;
69
+ const submit = (event: FormEvent) => {
70
+ event.preventDefault();
71
+ setSent({ name: name.trim(), at: state });
72
+ if (target === undefined) send(kind === "server" ? { action: "addServer", name, url } : { action: "addProfile", name, profileFile });
73
+ else send(kind === "server"
74
+ ? { action: "updateServer", name: target.name, newName: name, url }
75
+ : { action: "updateProfile", name: target.name, newName: name, profileFile });
76
+ };
77
+ return <PanelDialog onClose={onClose} title={target ? `${target.name} bearbeiten` : "Neue Umgebung"}>
78
+ <form className="grid gap-3" onSubmit={submit}>
79
+ <div aria-label="Art der Umgebung" className="flex gap-1.5" role="group">
80
+ <Toggle onPressedChange={() => setKind("server")} pressed={kind === "server"} size="sm" variant="outline">Server</Toggle>
81
+ <Toggle onPressedChange={() => setKind("profile")} pressed={kind === "profile"} size="sm" variant="outline">Lokales Profil</Toggle>
82
+ </div>
83
+ {kind === "server"
84
+ ? <div className="grid gap-1.5"><Label htmlFor="target-url">Adresse</Label><Input id="target-url" onChange={(event) => setUrl(event.target.value)} placeholder="https://werkstatt.example.com" value={url} /></div>
85
+ : <div className="grid gap-1.5">
86
+ <Label htmlFor="target-profile-file">Profildatei</Label>
87
+ <div className="flex items-center gap-2">
88
+ <Input aria-label="Profildatei" className="min-w-0 flex-1 font-mono text-[0.72rem]" id="target-profile-file" onChange={(event) => setProfileFile(event.target.value)} placeholder="~/repos/RAgents/ragents.config.core.ts" title={profileFile} value={profileFile} />
89
+ <Button className="flex-none" onClick={() => send({ action: "pickProfile" })} size="sm" type="button" variant="secondary"><FolderOpenIcon data-icon="inline-start" />Datei wählen ...</Button>
90
+ </div>
91
+ {state.profileSuggestions.length > 0 && <>
92
+ <p className="text-[0.7rem] leading-normal text-muted-foreground">Im Host-Ordner gefunden, ein Klick übernimmt sie:</p>
93
+ <ul className="grid grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-1.5">
94
+ {state.profileSuggestions.map((file) => <li key={file}>
95
+ <button aria-pressed={profileFile === file} className="flex w-full min-w-0 flex-col gap-0.5 rounded-md border border-border-soft px-2.5 py-1.5 text-left hover:border-border aria-pressed:border-primary aria-pressed:bg-accent" onClick={() => takeProfile(file)} type="button">
96
+ <strong className="truncate text-[0.75rem] font-semibold">{profileNameOf(file)}</strong>
97
+ <span className="truncate font-mono text-[0.65rem] text-muted-foreground" title={file}>{fileNameOf(file)}</span>
98
+ </button>
99
+ </li>)}
100
+ </ul>
101
+ </>}
102
+ </div>}
103
+ <div className="grid gap-1.5">
104
+ <Label htmlFor="target-name">Name</Label>
105
+ <Input id="target-name" onChange={(event) => setName(event.target.value)} placeholder={kind === "server" ? "Werkstatt" : "core"} value={name} />
106
+ {kind === "profile" && <span className="text-[0.68rem] text-muted-foreground">aus dem Dateinamen übernommen, überschreibbar</span>}
107
+ </div>
108
+ {state.problem && <p className="text-[0.75rem] leading-normal text-destructive [overflow-wrap:anywhere]" role="alert">{state.problem}</p>}
109
+ <div className="mt-1 flex flex-wrap justify-end gap-2">
110
+ <Button onClick={onClose} size="sm" type="button" variant="ghost">Abbrechen</Button>
111
+ <Button disabled={!name.trim() || !value.trim()} size="sm" type="submit">{target ? "Speichern" : "Anlegen"}</Button>
112
+ </div>
113
+ </form>
114
+ </PanelDialog>;
115
+ }
@@ -1,7 +1,7 @@
1
1
  import { OverviewPage, type PanelPageProps } from "./OverviewPage";
2
2
  import { SettingsPage } from "./SettingsPage";
3
3
 
4
- /** Das Panel der Erweiterung ohne geöffneten Run: die Übersicht aller Ziele, dahinter die Einstellungsseite. */
4
+ /** Das Panel der Erweiterung ohne geöffneten Run: die Übersicht aller Umgebungen, dahinter die Einstellungsseite. */
5
5
  export function PanelPage(props: PanelPageProps) {
6
6
  return <main className="min-h-dvh p-3">
7
7
  {props.state.page === "settings" ? <SettingsPage {...props} /> : <OverviewPage {...props} />}
@@ -1,102 +1,98 @@
1
- import { ArrowLeftIcon, FolderOpenIcon, PlusIcon, SettingsIcon, Trash2Icon } from "lucide-react";
2
- import { useEffect, useState, type FormEvent } from "react";
3
- import { Button, Card, Input, Label, Toggle } from "../ui";
1
+ import { ArrowLeftIcon, FolderIcon, PencilIcon, PlusIcon, ServerIcon, SettingsIcon, Trash2Icon } from "lucide-react";
2
+ import { useState } from "react";
3
+ import { cn } from "cn";
4
+ import { Button } from "../ui";
4
5
  import type { PanelAction, TargetView } from "./contract";
5
- import { LoginForm } from "./LoginForm";
6
6
  import type { PanelPageProps } from "./OverviewPage";
7
- import { stateLabel, stateTone } from "./target-state";
7
+ import { LoginDialog, TargetDialog } from "./PanelDialogs";
8
+ import { busyState, kindLabel, stateDetail, stateLabel, stateTone } from "./target-state";
8
9
 
9
- const sectionTitleClass = "text-[0.8rem] font-semibold";
10
+ const fileNameOf = (address: string): string => address.split(/[/\\]/).pop() ?? address;
10
11
 
11
- function Credentials({ target, busy, send }: { target: TargetView; busy: boolean; send: (action: PanelAction) => void }) {
12
- const demanded = target.state.kind === "login-required" ? target.state.mode : "password";
13
- const [mode, setMode] = useState<"password" | "token">(demanded);
14
- return <details className="rounded-md border border-border-soft px-2.5 py-2">
15
- <summary className="cursor-pointer text-[0.75rem] font-semibold text-muted-foreground">Anmeldedaten {target.savedLogin ? "(gespeichert)" : ""}</summary>
16
- <div className="mt-2.5 grid gap-2.5">
17
- <div className="flex gap-1.5">
18
- <Toggle onPressedChange={() => setMode("password")} pressed={mode === "password"} size="sm" variant="outline">Benutzer</Toggle>
19
- <Toggle onPressedChange={() => setMode("token")} pressed={mode === "token"} size="sm" variant="outline">Zugangstoken</Toggle>
20
- </div>
21
- <LoginForm busy={busy} mode={mode} send={send} submitLabel="Speichern und anmelden" target={target} />
22
- {target.savedLogin && <Button disabled={busy} onClick={() => send({ action: "logout", name: target.name })} size="sm" variant="ghost">Anmeldedaten löschen</Button>}
23
- </div>
24
- </details>;
12
+ /** Verbinden, Trennen, Starten und Stoppen sitzen hier; die Übersicht bietet nur den Weg aus einem Problem heraus. */
13
+ function RowActions({ target, busy, send, onLogin }: {
14
+ target: TargetView;
15
+ busy: boolean;
16
+ send: (action: PanelAction) => void;
17
+ onLogin: (name: string) => void;
18
+ }) {
19
+ const state = target.state.kind;
20
+ const profile = target.kind === "profile";
21
+ if (state === "connected") return <Button disabled={busy} onClick={() => send({ action: profile ? "stopProfile" : "disconnect", name: target.name })} size="xs" variant="ghost">{profile ? "Stoppen" : "Trennen"}</Button>;
22
+ if (state === "login-required" || state === "forbidden") return <Button disabled={busy} onClick={() => onLogin(target.name)} size="xs" variant="secondary">Anmelden</Button>;
23
+ if (state === "unreachable" || state === "failed") return <Button disabled={busy} onClick={() => send({ action: "retry", name: target.name })} size="xs" variant="secondary">Erneut versuchen</Button>;
24
+ if (state === "stopped") return <Button disabled={busy} onClick={() => send({ action: profile ? "startProfile" : "connect", name: target.name })} size="xs" variant="secondary">{profile ? "Starten" : "Verbinden"}</Button>;
25
+ return null;
25
26
  }
26
27
 
27
- function TargetRow({ target, send }: { target: TargetView; send: (action: PanelAction) => void }) {
28
+ function TargetRow({ target, send, onEdit, onLogin }: {
29
+ target: TargetView;
30
+ send: (action: PanelAction) => void;
31
+ onEdit: (name: string) => void;
32
+ onLogin: (name: string) => void;
33
+ }) {
28
34
  const [confirming, setConfirming] = useState(false);
29
- const busy = target.state.kind === "starting" || target.state.kind === "connecting";
30
- return <Card className="gap-2.5 p-3.5">
31
- <div className="flex min-w-0 items-start gap-2">
32
- <span className="min-w-0 flex-1">
33
- <strong className="block truncate text-[0.9rem] font-semibold">{target.name}</strong>
34
- <span className="block truncate text-[0.72rem] text-muted-foreground" title={target.description}>{target.description}</span>
35
- <span className={`block text-[0.7rem] ${stateTone(target)}`}>{stateLabel(target)}</span>
35
+ const busy = busyState(target);
36
+ const detail = stateDetail(target) ?? target.problem;
37
+ const profile = target.kind === "profile";
38
+ return <li className="grid grid-cols-[auto_minmax(0,1fr)] items-center gap-x-2.5 gap-y-1.5 border-b border-border-soft px-1 py-2.5 last:border-b-0">
39
+ <span aria-hidden className="grid size-[26px] flex-none place-items-center rounded-md bg-secondary text-muted-foreground">
40
+ {profile ? <FolderIcon className="size-3.5" /> : <ServerIcon className="size-3.5" />}
41
+ </span>
42
+ <span className="min-w-0">
43
+ <span className="flex min-w-0 items-baseline gap-2">
44
+ <strong className="min-w-0 truncate text-[0.8rem] font-semibold">{target.name}</strong>
45
+ <span className="flex-none text-[0.6rem] uppercase tracking-[0.05em] text-muted-foreground">{kindLabel(target)}</span>
36
46
  </span>
47
+ <span className="block truncate font-mono text-[0.68rem] text-muted-foreground" title={target.address}>{profile ? fileNameOf(target.address) : target.address}</span>
48
+ </span>
49
+ <span className={cn("col-span-2 flex items-center gap-1.5 text-[0.7rem]", stateTone(target))}>
50
+ <span aria-hidden className="size-[7px] flex-none rounded-full bg-current" />{stateLabel(target)}
51
+ </span>
52
+ {detail && <p className="col-span-2 text-[0.7rem] leading-normal text-destructive [overflow-wrap:anywhere]" role="alert">{detail}</p>}
53
+ <span className="col-span-2 flex flex-wrap items-center gap-1.5">
54
+ <RowActions busy={busy} onLogin={onLogin} send={send} target={target} />
55
+ {target.savedLogin && <Button disabled={busy} onClick={() => send({ action: "logout", name: target.name })} size="xs" variant="ghost">Abmelden</Button>}
56
+ <Button disabled={busy} onClick={() => onEdit(target.name)} size="xs" variant="ghost"><PencilIcon data-icon="inline-start" />Bearbeiten</Button>
37
57
  {confirming
38
- ? <span className="flex flex-none items-center gap-1">
58
+ ? <span className="flex items-center gap-1.5">
59
+ <span className="text-[0.7rem] text-muted-foreground">Wirklich entfernen?</span>
39
60
  <Button disabled={busy} onClick={() => send({ action: "remove", name: target.name })} size="xs" variant="destructive">Entfernen</Button>
40
61
  <Button onClick={() => setConfirming(false)} size="xs" variant="ghost">Nein</Button>
41
62
  </span>
42
- : <Button aria-label={`${target.name} entfernen`} disabled={busy} onClick={() => setConfirming(true)} size="icon-sm" variant="ghost"><Trash2Icon /></Button>}
43
- </div>
44
- {target.problem && <p className="text-[0.75rem] leading-normal text-destructive [overflow-wrap:anywhere]" role="alert">{target.problem}</p>}
45
- {target.kind === "server" && <Credentials busy={busy} send={send} target={target} />}
46
- </Card>;
47
- }
48
-
49
- function AddServer({ send }: { send: (action: PanelAction) => void }) {
50
- const [name, setName] = useState("");
51
- const [url, setUrl] = useState("");
52
- const submit = (event: FormEvent) => {
53
- event.preventDefault();
54
- send({ action: "addServer", name, url });
55
- setName("");
56
- setUrl("");
57
- };
58
- return <form className="grid gap-2.5" onSubmit={submit}>
59
- <h2 className={sectionTitleClass}>Server hinzufügen</h2>
60
- <div className="grid gap-1.5"><Label htmlFor="server-name">Name</Label><Input id="server-name" onChange={(event) => setName(event.target.value)} placeholder="Werkstatt" value={name} /></div>
61
- <div className="grid gap-1.5"><Label htmlFor="server-url">Adresse</Label><Input id="server-url" onChange={(event) => setUrl(event.target.value)} placeholder="https://werkstatt.example.com" value={url} /></div>
62
- <Button disabled={!name.trim() || !url.trim()} size="sm" type="submit"><PlusIcon data-icon="inline-start" />Server anlegen</Button>
63
- </form>;
63
+ : <Button aria-label={`${target.name} entfernen`} className="ml-auto" disabled={busy} onClick={() => setConfirming(true)} size="icon-sm" variant="ghost"><Trash2Icon /></Button>}
64
+ </span>
65
+ </li>;
64
66
  }
65
67
 
66
- function AddProfile({ picked, send }: { picked: string | undefined; send: (action: PanelAction) => void }) {
67
- const [name, setName] = useState("");
68
- const [profileFile, setProfileFile] = useState("");
69
- useEffect(() => { if (picked) setProfileFile(picked); }, [picked]);
70
- const submit = (event: FormEvent) => {
71
- event.preventDefault();
72
- send({ action: "addProfile", name, profileFile });
73
- setName("");
74
- setProfileFile("");
75
- };
76
- return <form className="grid gap-2.5" onSubmit={submit}>
77
- <h2 className={sectionTitleClass}>Lokales Profil hinzufügen</h2>
78
- <div className="grid gap-1.5"><Label htmlFor="profile-name">Name</Label><Input id="profile-name" onChange={(event) => setName(event.target.value)} placeholder="core" value={name} /></div>
79
- <div className="grid gap-1.5"><Label htmlFor="profile-file">Profildatei</Label><Input id="profile-file" onChange={(event) => setProfileFile(event.target.value)} placeholder="~/repos/RAgents/ragents.config.core.ts" value={profileFile} /></div>
80
- <div className="flex flex-wrap gap-2">
81
- <Button onClick={() => send({ action: "pickProfile" })} size="sm" type="button" variant="ghost"><FolderOpenIcon data-icon="inline-start" />Datei wählen ...</Button>
82
- <Button className="flex-1" disabled={!name.trim() || !profileFile.trim()} size="sm" type="submit"><PlusIcon data-icon="inline-start" />Profil anlegen</Button>
83
- </div>
84
- </form>;
85
- }
86
-
87
- /** Eine Seite für alles, was eingerichtet wird: Ziele anlegen und entfernen, Anmeldedaten je Server. */
68
+ /** Alles, was eingerichtet wird: die Umgebungen als Zeilenliste, dahinter die Dialoge zum Anlegen, Bearbeiten und Anmelden. */
88
69
  export function SettingsPage({ state, send }: PanelPageProps) {
70
+ const [dialog, setDialog] = useState<{ kind: "new" } | { kind: "edit"; name: string }>();
71
+ const [login, setLogin] = useState<string>();
72
+ const editing = dialog?.kind === "edit" ? state.targets.find((target) => target.name === dialog.name) : undefined;
73
+ const loginTarget = state.targets.find((target) => target.name === login);
74
+ const open = dialog?.kind === "new" || editing !== undefined;
89
75
  return <div className="grid gap-3">
90
76
  <div className="flex items-center gap-2">
91
77
  <Button aria-label="Zur Übersicht" onClick={() => send({ action: "page", page: "overview" })} size="icon-sm" title="Zur Übersicht" variant="ghost"><ArrowLeftIcon /></Button>
92
- <h1 className="min-w-0 flex-1 text-[1.15rem] font-semibold">Einstellungen</h1>
78
+ <h1 className="min-w-0 flex-1 text-[1.1rem] font-semibold">Einstellungen</h1>
93
79
  <Button aria-label="Einstellung öffnen" onClick={() => send({ action: "settingsFile" })} size="icon-sm" title="Einstellung öffnen (settings.json)" variant="ghost"><SettingsIcon /></Button>
94
80
  </div>
95
81
  {state.problem && <p className="text-[0.8rem] leading-normal text-destructive [overflow-wrap:anywhere]" role="alert">{state.problem}</p>}
96
- {state.targets.length === 0
97
- ? <p className="text-[0.85rem] leading-normal text-muted-foreground">Noch kein Ziel.</p>
98
- : state.targets.map((target) => <TargetRow key={target.name} send={send} target={target} />)}
99
- <Card className="gap-4 p-4"><AddServer send={send} /></Card>
100
- <Card className="gap-4 p-4"><AddProfile picked={state.pickedProfileFile} send={send} /></Card>
82
+ <section className="grid gap-1.5">
83
+ <h2 className="flex items-baseline gap-2 text-[0.66rem] font-bold uppercase tracking-[0.06em] text-muted-foreground">
84
+ Umgebungen<span className="font-mono text-[0.62rem] font-normal tracking-normal opacity-80">{state.targets.length}</span>
85
+ </h2>
86
+ {state.targets.length === 0
87
+ ? <p className="text-[0.85rem] leading-normal text-muted-foreground">Noch keine Umgebung.</p>
88
+ : <ul className="grid">
89
+ {state.targets.map((target) => <TargetRow key={target.name} onEdit={(name) => setDialog({ kind: "edit", name })} onLogin={setLogin} send={send} target={target} />)}
90
+ </ul>}
91
+ </section>
92
+ <div className="flex flex-wrap items-center gap-2">
93
+ <Button onClick={() => setDialog({ kind: "new" })} size="sm"><PlusIcon data-icon="inline-start" />Neue Umgebung</Button>
94
+ </div>
95
+ {open && <TargetDialog key={editing?.name ?? "neu"} onClose={() => setDialog(undefined)} send={send} state={state} target={editing} />}
96
+ {loginTarget && <LoginDialog onClose={() => setLogin(undefined)} send={send} target={loginTarget} />}
101
97
  </div>;
102
98
  }
@@ -1,6 +1,6 @@
1
1
  import type { ColumnTheme } from "../column/host-contract";
2
2
 
3
- /** Ein Ziel der Erweiterung: ein RAgents-Server oder ein lokales Profil, das sie selbst startet. */
3
+ /** Eine Umgebung der Erweiterung: ein RAgents-Server oder ein lokales Profil, das sie selbst startet. */
4
4
  export type TargetKind = "server" | "profile";
5
5
 
6
6
  export type TargetState =
@@ -25,14 +25,16 @@ export interface TargetEntry {
25
25
  readonly id: string;
26
26
  readonly title: string;
27
27
  readonly description: string;
28
- readonly action: "skill" | "script";
28
+ readonly kind: "skill" | "script";
29
+ /** Die Gruppe der Kachelansicht; ein Run-Script ohne eigene Kategorie steht unter "Run-Scripts". */
30
+ readonly category: string;
29
31
  }
30
32
 
31
33
  export interface TargetView {
32
34
  readonly name: string;
33
35
  readonly kind: TargetKind;
34
- /** Adresse des Servers oder Profil samt Pfad. */
35
- readonly description: string;
36
+ /** Adresse des Servers oder Pfad der Profildatei. */
37
+ readonly address: string;
36
38
  readonly state: TargetState;
37
39
  readonly runs: readonly TargetRun[];
38
40
  readonly entries: readonly TargetEntry[];
@@ -42,9 +44,9 @@ export interface TargetView {
42
44
  readonly user?: string;
43
45
  /** Die zuletzt versuchte Kennung; das Anmeldeformular übernimmt sie. */
44
46
  readonly loginUser?: string;
45
- /** Anmeldedaten liegen in der SecretStorage, damit sich das Ziel still anmeldet. */
47
+ /** Anmeldedaten liegen in der SecretStorage, damit die Umgebung sich still anmeldet. */
46
48
  readonly savedLogin?: boolean;
47
- /** Fehler der letzten Aktion an diesem Ziel, direkt in seiner Karte. */
49
+ /** Fehler der letzten Aktion an dieser Umgebung, direkt in ihrer Zeile. */
48
50
  readonly problem?: string;
49
51
  }
50
52
 
@@ -52,9 +54,11 @@ export interface PanelState {
52
54
  readonly theme: ColumnTheme;
53
55
  readonly page: "overview" | "settings";
54
56
  readonly targets: readonly TargetView[];
57
+ /** Die Profildateien im Host-Ordner; der Dialog bietet sie an, ohne Host bleibt die Liste leer. */
58
+ readonly profileSuggestions: readonly string[];
55
59
  /** Die Einstellung ragents.connections ist fehlerhaft; die Seite nennt den Grund. */
56
60
  readonly problem?: string;
57
- /** Der zuletzt über den Dateidialog gewählte Profilpfad; das Formular übernimmt ihn. */
61
+ /** Der zuletzt über den Dateidialog gewählte Profilpfad; der Dialog übernimmt ihn. */
58
62
  readonly pickedProfileFile?: string;
59
63
  }
60
64
 
@@ -71,7 +75,9 @@ export type PanelAction =
71
75
  | { readonly action: "pickProfile" }
72
76
  | { readonly action: "addServer"; readonly name: string; readonly url: string }
73
77
  | { readonly action: "addProfile"; readonly name: string; readonly profileFile: string }
74
- | { readonly action: "remove" | "connect" | "disconnect" | "logout"; readonly name: string }
78
+ | { readonly action: "updateServer"; readonly name: string; readonly newName: string; readonly url: string }
79
+ | { readonly action: "updateProfile"; readonly name: string; readonly newName: string; readonly profileFile: string }
80
+ | { readonly action: "remove" | "connect" | "disconnect" | "logout" | "startProfile" | "stopProfile" | "retry"; readonly name: string }
75
81
  | { readonly action: "login"; readonly name: string; readonly user: string; readonly password: string }
76
82
  | { readonly action: "login"; readonly name: string; readonly token: string }
77
83
  | { readonly action: "openRun"; readonly name: string; readonly runId: string }
@@ -88,7 +94,9 @@ export const isPanelActionMessage = (value: unknown): value is PanelActionMessag
88
94
  case "settingsFile": case "pickProfile": return true;
89
95
  case "addServer": return text(value, "name") && text(value, "url");
90
96
  case "addProfile": return text(value, "name") && text(value, "profileFile");
91
- case "remove": case "connect": case "disconnect": case "logout": return text(value, "name");
97
+ case "updateServer": return text(value, "name") && text(value, "newName") && text(value, "url");
98
+ case "updateProfile": return text(value, "name") && text(value, "newName") && text(value, "profileFile");
99
+ case "remove": case "connect": case "disconnect": case "logout": case "startProfile": case "stopProfile": case "retry": return text(value, "name");
92
100
  case "login": return text(value, "name") && (text(value, "token") || (text(value, "user") && text(value, "password")));
93
101
  case "openRun": return text(value, "name") && text(value, "runId");
94
102
  case "newRun": return text(value, "name") && ((value as { entryId?: unknown }).entryId === undefined || text(value, "entryId"));
@@ -1,6 +1,6 @@
1
1
  import type { TargetRun, TargetView } from "./contract";
2
2
 
3
- /** Wie der Zustand eines Ziels in seiner Karte steht; ein lokales Profil ist nicht getrennt, sondern nicht gestartet. */
3
+ /** Wie der Zustand einer Umgebung in ihrer Zeile steht; ein lokales Profil ist nicht getrennt, sondern nicht gestartet. */
4
4
  export const stateLabel = (target: TargetView): string => {
5
5
  switch (target.state.kind) {
6
6
  case "stopped": return target.kind === "profile" ? "nicht gestartet" : "getrennt";
@@ -27,13 +27,17 @@ export const stateTone = (target: TargetView): string => {
27
27
  }
28
28
  };
29
29
 
30
- /** Der Grund, der zum Zustand gehört; er steht unter der Kopfzeile der Karte. */
30
+ /** Der Grund, der zum Zustand gehört; er steht unter der Zeile der Umgebung. */
31
31
  export const stateDetail = (target: TargetView): string | undefined => {
32
32
  const state = target.state;
33
33
  if (state.kind === "unreachable" || state.kind === "forbidden" || state.kind === "failed") return state.message;
34
34
  return undefined;
35
35
  };
36
36
 
37
+ export const kindLabel = (target: TargetView): string => target.kind === "profile" ? "lokales Profil" : "Server";
38
+
39
+ export const busyState = (target: TargetView): boolean => target.state.kind === "starting" || target.state.kind === "connecting";
40
+
37
41
  export const runStateLabel = (run: TargetRun): string => {
38
42
  switch (run.state) {
39
43
  case "running": return "läuft";
@@ -51,3 +55,24 @@ export const runTone = (run: TargetRun): string => {
51
55
  case "idle": return "text-muted-foreground";
52
56
  }
53
57
  };
58
+
59
+ const MINUTE = 60_000;
60
+ const HOUR = 60 * MINUTE;
61
+ const DAY = 24 * HOUR;
62
+
63
+ /** Wann ein Run zuletzt etwas getan hat; alles ab gestern nennt das Datum statt einer Spanne. */
64
+ export const runTime = (run: TargetRun, now: number = Date.now()): string => {
65
+ const passed = now - run.updatedAt;
66
+ if (passed < MINUTE) return "gerade eben";
67
+ if (passed < HOUR) return `vor ${Math.floor(passed / MINUTE)} Min`;
68
+ if (passed < DAY) return `vor ${Math.floor(passed / HOUR)} Std`;
69
+ return new Date(run.updatedAt).toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric" });
70
+ };
71
+
72
+ const HUES = [255, 170, 300, 10, 130, 210, 60, 330];
73
+
74
+ /** Die Farbe einer Umgebung hängt an ihrem Namen, nicht an ihrer Stelle in der Liste; Umsortieren ändert sie nicht. */
75
+ export const environmentHue = (name: string): number => {
76
+ const hash = [...name].reduce((value, character) => (value * 31 + character.codePointAt(0)!) % 100_003, 7);
77
+ return HUES[hash % HUES.length]!;
78
+ };
@@ -134,6 +134,8 @@
134
134
  --warning-soft: #f3e4d8;
135
135
  --info: #4b648a;
136
136
  --teal: #356c70;
137
+ --env-lightness: 0.55;
138
+ --env-chroma: 0.12;
137
139
  --border: #c9c4d5;
138
140
  --border-soft: #dad6e3;
139
141
  --border-strong: #a397b2;
@@ -186,6 +188,8 @@
186
188
  --warning-soft: #43382b;
187
189
  --info: #9dbade;
188
190
  --teal: #8ec9c4;
191
+ --env-lightness: 0.79;
192
+ --env-chroma: 0.1;
189
193
  --border: #555065;
190
194
  --border-soft: #403c50;
191
195
  --border-strong: #8e809f;
@@ -0,0 +1,41 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "isolatedModules": true,
8
+ "skipLibCheck": true,
9
+ "jsx": "react-jsx",
10
+ "lib": [
11
+ "ES2024",
12
+ "DOM",
13
+ "DOM.Iterable"
14
+ ],
15
+ "noEmit": true,
16
+ "allowImportingTsExtensions": true,
17
+ "paths": {
18
+ "@/*": [
19
+ "./src/*"
20
+ ],
21
+ "@aicontainer/server/*": [
22
+ "../../apps/server/src/*"
23
+ ],
24
+ "@aicontainer/web/*": [
25
+ "../../apps/web/src/*"
26
+ ],
27
+ "@aicontainer/plugins/*": [
28
+ "../../plugins/*"
29
+ ],
30
+ "@ragents/workflow": [
31
+ "../../plugins/ragents.actor-programs/workflow/index.ts"
32
+ ]
33
+ }
34
+ },
35
+ "include": [
36
+ "src",
37
+ "../../plugins/*/web",
38
+ "../../plugins/ragents.actor-programs/client-ui/index.tsx",
39
+ "../../plugins/*/contract.ts"
40
+ ]
41
+ }
@@ -0,0 +1,140 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { execFileSync } from "node:child_process";
3
+ import { readdir, readFile } from "node:fs/promises";
4
+ import { readdirSync, readFileSync, statSync } from "node:fs";
5
+ import path from "node:path";
6
+ import react from "@vitejs/plugin-react";
7
+ import tailwindcss from "@tailwindcss/vite";
8
+ import { defineConfig, type Plugin } from "vite";
9
+
10
+ const apiTarget = process.env.API_TARGET ?? "http://localhost:4710";
11
+ const repositoryRoot = fileURLToPath(new URL("../../", import.meta.url));
12
+ const hostEntry = path.join(repositoryRoot, "apps/web/src/main.tsx");
13
+ const pluginEntriesModule = "virtual:ragents-plugins";
14
+
15
+ interface BundledPlugin {
16
+ readonly id: string;
17
+ readonly folder: string;
18
+ readonly web: string | undefined;
19
+ }
20
+
21
+ const webEntryOf = (folder: string): string | undefined =>
22
+ ["web/index.ts", "web/index.tsx"].map((name) => path.join(folder, name))
23
+ .find((candidate) => statSync(candidate, { throwIfNoEntry: false })?.isFile() === true);
24
+
25
+ const repositoryPlugins = (): BundledPlugin[] => {
26
+ const root = path.join(repositoryRoot, "plugins");
27
+ return readdirSync(root, { withFileTypes: true })
28
+ .filter((entry) => entry.isDirectory())
29
+ .map((entry) => ({ id: entry.name, folder: path.join(root, entry.name), web: webEntryOf(path.join(root, entry.name)) }));
30
+ };
31
+
32
+ /** The profile's plugins as the server resolves them; PLUGIN_LIST_FILE names them directly, otherwise the last stdout line carries the JSON. */
33
+ const profilePlugins = (): BundledPlugin[] => {
34
+ const listFile = process.env.PLUGIN_LIST_FILE;
35
+ if (listFile) return JSON.parse(readFileSync(listFile, "utf8")) as BundledPlugin[];
36
+ if (!process.env.PRODUCT_PROFILE) return [];
37
+ const output = execFileSync("node", ["--import", "tsx", "src/profile/plugin-list.ts"], {
38
+ cwd: path.join(repositoryRoot, "apps/server"),
39
+ encoding: "utf8",
40
+ stdio: ["ignore", "pipe", "inherit"],
41
+ });
42
+ return JSON.parse(output.trim().split("\n").at(-1) ?? "[]") as BundledPlugin[];
43
+ };
44
+
45
+ /** Bundles the repository's plugins and, with a profile, its plugins from anywhere on disk. */
46
+ const bundledPlugins = (): BundledPlugin[] => {
47
+ const plugins = repositoryPlugins();
48
+ for (const plugin of profilePlugins()) {
49
+ const known = plugins.find((candidate) => candidate.id === plugin.id);
50
+ if (known && known.folder !== plugin.folder) {
51
+ throw new Error(`Das Plugin ${plugin.id} liegt unter ${known.folder} und ${plugin.folder}`);
52
+ }
53
+ if (!known) plugins.push(plugin);
54
+ }
55
+ return plugins;
56
+ };
57
+
58
+ const isBareSpecifier = (specifier: string): boolean =>
59
+ !/^(\.{1,2}\/|\/|\0|[a-z][a-z0-9+.-]*:)/i.test(specifier);
60
+
61
+ const ragentsPlugins = (plugins: readonly BundledPlugin[]): Plugin => {
62
+ const external = plugins.filter((plugin) => !plugin.folder.startsWith(repositoryRoot));
63
+ const withWeb = plugins.filter((plugin) => plugin.web !== undefined);
64
+ return {
65
+ name: "ragents-plugins",
66
+ enforce: "pre",
67
+ config: () => ({
68
+ resolve: {
69
+ alias: {
70
+ "@aicontainer/ragents/src": path.join(repositoryRoot, "packages/ragents/src"),
71
+ "@aicontainer/server": path.join(repositoryRoot, "apps/server/src"),
72
+ "@aicontainer/web": path.join(repositoryRoot, "apps/web/src"),
73
+ "@aicontainer/plugins": path.join(repositoryRoot, "plugins"),
74
+ },
75
+ },
76
+ server: { fs: { allow: [repositoryRoot, ...external.map((plugin) => plugin.folder)] } },
77
+ }),
78
+ resolveId(source, importer) {
79
+ if (source === pluginEntriesModule) return `\0${pluginEntriesModule}`;
80
+ if (!importer || importer.startsWith(repositoryRoot) || !isBareSpecifier(source)) return null;
81
+ return this.resolve(source, hostEntry, { skipSelf: true });
82
+ },
83
+ load(id) {
84
+ if (id !== `\0${pluginEntriesModule}`) return null;
85
+ const entries = withWeb.map((plugin) => ` ${JSON.stringify(plugin.id)}: () => import(${JSON.stringify(plugin.web)}),`);
86
+ return `export const webPluginEntries = {\n${entries.join("\n")}\n};\n`;
87
+ },
88
+ transform(code, id) {
89
+ if (!id.endsWith("/apps/web/src/ui/tailwind.css") || external.length === 0) return null;
90
+ const sources = external.flatMap((plugin) => [
91
+ `@source ${JSON.stringify(path.join(plugin.folder, "web/**/*"))};`,
92
+ `@source ${JSON.stringify(path.join(plugin.folder, "client-ui/**/*"))};`,
93
+ ]);
94
+ return `${code}\n${sources.join("\n")}\n`;
95
+ },
96
+ };
97
+ };
98
+
99
+ export default defineConfig({
100
+ plugins: [ragentsPlugins(bundledPlugins()), react(), tailwindcss(), {
101
+ name: "ragents-help",
102
+ apply: "build",
103
+ buildStart() {
104
+ const script = path.join(repositoryRoot, "build/homepage.sh");
105
+ if (statSync(script, { throwIfNoEntry: false })?.isFile()) execFileSync("bash", [script], { cwd: repositoryRoot, stdio: "inherit" });
106
+ },
107
+ async generateBundle() {
108
+ const directory = path.join(repositoryRoot, "docs/homepage/dist");
109
+ if (!statSync(directory, { throwIfNoEntry: false })?.isDirectory()) return;
110
+ const include = async (relative: string): Promise<void> => {
111
+ for (const entry of await readdir(path.join(directory, relative), { withFileTypes: true })) {
112
+ const name = path.posix.join(relative, entry.name);
113
+ if (entry.isDirectory()) await include(name);
114
+ else this.emitFile({ type: "asset", fileName: `help/${name}`, source: await readFile(path.join(directory, name)) });
115
+ }
116
+ };
117
+ await include("");
118
+ },
119
+ }],
120
+ build: {
121
+ outDir: path.join(repositoryRoot, "apps/web/dist", process.env.PRODUCT_PROFILE ?? "ohne-profil"),
122
+ rollupOptions: {
123
+ input: {
124
+ main: fileURLToPath(new URL("index.html", import.meta.url)),
125
+ column: fileURLToPath(new URL("column.html", import.meta.url)),
126
+ },
127
+ },
128
+ },
129
+ server: {
130
+ port: 5710,
131
+ strictPort: true,
132
+ proxy: {
133
+ "/api": apiTarget,
134
+ "/chat": apiTarget,
135
+ "/plugin-assets": apiTarget,
136
+ "/ragents": apiTarget,
137
+ "/help": apiTarget,
138
+ },
139
+ },
140
+ });