@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.
- package/apps/server/src/plugin-support/language-server/contract.ts +6 -1
- package/apps/server/src/plugin-support/language-server/tools.ts +29 -4
- package/apps/server/src/plugin-support/run-scripts.ts +4 -1
- package/apps/server/src/plugin-support/start-entries-contract.ts +4 -3
- package/apps/server/src/plugin-support/workspace-sandbox-host.ts +2 -0
- package/apps/web/src/App.tsx +0 -10
- package/apps/web/src/PluginChat.tsx +8 -8
- package/apps/web/src/PluginRegistry.tsx +0 -12
- package/apps/web/src/StartSurface.tsx +2 -2
- package/apps/web/src/ThemeSettings.tsx +1 -1
- package/apps/web/src/column/ColumnApp.tsx +83 -7
- package/apps/web/src/column/clipboard.ts +64 -0
- package/apps/web/src/column/host-contract.ts +10 -0
- package/apps/web/src/column.tsx +2 -0
- package/apps/web/src/panel/LoginForm.tsx +9 -3
- package/apps/web/src/panel/OverviewPage.tsx +187 -62
- package/apps/web/src/panel/PanelDialogs.tsx +115 -0
- package/apps/web/src/panel/PanelPage.tsx +1 -1
- package/apps/web/src/panel/SettingsPage.tsx +75 -79
- package/apps/web/src/panel/contract.ts +17 -9
- package/apps/web/src/panel/target-state.ts +27 -2
- package/apps/web/src/ui/theme.css +4 -0
- package/package.json +2 -2
- package/packages/ragents/src/plugin-host.ts +5 -2
- package/packages/ragents/src/plugin-types.ts +3 -2
- package/packages/workspace-executor/src/context.ts +4 -0
- package/packages/workspace-executor/src/executor.ts +9 -2
- package/packages/workspace-executor/src/index.ts +2 -0
- package/packages/workspace-executor/src/language-server/adapters/fsharp.ts +1 -0
- package/packages/workspace-executor/src/language-server/adapters/roslyn.ts +1 -0
- package/packages/workspace-executor/src/language-server/adapters/typescript.ts +34 -6
- package/packages/workspace-executor/src/language-server/contract.ts +7 -3
- package/packages/workspace-executor/src/language-server/host.ts +275 -138
- package/plugins/ragents.actor-programs/contract.ts +1 -1
- package/plugins/ragents.actor-programs/prompt.hbs +4 -4
- package/plugins/ragents.actor-programs/server/app-project.ts +0 -2
- package/plugins/ragents.actor-programs/server/runtime.ts +1 -1
- package/plugins/ragents.actor-programs/web/AppsPanel.tsx +3 -5
- package/plugins/ragents.actor-programs/web/EmbeddedApps.tsx +7 -39
- package/plugins/ragents.actor-programs/web/api.ts +1 -10
- package/plugins/ragents.lsp-roslyn/web/language-server-plugin.tsx +73 -44
- package/plugins/ragents.orchestration/canvas.hbs +28 -44
- package/plugins/ragents.orchestration/contract.ts +17 -287
- package/plugins/ragents.orchestration/server/canvas-tool.ts +18 -123
- package/plugins/ragents.orchestration/tiled-layout.ts +12 -4
- package/plugins/ragents.orchestration/web/ActorCanvasControls.tsx +23 -47
- package/plugins/ragents.orchestration/web/ActorChatPreview.tsx +3 -9
- package/plugins/ragents.orchestration/web/ActorShortcut.tsx +3 -5
- package/plugins/ragents.orchestration/web/ActorTile.tsx +3 -2
- package/plugins/ragents.orchestration/web/CanvasTiles.tsx +3 -3
- package/plugins/ragents.orchestration/web/Center.tsx +28 -463
- package/plugins/ragents.orchestration/web/MaterialBody.tsx +6 -87
- package/plugins/ragents.orchestration/web/TiledCanvas.tsx +2 -3
- package/plugins/ragents.orchestration/web/canvas-view-settings.ts +7 -13
- package/plugins/ragents.orchestration/web/column/RunColumn.tsx +44 -19
- package/plugins/ragents.orchestration/web/column/column-state.ts +13 -4
- package/plugins/ragents.orchestration/web/constants.ts +0 -2
- package/plugins/ragents.orchestration/web/index.tsx +0 -6
- package/plugins/ragents.orchestration/web/tile-docking.ts +7 -1
- package/plugins/ragents.orchestration/web/tiled-view-settings.ts +7 -9
- package/plugins/ragents.overseer/server/coordinator-prompt.ts +1 -1
- package/plugins/ragents.reference/examples.ts +1 -2
- package/plugins/ragents.reference/run-scripts/balcony-wizard/RUN.md +1 -1
- package/plugins/ragents.reference/run-scripts/balcony-wizard/actors/balcony-app/package.json +1 -1
- package/plugins/ragents.reference/run-scripts/balcony-wizard/src/server.ts +1 -1
- package/plugins/ragents.reference/run-scripts/balcony-wizard/tests/program.test.ts +2 -2
- package/plugins/ragents.reference/run-scripts/conversation-circle/RUN.md +3 -4
- package/plugins/ragents.reference/run-scripts/conversation-circle/src/server.ts +9 -15
- package/plugins/ragents.reference/run-scripts/conversation-circle/tests/program.test.ts +5 -3
- package/plugins/ragents.reference/run-scripts/learning-afternoon/package.json +1 -1
- package/plugins/ragents.reference/run-scripts/learning-afternoon/src/server.ts +14 -4
- package/plugins/ragents.reference/run-scripts/word-game/src/server.ts +1 -1
- package/plugins/ragents.reference/skills/210-moderator-handover/SKILL.md +1 -1
- package/plugins/ragents.reference/walkthroughs.ts +14 -14
- package/plugins/ragents.workspace/client/workspace-client.ts +4 -1
- package/scripts/package/publish-package.ts +4 -5
- package/scripts/workspace-client/run-workspace-client.ts +3 -1
- package/apps/web/src/MaterialSettings.tsx +0 -40
- package/apps/web/src/material-settings.ts +0 -104
- package/plugins/ragents.orchestration/web/ActorNet.tsx +0 -607
- package/plugins/ragents.orchestration/web/CardSizeSettings.tsx +0 -42
- package/plugins/ragents.orchestration/web/ScriptActorCard.tsx +0 -56
- package/plugins/ragents.orchestration/web/ZoomSettings.tsx +0 -38
- package/plugins/ragents.orchestration/web/canvas-layout-storage.ts +0 -39
- package/plugins/ragents.orchestration/web/canvas-layout.ts +0 -326
- package/plugins/ragents.orchestration/web/canvas-scene.ts +0 -311
- package/plugins/ragents.orchestration/web/card-size-settings.ts +0 -36
- package/plugins/ragents.orchestration/web/keyboard-navigation.ts +0 -55
- package/plugins/ragents.orchestration/web/material-layout.ts +0 -14
- package/plugins/ragents.orchestration/web/useCanvasMeasurements.ts +0 -78
- package/plugins/ragents.orchestration/web/zoom-settings.ts +0 -28
- package/plugins/ragents.reference/skills/120-canvas-conversation-circle/SKILL.md +0 -12
- package/plugins/ragents.reference/skills/130-canvas-pipeline-with-approval/SKILL.md +0 -12
- package/plugins/ragents.reference/skills/140-canvas-team-tree-and-topic-grid/SKILL.md +0 -12
|
@@ -3,7 +3,12 @@ import { defineOperation } from "@aicontainer/ragents/src/rpc/contract";
|
|
|
3
3
|
import { openJson } from "@aicontainer/ragents/src/http/contracts";
|
|
4
4
|
import type { LanguageServerSnapshot } from "@aicontainer/workspace-executor";
|
|
5
5
|
|
|
6
|
-
export type {
|
|
6
|
+
export type {
|
|
7
|
+
LanguageServerInstanceSnapshot,
|
|
8
|
+
LanguageServerSeverity,
|
|
9
|
+
LanguageServerSnapshot,
|
|
10
|
+
LanguageServerState,
|
|
11
|
+
} from "@aicontainer/workspace-executor";
|
|
7
12
|
|
|
8
13
|
/** Je Sprachserver-Plugin eine Methode; die Rechte tragen seine Kennung. */
|
|
9
14
|
export const languageServerSnapshotContract = (pluginId: string) =>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { defineRunFunction, type ToolContributor } from "@aicontainer/ragents";
|
|
3
3
|
import {
|
|
4
|
+
languageServerCloseTool,
|
|
4
5
|
languageServerDiagnosticsTool,
|
|
5
6
|
languageServerOpenTool,
|
|
6
7
|
type LanguageServerAdapter,
|
|
@@ -19,15 +20,22 @@ export const createLanguageServerToolContributor = (
|
|
|
19
20
|
const openMetadata = {
|
|
20
21
|
name: languageServerOpenTool(id),
|
|
21
22
|
nativeTool: true,
|
|
22
|
-
description: `Start
|
|
23
|
+
description: `Start a ${label} language server instance for this conversation's workspace and load ${rootDescription}. `
|
|
23
24
|
+ `Afterwards every edit or write of a ${extensions} file gets its diagnostics appended automatically, `
|
|
24
|
-
+ `and ${languageServerDiagnosticsTool(id)} is available. Idempotent for the same root;
|
|
25
|
+
+ `and ${languageServerDiagnosticsTool(id)} is available. Idempotent for the same root; other roots stay open.`,
|
|
25
26
|
} as const;
|
|
26
27
|
const diagnosticsMetadata = {
|
|
27
28
|
name: languageServerDiagnosticsTool(id),
|
|
28
29
|
nativeTool: true,
|
|
29
30
|
description: `Current ${label} diagnostics (errors, warnings on request) for ${extensions} files from the running `
|
|
30
|
-
+ `language server, without building. Without paths: all changed files of
|
|
31
|
+
+ `language server, without building. Without paths: all changed files of every open root according to git. `
|
|
32
|
+
+ `With paths every file is answered by the instance whose root contains it; root asks one instance.`,
|
|
33
|
+
} as const;
|
|
34
|
+
const closeMetadata = {
|
|
35
|
+
name: languageServerCloseTool(id),
|
|
36
|
+
nativeTool: true,
|
|
37
|
+
description: `Stop the ${label} language server instance of one root; without root every ${label} instance of `
|
|
38
|
+
+ "this conversation. Other languages and other conversations stay untouched.",
|
|
31
39
|
} as const;
|
|
32
40
|
const openTool = defineRunFunction({
|
|
33
41
|
...openMetadata,
|
|
@@ -51,6 +59,7 @@ export const createLanguageServerToolContributor = (
|
|
|
51
59
|
paths: Type.Optional(Type.Array(Type.String(), {
|
|
52
60
|
description: "Files relative to the workspace root; omit for all changed files",
|
|
53
61
|
})),
|
|
62
|
+
root: Type.Optional(Type.String({ description: "Ask only the instance of this open root" })),
|
|
54
63
|
warnings: Type.Optional(Type.Boolean({ description: "Also list warnings (default: only counted)" })),
|
|
55
64
|
}),
|
|
56
65
|
resultSchema: Type.String(),
|
|
@@ -62,12 +71,28 @@ export const createLanguageServerToolContributor = (
|
|
|
62
71
|
...(scope.signal ? { signal: scope.signal } : {}),
|
|
63
72
|
}) as string,
|
|
64
73
|
});
|
|
74
|
+
const closeTool = defineRunFunction({
|
|
75
|
+
...closeMetadata,
|
|
76
|
+
label: `${label} schließen`,
|
|
77
|
+
schema: Type.Object({
|
|
78
|
+
root: Type.Optional(Type.String({ description: "The open root to stop; omit for every instance of this conversation" })),
|
|
79
|
+
}),
|
|
80
|
+
resultSchema: Type.String(),
|
|
81
|
+
available: alwaysAvailable,
|
|
82
|
+
executionMode: "sequential",
|
|
83
|
+
run: async (scope, toolCallId, input) =>
|
|
84
|
+
await sandbox.execute(scope.caller.runId, closeMetadata.name, input, {
|
|
85
|
+
toolCallId,
|
|
86
|
+
...(scope.signal ? { signal: scope.signal } : {}),
|
|
87
|
+
}) as string,
|
|
88
|
+
});
|
|
65
89
|
return {
|
|
66
90
|
name: `ragents.lsp-${id}`,
|
|
67
91
|
descriptors: [
|
|
68
92
|
toolDescriptorFrom(openMetadata, alwaysAvailable),
|
|
69
93
|
toolDescriptorFrom(diagnosticsMetadata, alwaysAvailable),
|
|
94
|
+
toolDescriptorFrom(closeMetadata, alwaysAvailable),
|
|
70
95
|
],
|
|
71
|
-
tools: () => [openTool, diagnosticsTool],
|
|
96
|
+
tools: () => [openTool, diagnosticsTool, closeTool],
|
|
72
97
|
};
|
|
73
98
|
};
|
|
@@ -6,7 +6,7 @@ import { flag, frontMatterOf, numberOf, tagsOf } from "./front-matter.js";
|
|
|
6
6
|
export type ScriptStartEntry = Extract<StartEntryContribution, { action: "script" }>;
|
|
7
7
|
|
|
8
8
|
const PACKAGE_NAME = /^[a-z0-9][a-z0-9-]*$/;
|
|
9
|
-
const RUN_FIELDS = ["title", "description", "order", "guide", "tags", "coordinator"];
|
|
9
|
+
const RUN_FIELDS = ["title", "description", "order", "guide", "tags", "coordinator", "category"];
|
|
10
10
|
|
|
11
11
|
const requiredFile = (directory: string, name: string): string => {
|
|
12
12
|
const file = path.join(directory, name);
|
|
@@ -45,6 +45,8 @@ const runScriptFrom = (directory: string, name: string, id: string): ScriptStart
|
|
|
45
45
|
const order = numberOf(runFile, "order", fields.get("order"));
|
|
46
46
|
const guide = fields.get("guide");
|
|
47
47
|
const tags = tagsOf(runFile, fields.get("tags"));
|
|
48
|
+
const category = fields.get("category");
|
|
49
|
+
if (category !== undefined && !category.trim()) throw new Error(`${runFile}: category ist leer`);
|
|
48
50
|
const programsDirectory = path.join(directory, "actors");
|
|
49
51
|
const programsStats = statSync(programsDirectory, {throwIfNoEntry: false});
|
|
50
52
|
if (programsStats && !programsStats.isDirectory()) throw new Error(`${programsDirectory} muss ein Verzeichnis sein`);
|
|
@@ -60,6 +62,7 @@ const runScriptFrom = (directory: string, name: string, id: string): ScriptStart
|
|
|
60
62
|
...(order !== undefined ? {order} : {}),
|
|
61
63
|
...(guide ? {guide} : {}),
|
|
62
64
|
...(tags !== undefined ? {tags} : {}),
|
|
65
|
+
...(category ? {category} : {}),
|
|
63
66
|
script: {
|
|
64
67
|
handle: name,
|
|
65
68
|
coordinator: flag(runFile, "coordinator", fields.get("coordinator"), true),
|
|
@@ -13,7 +13,7 @@ export interface StartEntryBase {
|
|
|
13
13
|
/** One entry of the start surface as GET /api/plugins publishes it; a script entry never carries its source. */
|
|
14
14
|
export type StartEntry = StartEntryBase & (
|
|
15
15
|
| { action: "skill"; skill: string; category: string; prompt: string }
|
|
16
|
-
| { action: "script"; coordinator: boolean }
|
|
16
|
+
| { action: "script"; coordinator: boolean; category?: string }
|
|
17
17
|
);
|
|
18
18
|
|
|
19
19
|
export type SkillStartEntry = Extract<StartEntry, { action: "skill" }>;
|
|
@@ -62,9 +62,10 @@ export const startEntryFrom = (value: unknown): StartEntry => {
|
|
|
62
62
|
return { ...base, action: "skill", skill: value.skill, category: value.category, prompt: value.prompt };
|
|
63
63
|
}
|
|
64
64
|
case "script":
|
|
65
|
-
allowed(["coordinator"]);
|
|
65
|
+
allowed(["coordinator", "category"]);
|
|
66
66
|
if (typeof value.coordinator !== "boolean") throw invalid(`${id}: coordinator muss true oder false sein`);
|
|
67
|
-
|
|
67
|
+
if (value.category !== undefined && (!text(value.category) || value.category !== value.category.trim())) throw invalid(`${id}: category muss ein einzelner nicht leerer Text sein`);
|
|
68
|
+
return { ...base, action: "script", coordinator: value.coordinator, ...(value.category !== undefined ? { category: value.category } : {}) };
|
|
68
69
|
default:
|
|
69
70
|
throw invalid(`${id}: unbekannte Aktion ${String(value.action)}; gültig sind skill und script`);
|
|
70
71
|
}
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
type WorkspaceExecutor,
|
|
26
26
|
type WorkspaceProcessContext,
|
|
27
27
|
} from "@aicontainer/workspace-executor";
|
|
28
|
+
import { hostRoot } from "../host-version.js";
|
|
28
29
|
import type { SessionWorkspace } from "../ragents/workspace-runtime.js";
|
|
29
30
|
import { agentToolFrom, toolDescriptorFrom, type AgentToolDefinition } from "./agent-tool.js";
|
|
30
31
|
import { alwaysAvailable } from "./tool-availability.js";
|
|
@@ -137,6 +138,7 @@ export class WorkspaceSandboxHost implements SandboxServices {
|
|
|
137
138
|
root: await workspace.currentRoot(),
|
|
138
139
|
home,
|
|
139
140
|
logDirectory: home.home,
|
|
141
|
+
hostRoot: hostRoot(),
|
|
140
142
|
...(ident ? { ident } : {}),
|
|
141
143
|
additionalRoots: await this.#additionalRoots(runId, ident),
|
|
142
144
|
readOnlyRoots,
|
package/apps/web/src/App.tsx
CHANGED
|
@@ -30,7 +30,6 @@ export function App() {
|
|
|
30
30
|
const overviewButtonRef = useRef<HTMLButtonElement>(null);
|
|
31
31
|
const settingsButtonRef = useRef<HTMLButtonElement>(null);
|
|
32
32
|
const [runHeaderContainer, setRunHeaderContainer] = useState<HTMLDivElement | null>(null);
|
|
33
|
-
const [canvasToolbarContainer, setCanvasToolbarContainer] = useState<HTMLDivElement | null>(null);
|
|
34
33
|
const [workspaceHeaderContainer, setWorkspaceHeaderContainer] = useState<HTMLDivElement | null>(null);
|
|
35
34
|
const [statusContainer, setStatusContainer] = useState<HTMLElement | null>(null);
|
|
36
35
|
const [workspaceModalContainer, setWorkspaceModalContainer] = useState<HTMLDivElement | null>(null);
|
|
@@ -80,13 +79,6 @@ export function App() {
|
|
|
80
79
|
if (restoreFocus) overviewButtonRef.current?.focus({ preventScroll: true });
|
|
81
80
|
}, []);
|
|
82
81
|
|
|
83
|
-
useEffect(() => {
|
|
84
|
-
if (!canvasToolbarContainer) return;
|
|
85
|
-
const activateCanvas = () => { closeOverview(); setToolbarPanel(undefined); };
|
|
86
|
-
canvasToolbarContainer.addEventListener("click", activateCanvas);
|
|
87
|
-
return () => canvasToolbarContainer.removeEventListener("click", activateCanvas);
|
|
88
|
-
}, [canvasToolbarContainer, closeOverview]);
|
|
89
|
-
|
|
90
82
|
// Overview contributions keep their state after the first opening.
|
|
91
83
|
const openOverview = useCallback(() => {
|
|
92
84
|
setToolbarPanel(undefined);
|
|
@@ -202,7 +194,6 @@ export function App() {
|
|
|
202
194
|
{busy ? <Spinner aria-hidden className="size-[18px]" /> : <SparklesIcon size={22} />}
|
|
203
195
|
</button>
|
|
204
196
|
</div>}
|
|
205
|
-
<div className="relative flex flex-none self-stretch empty:hidden" ref={setCanvasToolbarContainer} />
|
|
206
197
|
{toolbarPanels.map(({ id, Panel }) => <ToolbarPanelHost
|
|
207
198
|
id={id} key={id} onBusy={setPanelBusy}
|
|
208
199
|
onClose={() => setToolbarPanel(undefined)}
|
|
@@ -230,7 +221,6 @@ export function App() {
|
|
|
230
221
|
{readRuns && activeId
|
|
231
222
|
? (
|
|
232
223
|
<PluginChat
|
|
233
|
-
canvasToolbarContainer={canvasToolbarContainer}
|
|
234
224
|
headerContainer={runHeaderContainer}
|
|
235
225
|
workspaceHeaderContainer={workspaceHeaderContainer}
|
|
236
226
|
statusContainer={statusContainer}
|
|
@@ -55,7 +55,7 @@ export type ChatLayout = "workspace" | "column" | { element: string };
|
|
|
55
55
|
|
|
56
56
|
interface PluginChatProps {
|
|
57
57
|
layout?: ChatLayout;
|
|
58
|
-
|
|
58
|
+
toolbarContainer?: HTMLElement | null;
|
|
59
59
|
statusContainer?: HTMLElement | null;
|
|
60
60
|
headerContainer?: HTMLElement | null;
|
|
61
61
|
workspaceHeaderContainer?: HTMLElement | null;
|
|
@@ -71,7 +71,7 @@ interface PluginChatProps {
|
|
|
71
71
|
|
|
72
72
|
interface ChatWorkspaceProps {
|
|
73
73
|
layout: ChatLayout;
|
|
74
|
-
|
|
74
|
+
toolbarContainer?: HTMLElement | null;
|
|
75
75
|
statusContainer?: HTMLElement | null;
|
|
76
76
|
panelState: WorkspacePanelState;
|
|
77
77
|
onTogglePanel: () => void;
|
|
@@ -90,7 +90,7 @@ interface ChatWorkspaceProps {
|
|
|
90
90
|
tabs: readonly WorkspaceTabContribution[];
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
export function PluginChat({ layout = "workspace", headerContainer,
|
|
93
|
+
export function PluginChat({ layout = "workspace", headerContainer, toolbarContainer, workspaceHeaderContainer, statusContainer, initialStartOptions, onStarted, onLocationChange, onViewed, viewing = false, registry, session, startDialog }: PluginChatProps) {
|
|
94
94
|
const access = useAccess();
|
|
95
95
|
const startOnly = startDialog !== undefined;
|
|
96
96
|
const [modalContainer, setModalContainer] = useState<HTMLDivElement | null>(null);
|
|
@@ -235,7 +235,7 @@ export function PluginChat({ layout = "workspace", headerContainer, canvasToolba
|
|
|
235
235
|
panelVisible={panelState === "expanded"} runId={session.id} tabs={availableTabs} />}
|
|
236
236
|
<ChatWorkspace
|
|
237
237
|
layout={layout}
|
|
238
|
-
|
|
238
|
+
toolbarContainer={toolbarContainer}
|
|
239
239
|
panelState={panelState}
|
|
240
240
|
onTogglePanel={() => updatePanelState(panelState === "expanded" ? "collapsed" : "expanded")}
|
|
241
241
|
headerContainer={headerContainer}
|
|
@@ -281,7 +281,7 @@ function UserLocationReporter({ activeTabId, onChange, panelVisible, runId, tabs
|
|
|
281
281
|
|
|
282
282
|
function ChatWorkspace({
|
|
283
283
|
layout,
|
|
284
|
-
|
|
284
|
+
toolbarContainer = null,
|
|
285
285
|
panelState,
|
|
286
286
|
onTogglePanel,
|
|
287
287
|
headerContainer,
|
|
@@ -385,7 +385,7 @@ function ChatWorkspace({
|
|
|
385
385
|
? <CanvasElementView elementId={layout.element} navigation={navigation} registry={registry} session={session} />
|
|
386
386
|
: CanvasColumn
|
|
387
387
|
? <CanvasColumn
|
|
388
|
-
toolbarContainer={
|
|
388
|
+
toolbarContainer={toolbarContainer}
|
|
389
389
|
runToolbarContainer={null}
|
|
390
390
|
statusContainer={statusContainer}
|
|
391
391
|
cardSections={registry.cardSections}
|
|
@@ -420,7 +420,7 @@ function ChatWorkspace({
|
|
|
420
420
|
{CanvasCenter
|
|
421
421
|
? (
|
|
422
422
|
<CanvasCenter
|
|
423
|
-
toolbarContainer={
|
|
423
|
+
toolbarContainer={toolbarContainer}
|
|
424
424
|
runToolbarContainer={runToolbarContainer}
|
|
425
425
|
statusContainer={statusContainer}
|
|
426
426
|
cardSections={registry.cardSections}
|
|
@@ -477,7 +477,7 @@ function CanvasElementView({ elementId, navigation, registry, session }: {
|
|
|
477
477
|
{session.runView === undefined ? "Der Run wird geladen ..." : "Diese Mini-App gibt es in dem Run nicht mehr."}
|
|
478
478
|
</div>;
|
|
479
479
|
return <div className="flex min-h-0 min-w-0 flex-1 flex-col" data-view="element">
|
|
480
|
-
<match.Element definition={match.definition} navigation={navigation}
|
|
480
|
+
<match.Element definition={match.definition} navigation={navigation} session={session} />
|
|
481
481
|
</div>;
|
|
482
482
|
}
|
|
483
483
|
|
|
@@ -152,25 +152,13 @@ export interface CanvasElementDefinition {
|
|
|
152
152
|
id: string;
|
|
153
153
|
visible?: boolean;
|
|
154
154
|
title?: string;
|
|
155
|
-
width: number;
|
|
156
|
-
height: number;
|
|
157
|
-
collapsedHeight?: number;
|
|
158
155
|
anchorActorId?: string;
|
|
159
156
|
entity?: EntityReference;
|
|
160
|
-
resizable?: {
|
|
161
|
-
minWidth: number;
|
|
162
|
-
maxWidth: number;
|
|
163
|
-
minHeight: number;
|
|
164
|
-
maxHeight: number;
|
|
165
|
-
};
|
|
166
157
|
data?: unknown;
|
|
167
158
|
}
|
|
168
159
|
|
|
169
160
|
export interface CanvasElementContext {
|
|
170
161
|
definition: CanvasElementDefinition;
|
|
171
|
-
presentation?: "canvas" | "tiled";
|
|
172
|
-
collapsed?: boolean;
|
|
173
|
-
onCollapsedChange?: (collapsed: boolean) => void;
|
|
174
162
|
navigation: SessionNavigation;
|
|
175
163
|
session: SessionContext;
|
|
176
164
|
}
|
|
@@ -120,13 +120,13 @@ export function StartEntryLibrary({ skills, scripts, launchDisabled, onOpen, has
|
|
|
120
120
|
const sorted = useMemo(() => [...[...skills].sort(byOrder), ...[...scripts].sort(byOrder)], [skills, scripts]);
|
|
121
121
|
const tags = [...new Set(sorted.flatMap((entry) => entry.tags ?? []))].sort((a, b) => a.localeCompare(b, "de-DE"));
|
|
122
122
|
const filtered = sorted.filter((entry) => (!tag || entry.tags?.includes(tag))
|
|
123
|
-
&& `${entry.title} ${entry.description} ${entry.
|
|
123
|
+
&& `${entry.title} ${entry.description} ${entry.category ?? kindLabel(entry)} ${entry.owner} ${(entry.tags ?? []).join(" ")}`
|
|
124
124
|
.toLocaleLowerCase("de-DE").includes(query.trim().toLocaleLowerCase("de-DE")));
|
|
125
125
|
const selected = filtered.find((entry) => entry.id === selectedId) ?? filtered[0];
|
|
126
126
|
const guided = selected && hasGuide(selected);
|
|
127
127
|
const tagOptions = [{ value: "", label: "Alle Schlagworte" }, ...tags.map((value) => ({ value, label: value }))];
|
|
128
128
|
const items: ListDetailItem[] = filtered.map((entry) => ({ id: entry.id, title: entry.title,
|
|
129
|
-
description: entry.description, group: entry.
|
|
129
|
+
description: entry.description, group: entry.category ?? (technical ? "Run-Scripts" : "Abläufe"),
|
|
130
130
|
icon: entryIcon(entry), meta: technical ? kindLabel(entry) : undefined, tone: entry.action === "skill" ? "purple" : "success" }));
|
|
131
131
|
|
|
132
132
|
return <ListDetail className="min-h-0 flex-1" label="Skills und Abläufe" detailLabel="Vorschau" items={items}
|
|
@@ -25,7 +25,7 @@ export function ThemeSettings() {
|
|
|
25
25
|
</div>
|
|
26
26
|
<div className="flex-[1_1_220px]">
|
|
27
27
|
<h3 className="mb-1.5 text-lg font-semibold" id="theme-settings-title">Schichtwerk</h3>
|
|
28
|
-
<p className={noteClasses}>Matte Flächen
|
|
28
|
+
<p className={noteClasses}>Matte Flächen und gezeichnete Konturen.</p>
|
|
29
29
|
</div>
|
|
30
30
|
<ToggleGroup aria-label="Schichtwerk-Farbschema" className="flex-none max-sm:w-full" disabled={!writable} size="sm" spacing={0}
|
|
31
31
|
value={[preference]} variant="outline"
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { useCallback, useEffect, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { ChevronDownIcon, ChevronUpIcon, LayoutGridIcon } from "lucide-react";
|
|
3
|
-
import { canStartEntry } from "../../../../packages/ragents/src/access";
|
|
3
|
+
import { canStartEntry, type AccessContext } from "../../../../packages/ragents/src/access";
|
|
4
|
+
import { getStartOptions, setStartOption } from "../api";
|
|
5
|
+
import { sendChatMessage, startChatEntry } from "../chat/requests";
|
|
6
|
+
import { preparedRunInput } from "../run-preparation";
|
|
7
|
+
import { initialStartOptionUpdates } from "../StartOptions";
|
|
4
8
|
import { AccessScreen, useAccess } from "../AccessContext";
|
|
5
9
|
import type { SessionInfo } from "../api";
|
|
6
10
|
import { PluginChat } from "../PluginChat";
|
|
@@ -9,7 +13,7 @@ import { usePluginActivation } from "../PluginActivation";
|
|
|
9
13
|
import { useRunReadState } from "../run-read-state";
|
|
10
14
|
import { SessionList } from "../SessionList";
|
|
11
15
|
import { SettingsModal } from "../SettingsModal";
|
|
12
|
-
import { Alert, Button } from "../ui";
|
|
16
|
+
import { Alert, Button, Spinner } from "../ui";
|
|
13
17
|
import type { ColumnLocation } from "./column-location";
|
|
14
18
|
import { ColumnMenu } from "./ColumnMenu";
|
|
15
19
|
import { useColumnHost } from "./host";
|
|
@@ -45,25 +49,40 @@ function ColumnPage({ initialRunId, registry }: { initialRunId: string | undefin
|
|
|
45
49
|
const canCreate = writeRuns && (access.can("runs.create") || registry.scriptEntries.some((entry) => canStartEntry(access, entry.id)));
|
|
46
50
|
const runReadState = useRunReadState(access.user?.id);
|
|
47
51
|
const [runId, setRunId] = useState(initialRunId);
|
|
52
|
+
const [runStartOptions, setRunStartOptions] = useState<Readonly<Record<string, unknown>>>();
|
|
48
53
|
const [draft, setDraft] = useState<{ session: SessionInfo; startOptions?: Readonly<Record<string, unknown>>; entryId?: string }>();
|
|
54
|
+
const [launch, setLaunch] = useState<Launch>();
|
|
49
55
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
50
56
|
const [listOpen, setListOpen] = useState(false);
|
|
51
57
|
const openSettings = useCallback(() => setSettingsOpen(true), []);
|
|
52
|
-
const { sessions, unreachable, refresh } = useSessionList(readRuns && !draft);
|
|
58
|
+
const { sessions, unreachable, refresh } = useSessionList(readRuns && !draft && !launch);
|
|
53
59
|
const [headerContainer, setHeaderContainer] = useState<HTMLDivElement | null>(null);
|
|
54
60
|
const [toolsContainer, setToolsContainer] = useState<HTMLDivElement | null>(null);
|
|
55
61
|
const startDraft = useCallback((startOptions?: Readonly<Record<string, unknown>>, entryId?: string) => { setListOpen(false); setDraft({ session: newDraft(), startOptions, entryId }); }, []);
|
|
56
62
|
const createRun = useCallback(() => startDraft(), [startDraft]);
|
|
63
|
+
const openRun = useCallback((id: string, startOptions?: Readonly<Record<string, unknown>>) => {
|
|
64
|
+
setListOpen(false);
|
|
65
|
+
setDraft(undefined);
|
|
66
|
+
setLaunch(undefined);
|
|
67
|
+
setRunStartOptions(startOptions);
|
|
68
|
+
setRunId(id);
|
|
69
|
+
}, []);
|
|
57
70
|
const selectRun = useCallback((id: string) => { setListOpen(false); setRunId(id); }, []);
|
|
58
71
|
|
|
59
72
|
useEffect(() => host.onCommand((message) => {
|
|
60
73
|
if (message.type === "selectRun") {
|
|
61
74
|
setDraft(undefined);
|
|
75
|
+
setLaunch(undefined);
|
|
62
76
|
setListOpen(false);
|
|
77
|
+
setRunStartOptions(undefined);
|
|
63
78
|
setRunId(message.runId ?? undefined);
|
|
64
79
|
}
|
|
65
|
-
if (message.type
|
|
66
|
-
|
|
80
|
+
if (message.type !== "newRun" || !canCreate) return;
|
|
81
|
+
// Im Panel ist ein Klick ein Klick: die Vorlage startet sofort, der freie Auftrag entsteht im Chat des neuen Runs.
|
|
82
|
+
if (host.kind !== "vscode") startDraft(message.startOptions, message.entryId);
|
|
83
|
+
else if (message.entryId === undefined) openRun(crypto.randomUUID(), message.startOptions);
|
|
84
|
+
else setLaunch({ runId: crypto.randomUUID(), entryId: message.entryId, startOptions: message.startOptions });
|
|
85
|
+
}), [canCreate, host, openRun, startDraft]);
|
|
67
86
|
useEffect(() => { host.notify({ type: "ready" }); }, [host]);
|
|
68
87
|
useEffect(() => { host.notify({ type: "runChanged", runId: runId ?? null }); }, [host, runId]);
|
|
69
88
|
useEffect(() => {
|
|
@@ -73,6 +92,9 @@ function ColumnPage({ initialRunId, registry }: { initialRunId: string | undefin
|
|
|
73
92
|
return () => window.removeEventListener("keydown", onKeyDown);
|
|
74
93
|
}, [listOpen]);
|
|
75
94
|
|
|
95
|
+
if (launch) return <RunLaunch access={access} launch={launch} onCancel={() => setLaunch(undefined)} onOpenSettings={openSettings}
|
|
96
|
+
onStarted={openRun} registry={registry} />;
|
|
97
|
+
|
|
76
98
|
const session = sessions.find((entry) => entry.id === runId);
|
|
77
99
|
const list = readRuns
|
|
78
100
|
? <SessionList activeId={runId} onCreate={canCreate ? createRun : undefined} onSelect={selectRun} onToggleSelected={() => undefined} registry={registry} seenRevisions={runReadState.revisions} selectMode={false} selectedIds={noSelection} sessions={sessions} />
|
|
@@ -90,8 +112,9 @@ function ColumnPage({ initialRunId, registry }: { initialRunId: string | undefin
|
|
|
90
112
|
</header>
|
|
91
113
|
<main className="relative z-1 flex min-h-0 min-w-0 flex-1 @container/chat-content">
|
|
92
114
|
<PluginChat
|
|
93
|
-
|
|
115
|
+
toolbarContainer={toolsContainer}
|
|
94
116
|
headerContainer={headerContainer}
|
|
117
|
+
initialStartOptions={runStartOptions}
|
|
95
118
|
key={runId}
|
|
96
119
|
layout="column"
|
|
97
120
|
onViewed={runReadState.markViewed}
|
|
@@ -134,6 +157,59 @@ function ColumnPage({ initialRunId, registry }: { initialRunId: string | undefin
|
|
|
134
157
|
);
|
|
135
158
|
}
|
|
136
159
|
|
|
160
|
+
interface Launch {
|
|
161
|
+
readonly runId: string;
|
|
162
|
+
readonly entryId: string;
|
|
163
|
+
readonly startOptions?: Readonly<Record<string, unknown>>;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Ein Klick auf eine Kachel der Übersicht: Startoptionen setzen, den Einstieg starten, fertig. */
|
|
167
|
+
async function launchRun(launch: Launch, registry: PluginRegistry, access: AccessContext): Promise<void> {
|
|
168
|
+
const entry = registry.startEntries.find((candidate) => candidate.id === launch.entryId);
|
|
169
|
+
if (!entry) throw new Error("Diese Startvorlage gibt es in diesem Profil nicht.");
|
|
170
|
+
if (!canStartEntry(access, entry.id)) throw new Error(`${entry.title} ist für dieses Benutzerkonto nicht freigegeben.`);
|
|
171
|
+
if (launch.startOptions) {
|
|
172
|
+
const options = await getStartOptions(launch.runId);
|
|
173
|
+
for (const [id, value] of initialStartOptionUpdates(options, launch.startOptions)) await setStartOption(launch.runId, id, value);
|
|
174
|
+
}
|
|
175
|
+
if (entry.action === "script") await startChatEntry(launch.runId, entry.id, null);
|
|
176
|
+
else await sendChatMessage(launch.runId, preparedRunInput([], entry.prompt, undefined, entry.skill).text);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function RunLaunch({ launch, registry, access, onStarted, onCancel, onOpenSettings }: {
|
|
180
|
+
launch: Launch;
|
|
181
|
+
registry: PluginRegistry;
|
|
182
|
+
access: AccessContext;
|
|
183
|
+
onStarted: (runId: string) => void;
|
|
184
|
+
onCancel: () => void;
|
|
185
|
+
onOpenSettings: () => void;
|
|
186
|
+
}) {
|
|
187
|
+
const [error, setError] = useState<string>();
|
|
188
|
+
const running = useRef(false);
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (running.current) return;
|
|
191
|
+
running.current = true;
|
|
192
|
+
void launchRun(launch, registry, access).then(
|
|
193
|
+
() => onStarted(launch.runId),
|
|
194
|
+
(cause: unknown) => setError(cause instanceof Error ? cause.message : String(cause)),
|
|
195
|
+
);
|
|
196
|
+
}, [access, launch, onStarted, registry]);
|
|
197
|
+
return (
|
|
198
|
+
<div className="flex h-full flex-col bg-[image:var(--canvas-backdrop)]">
|
|
199
|
+
<header className={listHeaderClass}>
|
|
200
|
+
<LayoutGridIcon aria-hidden className="size-4 flex-none text-muted-foreground" />
|
|
201
|
+
<strong className="min-w-0 flex-1 truncate text-[0.82rem] font-semibold">{registry.brand.title}</strong>
|
|
202
|
+
<ColumnMenu onOpenSettings={onOpenSettings} />
|
|
203
|
+
</header>
|
|
204
|
+
<div className="flex min-h-0 flex-1 items-center justify-center p-6">
|
|
205
|
+
{error
|
|
206
|
+
? <Alert className="max-w-[420px] gap-3" variant="destructive"><p>{error}</p><Button onClick={onCancel} variant="outline">Zurück</Button></Alert>
|
|
207
|
+
: <p className="flex items-center gap-2 text-muted-foreground" role="status"><Spinner />Run wird gestartet ...</p>}
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
137
213
|
function ElementPage({ elementId, registry, runId }: { elementId: string; registry: PluginRegistry; runId: string }) {
|
|
138
214
|
const host = useColumnHost();
|
|
139
215
|
const readRuns = useAccess().can("runs.read");
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { isClipboardColumnMessage } from "./host-contract";
|
|
2
|
+
|
|
3
|
+
type EditingAction = "paste" | "copy" | "cut";
|
|
4
|
+
|
|
5
|
+
const actionOf = (event: KeyboardEvent): EditingAction | undefined => {
|
|
6
|
+
if (!(event.metaKey || event.ctrlKey) || event.altKey) return undefined;
|
|
7
|
+
switch (event.key) {
|
|
8
|
+
case "v": return "paste";
|
|
9
|
+
case "c": return "copy";
|
|
10
|
+
case "x": return "cut";
|
|
11
|
+
default: return undefined;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const isEditable = (element: Element | null): element is HTMLElement =>
|
|
16
|
+
element !== null && (element.tagName === "INPUT" || element.tagName === "TEXTAREA" || (element as HTMLElement).isContentEditable === true);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* macOS liefert Einfügen, Kopieren und Ausschneiden über das Menü der Anwendung, und VS Code reicht
|
|
20
|
+
* den Befehl nur an das Dokument seines Webviews weiter, nicht in ein iframe fremder Herkunft. Die
|
|
21
|
+
* Spalte führt ihn deshalb selbst aus; den Text der Zwischenablage holt sie über die Hülle.
|
|
22
|
+
*/
|
|
23
|
+
export function installClipboardBridge(browser: Window): () => void {
|
|
24
|
+
const parent = browser.parent;
|
|
25
|
+
const pending = new Map<string, (text: string) => void>();
|
|
26
|
+
|
|
27
|
+
const onMessage = (event: MessageEvent) => {
|
|
28
|
+
if (event.source !== parent || !isClipboardColumnMessage(event.data)) return;
|
|
29
|
+
const settle = pending.get(event.data.id);
|
|
30
|
+
if (!settle) return;
|
|
31
|
+
pending.delete(event.data.id);
|
|
32
|
+
settle(event.data.text);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const read = () => new Promise<string>((resolve) => {
|
|
36
|
+
const id = crypto.randomUUID();
|
|
37
|
+
pending.set(id, resolve);
|
|
38
|
+
parent.postMessage({ type: "clipboardRead", id }, "*");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
42
|
+
const action = actionOf(event);
|
|
43
|
+
if (action === undefined || event.defaultPrevented) return;
|
|
44
|
+
const target = browser.document.activeElement;
|
|
45
|
+
if (action !== "copy" && !isEditable(target)) return;
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
if (action !== "paste") {
|
|
48
|
+
browser.document.execCommand(action);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
void read().then((text) => {
|
|
52
|
+
if (!text || !isEditable(target)) return;
|
|
53
|
+
target.focus();
|
|
54
|
+
browser.document.execCommand("insertText", false, text);
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
browser.addEventListener("message", onMessage);
|
|
59
|
+
browser.addEventListener("keydown", onKeyDown, true);
|
|
60
|
+
return () => {
|
|
61
|
+
browser.removeEventListener("message", onMessage);
|
|
62
|
+
browser.removeEventListener("keydown", onKeyDown, true);
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -31,6 +31,10 @@ export type ColumnHostMessage =
|
|
|
31
31
|
| { type: "openExternal"; url: string }
|
|
32
32
|
| { type: "openPage"; url: string; title: string };
|
|
33
33
|
|
|
34
|
+
/** Der Text der Zwischenablage: ein iframe fremder Herkunft darf sie im Webview nicht lesen, die Hülle darüber schon. */
|
|
35
|
+
export type ColumnClipboardMessage = { type: "clipboardRead"; id: string };
|
|
36
|
+
export type ClipboardColumnMessage = { type: "clipboardText"; id: string; text: string };
|
|
37
|
+
|
|
34
38
|
/** Nachrichten des Hosts an das Panel. */
|
|
35
39
|
export type HostColumnMessage =
|
|
36
40
|
| { type: "selectRun"; runId: string | null }
|
|
@@ -65,6 +69,12 @@ export const isColumnHostMessage = (value: unknown): value is ColumnHostMessage
|
|
|
65
69
|
}
|
|
66
70
|
};
|
|
67
71
|
|
|
72
|
+
export const isColumnClipboardMessage = (value: unknown): value is ColumnClipboardMessage =>
|
|
73
|
+
isObject(value) && value.type === "clipboardRead" && isText(value.id);
|
|
74
|
+
|
|
75
|
+
export const isClipboardColumnMessage = (value: unknown): value is ClipboardColumnMessage =>
|
|
76
|
+
isObject(value) && value.type === "clipboardText" && isText(value.id) && typeof value.text === "string";
|
|
77
|
+
|
|
68
78
|
export const isHostColumnMessage = (value: unknown): value is HostColumnMessage => {
|
|
69
79
|
if (!isObject(value)) return false;
|
|
70
80
|
switch (value.type) {
|
package/apps/web/src/column.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import { createRoot } from "react-dom/client";
|
|
|
7
7
|
import { Button } from "./ui";
|
|
8
8
|
import { AccessGate, AccessScreen } from "./AccessContext";
|
|
9
9
|
import { installAccessToken } from "./access-token";
|
|
10
|
+
import { installClipboardBridge } from "./column/clipboard";
|
|
10
11
|
import { ColumnApp, HostLogin } from "./column/ColumnApp";
|
|
11
12
|
import { parseColumnLocation } from "./column/column-location";
|
|
12
13
|
import { ColumnHostProvider, createColumnHost } from "./column/host";
|
|
@@ -21,6 +22,7 @@ try {
|
|
|
21
22
|
const location = parseColumnLocation(window.location.search);
|
|
22
23
|
if (location.access !== undefined) installAccessToken(location.access);
|
|
23
24
|
const host = createColumnHost(location.host, window);
|
|
25
|
+
if (location.host === "vscode") installClipboardBridge(window);
|
|
24
26
|
const theme = initializeTheme(window);
|
|
25
27
|
if (location.theme !== undefined) theme.setPreference(location.theme);
|
|
26
28
|
host.onCommand((message) => { if (message.type === "theme") theme.setPreference(message.theme); });
|
|
@@ -2,13 +2,14 @@ import { useState, type FormEvent } from "react";
|
|
|
2
2
|
import { Button, Input, Label } from "../ui";
|
|
3
3
|
import type { PanelAction, TargetView } from "./contract";
|
|
4
4
|
|
|
5
|
-
/** Die Anmeldung
|
|
6
|
-
export function LoginForm({ target, mode, busy, send, submitLabel }: {
|
|
5
|
+
/** Die Anmeldung einer Umgebung im Dialogkörper: Benutzer und Passwort oder ein Zugangstoken. */
|
|
6
|
+
export function LoginForm({ target, mode, busy, send, submitLabel, onCancel }: {
|
|
7
7
|
target: TargetView;
|
|
8
8
|
mode: "password" | "token";
|
|
9
9
|
busy: boolean;
|
|
10
10
|
send: (action: PanelAction) => void;
|
|
11
11
|
submitLabel: string;
|
|
12
|
+
onCancel?: () => void;
|
|
12
13
|
}) {
|
|
13
14
|
const [user, setUser] = useState(target.loginUser ?? "");
|
|
14
15
|
const [password, setPassword] = useState("");
|
|
@@ -26,6 +27,11 @@ export function LoginForm({ target, mode, busy, send, submitLabel }: {
|
|
|
26
27
|
<div className="grid gap-1.5"><Label htmlFor={`user-${field}`}>Benutzer</Label><Input autoComplete="username" disabled={busy} id={`user-${field}`} onChange={(event) => setUser(event.target.value)} value={user} /></div>
|
|
27
28
|
<div className="grid gap-1.5"><Label htmlFor={`password-${field}`}>Passwort</Label><Input autoComplete="current-password" disabled={busy} id={`password-${field}`} onChange={(event) => setPassword(event.target.value)} type="password" value={password} /></div>
|
|
28
29
|
</>}
|
|
29
|
-
<
|
|
30
|
+
{target.problem && <p className="text-[0.75rem] leading-normal text-destructive [overflow-wrap:anywhere]" role="alert">{target.problem}</p>}
|
|
31
|
+
<p className="text-[0.72rem] leading-normal text-muted-foreground">Die Anmeldedaten liegen im Schlüsselspeicher von VS Code; die Umgebung meldet sich danach still an.</p>
|
|
32
|
+
<div className="mt-1 flex flex-wrap justify-end gap-2">
|
|
33
|
+
{onCancel && <Button onClick={onCancel} size="sm" type="button" variant="ghost">Abbrechen</Button>}
|
|
34
|
+
<Button disabled={busy || !complete} size="sm" type="submit">{submitLabel}</Button>
|
|
35
|
+
</div>
|
|
30
36
|
</form>;
|
|
31
37
|
}
|